@realitycollective/webxr-interactions 0.1.0-preview.6 → 0.1.1-preview.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,7 +4,25 @@ Change log for the Reality Collective WebXR Interaction Extensions packages. All
4
4
 
5
5
  The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Preview builds are not listed separately. The entry for a version accumulates while its previews are published, and is dated when that version is released.
6
6
 
7
- ## [0.1.0]
7
+ ## [0.1.1]
8
+
9
+ ### Added
10
+
11
+ - `@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.
12
+ - `@realitycollective/native-interactions` - a native host adapter, the fifth platform. It reads the `input` and `interactions` slices a native app (OpenXR on Quest, CompositorServices on visionOS) installs on `globalThis.__rcHost`, or hands in directly, and maps them onto `InputProvider`, `HitTester` and `TransformPort` with no engine dependency at all - assets and rendering stay in the native app, and only tuples and target ids cross the boundary. Every snapshot, hit and pose read across that boundary is copied, so a host that reuses its own buffers cannot reach an object the app is still holding, and optional members (`getHeadPose`, `sampleHints`, `pulse`, `setPresenceVisible`, `setPresenceModality`, `setWorldPose`, `setEffect`) are present only when the host slice itself carries them. `createNativeInteractions()` is the one-call setup, with the same shape as the other adapters' setups; `attachToHost` drives updates from the app's frame callback. The public surface matches the other adapters, and slice reading and tuple copying stay internal. Covered headlessly by an in-memory fake of both slices, including the shared `InputProvider` contract suite.
13
+
14
+ ### Changed
15
+
16
+ - `@realitycollective/webxr-interactions` - `TransformPort.getWorldPose()` is the LIVE world pose on every platform, and the new required `getRestWorldPose()` returns the rest pose captured at registration, in world space. The core only said "current world pose", and the platforms disagreed: the three.js, IWSDK and XR Blocks ports returned the rest pose, while the Babylon.js port returned the live one. The same behaviour therefore acted differently by platform. The hinge, slide and dial behaviours measure the hand against the rest pose, so they now call `getRestWorldPose()`. Grab and toss scoring keep `getWorldPose()` and now see where the object really is. `ports.ts` also states tuple ownership for `TransformPort` and `HitTester`: every returned tuple is fresh and owned by the caller, and a tuple passed in is read during the call and not kept. Every platform already met the ownership rule; the suites now hold them to it. The native `interactions` slice gains `getRestWorldPose(targetId)`, which the native app must implement. This adds a required member to `TransformPort`, so a port written outside this repository must implement it.
17
+ - `@realitycollective/xrblocks-interactions` - the structural slices `XBVec3Like`, `XBQuatLike`, `XBRayLike` and `XBControllerLike` are exported, under the same `XB` prefix as `XBRaySourceLike`, `XBDirectTouchLike` and `XBFrameLike` whose fields they type. They were module-private, so a consumer building a fake frame for a test had nothing to name, and TypeDoc reported each as referenced but undocumented.
18
+
19
+ ### Fixed
20
+
21
+ - `@realitycollective/threejs-interactions`, `@realitycollective/iwsdk-interactions` and `@realitycollective/xrblocks-interactions` - `getWorldPose()` returned the pose captured at registration, not where the object was. Toss scoring therefore read a thrown ball at its registered position until the app called `recaptureRest()`, and a second grab of an object moved without a recapture took its hold offset from the old position. `getWorldPose()` now returns the live pose, as the Babylon.js port already did.
22
+ - `@realitycollective/webxr-interactions` - `InteractableDescriptor.pokeRadius` had no effect. The value was stored on the registration and documented as the poke trigger radius, but the proximity query always used the 5 cm default, so an interactable asking for a wider or narrower poke got the default. The runtime now queries once per source at the largest radius any enabled interactable registered, and accepts the nearest hit only when it lies inside that interactable's own radius. One query per source is kept deliberately; the trade is that a farther interactable with a larger radius is not found behind a nearer one with a smaller radius, because the hit tester returns only the nearest. Covered by a runtime test that failed before the change.
23
+ - CI - the staging deploy published under `--branch=pr-<number>` while the `-test` Pages project's production branch is `staging`, so only `pr-<n>.webxr-interactions-test.pages.dev` aliases were ever created and the project's root URL was a 404. It now deploys as `staging` on every pull request and on every push to `development`, so `webxr-interactions-test.pages.dev` serves the newest preview build, the same arrangement WebXR-UIExtensions already had.
24
+
25
+ ## [0.1.0] - 2026-09-17
8
26
 
9
27
  ### Added
10
28
 
@@ -57,4 +75,5 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
57
75
  - All five packages depend on `@realitycollective/webxr-input`, released independently from the [WebXR-Input](https://github.com/realitycollective/WebXR-Input) repository. That package must be published before this one.
58
76
  - The interaction packages themselves carry no IWSDK coupling beyond the adapter: `webxr-interactions` is engine-free, and `threejs-interactions` and `xrblocks-interactions` peer only on three.js.
59
77
 
60
- [0.1.0]: https://github.com/realitycollective/WebXR-Interactions/commits/main
78
+ [0.1.1]: https://github.com/realitycollective/WebXR-Interactions/compare/v0.1.0...development
79
+ [0.1.0]: https://github.com/realitycollective/WebXR-Interactions/releases/tag/v0.1.0
package/README.md CHANGED
@@ -29,6 +29,10 @@ All input arrives through the shared [`@realitycollective/webxr-input`](https://
29
29
  - **Events out, never callbacks in.** The core never reaches into your objects.
30
30
  - **Fail loudly, not silently.** A behaviour the hardware cannot support reports `behaviourDisabled` instead of quietly doing nothing.
31
31
 
32
+ ## Conformance suites
33
+
34
+ Every adapter implements `HitTester` and `TransformPort` (see `ports.ts`) against its own scene graph, so this package ships the checks as data rather than as tests: `hitTesterContractCases()` and `transformPortContractCases()`, alongside `inputProviderContractCases()` from [`@realitycollective/webxr-input`](https://www.npmjs.com/package/@realitycollective/webxr-input) for `InputProvider`. Each case throws a plain `Error` naming the rule a platform broke, so any test runner can host it - an adapter written outside this repository runs the same suite its own way. All three run against every platform in `packages/iwsdk-interactions/test/`, next to each other: `provider-parity.test.ts` for `InputProvider`, `port-parity.test.ts` for `HitTester` and `TransformPort`.
35
+
32
36
  ## Live demo
33
37
 
34
38
  The interaction playground - the full station set, mouse-capable on desktop, VR button for headsets: **[webxr-interactions.pages.dev](https://webxr-interactions.pages.dev)**
@@ -35,7 +35,7 @@ export interface BehaviourContext {
35
35
  transform: TransformPort | undefined;
36
36
  emit(event: Omit<InteractionEvent, "interactableId">): void;
37
37
  feedback(intent: Omit<FeedbackIntent, "interactableId">): void;
38
- /** REST world pose of another registered interactable (toss scoring). */
38
+ /** LIVE world pose of another registered interactable (toss scoring). */
39
39
  getWorldPose(interactableId: string): PoseTuple | undefined;
40
40
  }
41
41
  export interface Behaviour {
@@ -1 +1 @@
1
- {"version":3,"file":"behaviour.js","sourceRoot":"","sources":["../../src/behaviours/behaviour.ts"],"names":[],"mappings":"AA2EA,0EAA0E;AAC1E,MAAM,UAAU,WAAW,CAAC,MAAsB;IAChD,OAAO,MAAM,CAAC,QAAQ,IAAI,MAAM,CAAC,QAAQ,EAAE,QAAQ,IAAI,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC;AAC5E,CAAC","sourcesContent":["/**\n * The behaviour contract - a mechanism attached to an interactable.\n *\n * Behaviours are pure state machines: they receive routed input moments\n * (press/grab transitions), tick from `update(dt)`, and write their output\n * through the interactable's {@link TransformPort}. They declare:\n *\n * - `ownership` - who owns the transform (the formalised pale-signal\n * driven / dragged / handDriven / native models), so a binder knows\n * without reading code;\n * - `requires` / `requiresAnyOf` - the input capabilities they need;\n * capability negotiation disables unsatisfiable behaviours VISIBLY\n * (a `behaviourDisabled` event with the unmet list), never silently.\n */\nimport type {\n InputCapabilityRequirement,\n InputSourceKind,\n PoseTuple,\n RayTuple,\n Vec3Tuple,\n} from \"@realitycollective/webxr-input\";\nimport type { InteractionEvent } from \"../events.js\";\nimport type { FeedbackIntent } from \"../feedback.js\";\nimport type { TransformPort } from \"../ports.js\";\n\n/** Who owns the interactable's transform while this behaviour is active. */\nexport type TransformOwnership = \"driven\" | \"dragged\" | \"handDriven\" | \"native\";\n\n/** The slice of a live interactor a behaviour may consume. */\nexport interface InteractorInfo {\n id: string;\n kind: InputSourceKind;\n ray?: RayTuple;\n gripPose?: PoseTuple;\n indexTip?: Vec3Tuple;\n select: number;\n squeeze: number;\n}\n\n/** Everything a behaviour can reach during a tick or a routed moment. */\nexport interface BehaviourContext {\n dt: number;\n interactableId: string;\n transform: TransformPort | undefined;\n emit(event: Omit<InteractionEvent, \"interactableId\">): void;\n feedback(intent: Omit<FeedbackIntent, \"interactableId\">): void;\n /** REST world pose of another registered interactable (toss scoring). */\n getWorldPose(interactableId: string): PoseTuple | undefined;\n}\n\nexport interface Behaviour {\n readonly kind: string;\n readonly ownership: TransformOwnership;\n /** ALL of these must be satisfied. */\n readonly requires: readonly InputCapabilityRequirement[];\n /** For each group, AT LEAST ONE must be satisfied. */\n readonly requiresAnyOf?: readonly (readonly InputCapabilityRequirement[])[];\n /** True when this behaviour makes the interactable a grab target. */\n readonly grabbable: boolean;\n\n onPressStart?(ctx: BehaviourContext, interactor: InteractorInfo): void;\n onPressEnd?(ctx: BehaviourContext, interactor: InteractorInfo): void;\n onGrabStart?(ctx: BehaviourContext, interactor: InteractorInfo): void;\n onGrabEnd?(ctx: BehaviourContext, interactor: InteractorInfo): void;\n\n /**\n * Per-frame tick. `holder` is set while a grab interactor holds this\n * interactable (grab-family behaviours consume it; others ignore it).\n */\n update(ctx: BehaviourContext, holder?: InteractorInfo): void;\n\n /** Current logical 0..1 value (state snapshots, HUDs). */\n getValue(): number;\n}\n\n/** Hand position a hand-driven behaviour should track, best available. */\nexport function holderPoint(holder: InteractorInfo): Vec3Tuple | undefined {\n return holder.indexTip ?? holder.gripPose?.position ?? holder.ray?.origin;\n}\n"]}
1
+ {"version":3,"file":"behaviour.js","sourceRoot":"","sources":["../../src/behaviours/behaviour.ts"],"names":[],"mappings":"AA2EA,0EAA0E;AAC1E,MAAM,UAAU,WAAW,CAAC,MAAsB;IAChD,OAAO,MAAM,CAAC,QAAQ,IAAI,MAAM,CAAC,QAAQ,EAAE,QAAQ,IAAI,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC;AAC5E,CAAC","sourcesContent":["/**\n * The behaviour contract - a mechanism attached to an interactable.\n *\n * Behaviours are pure state machines: they receive routed input moments\n * (press/grab transitions), tick from `update(dt)`, and write their output\n * through the interactable's {@link TransformPort}. They declare:\n *\n * - `ownership` - who owns the transform (the formalised pale-signal\n * driven / dragged / handDriven / native models), so a binder knows\n * without reading code;\n * - `requires` / `requiresAnyOf` - the input capabilities they need;\n * capability negotiation disables unsatisfiable behaviours VISIBLY\n * (a `behaviourDisabled` event with the unmet list), never silently.\n */\nimport type {\n InputCapabilityRequirement,\n InputSourceKind,\n PoseTuple,\n RayTuple,\n Vec3Tuple,\n} from \"@realitycollective/webxr-input\";\nimport type { InteractionEvent } from \"../events.js\";\nimport type { FeedbackIntent } from \"../feedback.js\";\nimport type { TransformPort } from \"../ports.js\";\n\n/** Who owns the interactable's transform while this behaviour is active. */\nexport type TransformOwnership = \"driven\" | \"dragged\" | \"handDriven\" | \"native\";\n\n/** The slice of a live interactor a behaviour may consume. */\nexport interface InteractorInfo {\n id: string;\n kind: InputSourceKind;\n ray?: RayTuple;\n gripPose?: PoseTuple;\n indexTip?: Vec3Tuple;\n select: number;\n squeeze: number;\n}\n\n/** Everything a behaviour can reach during a tick or a routed moment. */\nexport interface BehaviourContext {\n dt: number;\n interactableId: string;\n transform: TransformPort | undefined;\n emit(event: Omit<InteractionEvent, \"interactableId\">): void;\n feedback(intent: Omit<FeedbackIntent, \"interactableId\">): void;\n /** LIVE world pose of another registered interactable (toss scoring). */\n getWorldPose(interactableId: string): PoseTuple | undefined;\n}\n\nexport interface Behaviour {\n readonly kind: string;\n readonly ownership: TransformOwnership;\n /** ALL of these must be satisfied. */\n readonly requires: readonly InputCapabilityRequirement[];\n /** For each group, AT LEAST ONE must be satisfied. */\n readonly requiresAnyOf?: readonly (readonly InputCapabilityRequirement[])[];\n /** True when this behaviour makes the interactable a grab target. */\n readonly grabbable: boolean;\n\n onPressStart?(ctx: BehaviourContext, interactor: InteractorInfo): void;\n onPressEnd?(ctx: BehaviourContext, interactor: InteractorInfo): void;\n onGrabStart?(ctx: BehaviourContext, interactor: InteractorInfo): void;\n onGrabEnd?(ctx: BehaviourContext, interactor: InteractorInfo): void;\n\n /**\n * Per-frame tick. `holder` is set while a grab interactor holds this\n * interactable (grab-family behaviours consume it; others ignore it).\n */\n update(ctx: BehaviourContext, holder?: InteractorInfo): void;\n\n /** Current logical 0..1 value (state snapshots, HUDs). */\n getValue(): number;\n}\n\n/** Hand position a hand-driven behaviour should track, best available. */\nexport function holderPoint(holder: InteractorInfo): Vec3Tuple | undefined {\n return holder.indexTip ?? holder.gripPose?.position ?? holder.ray?.origin;\n}\n"]}
@@ -21,7 +21,7 @@ export class DialBehaviour {
21
21
  const transform = ctx.transform;
22
22
  if (!hand || !transform)
23
23
  return null;
24
- const rest = transform.getWorldPose();
24
+ const rest = transform.getRestWorldPose();
25
25
  const local = worldToLocal(hand, rest.position, rest.quaternion);
26
26
  // Project onto the plane perpendicular to the axis.
27
27
  const along = vDot(local, this.axis);
@@ -1 +1 @@
1
- {"version":3,"file":"dial.js","sourceRoot":"","sources":["../../src/behaviours/dial.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Dial - a twistable knob. `handDriven`: while held, the hand's bearing\n * around the dial axis accumulates a clamped rotation `[0, maxAngle]`;\n * publishes `normalize01(angle, maxAngle)`. Works from any provider that\n * can grab and report a hand/grip position (the research prototype's\n * engine-dragged variant becomes an adapter option later).\n */\nimport type { Vec3Tuple } from \"@realitycollective/webxr-input\";\nimport {\n normalize01,\n quatFromAxisAngle,\n vCross,\n vDot,\n vNormalize,\n vSub,\n worldToLocal,\n} from \"../math.js\";\nimport { holderPoint, type Behaviour, type BehaviourContext, type InteractorInfo } from \"./behaviour.js\";\n\nexport interface DialConfig {\n kind: \"dial\";\n /** Local twist axis. */\n axis?: Vec3Tuple;\n /** Max rotation from rest (radians). */\n maxAngle?: number;\n}\n\nconst VALUE_EPSILON = 1e-3;\n\nexport class DialBehaviour implements Behaviour {\n readonly kind = \"dial\";\n readonly ownership = \"handDriven\" as const;\n readonly requires = [\"grabs\"] as const;\n readonly grabbable = true;\n\n private readonly axis: Vec3Tuple;\n private maxAngle: number;\n private angle = 0;\n private angleAtGrab = 0;\n private refBearing: Vec3Tuple | null = null;\n private lastEmitted = -1;\n\n constructor(config: Omit<DialConfig, \"kind\"> = {}) {\n this.axis = config.axis ?? [0, 1, 0];\n this.maxAngle = config.maxAngle ?? (3 * Math.PI) / 2;\n }\n\n private bearingOf(ctx: BehaviourContext, holder: InteractorInfo): Vec3Tuple | null {\n const hand = holderPoint(holder);\n const transform = ctx.transform;\n if (!hand || !transform) return null;\n const rest = transform.getWorldPose();\n const local = worldToLocal(hand, rest.position, rest.quaternion);\n // Project onto the plane perpendicular to the axis.\n const along = vDot(local, this.axis);\n const planar = vSub(local, [\n this.axis[0] * along,\n this.axis[1] * along,\n this.axis[2] * along,\n ]);\n const n = vNormalize(planar);\n return n[0] === 0 && n[1] === 0 && n[2] === 0 ? null : n;\n }\n\n onGrabStart(ctx: BehaviourContext, interactor: InteractorInfo): void {\n this.refBearing = this.bearingOf(ctx, interactor);\n this.angleAtGrab = this.angle;\n ctx.emit({ type: \"grabStart\", behaviourKind: this.kind, interactorId: interactor.id });\n ctx.feedback({\n cue: \"grab\",\n behaviourKind: this.kind,\n sourceId: interactor.id,\n intensity: 0.4,\n durationMs: 25,\n });\n }\n\n onGrabEnd(ctx: BehaviourContext, interactor: InteractorInfo): void {\n this.refBearing = null;\n ctx.emit({ type: \"grabEnd\", behaviourKind: this.kind, interactorId: interactor.id });\n ctx.feedback({\n cue: \"drop\",\n behaviourKind: this.kind,\n sourceId: interactor.id,\n intensity: 0.2,\n durationMs: 15,\n });\n }\n\n update(ctx: BehaviourContext, holder?: InteractorInfo): void {\n if (holder && this.refBearing && ctx.transform) {\n const current = this.bearingOf(ctx, holder);\n if (current) {\n const cross = vCross(this.refBearing, current);\n const delta = Math.atan2(vDot(cross, this.axis), vDot(this.refBearing, current));\n const next = Math.min(this.maxAngle, Math.max(0, this.angleAtGrab + delta));\n this.angle = next;\n ctx.transform.setLocalRotation(quatFromAxisAngle(this.axis, this.angle));\n }\n }\n const value = this.getValue();\n if (Math.abs(value - this.lastEmitted) > VALUE_EPSILON) {\n this.lastEmitted = value;\n ctx.emit({ type: \"valueChanged\", behaviourKind: this.kind, value });\n }\n }\n\n getValue(): number {\n return normalize01(this.angle, this.maxAngle);\n }\n\n setMaxAngle(maxAngle: number): void {\n this.maxAngle = maxAngle;\n if (this.angle > maxAngle) this.angle = maxAngle;\n }\n\n getMaxAngle(): number {\n return this.maxAngle;\n }\n}\n"]}
1
+ {"version":3,"file":"dial.js","sourceRoot":"","sources":["../../src/behaviours/dial.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Dial - a twistable knob. `handDriven`: while held, the hand's bearing\n * around the dial axis accumulates a clamped rotation `[0, maxAngle]`;\n * publishes `normalize01(angle, maxAngle)`. Works from any provider that\n * can grab and report a hand/grip position (the research prototype's\n * engine-dragged variant becomes an adapter option later).\n */\nimport type { Vec3Tuple } from \"@realitycollective/webxr-input\";\nimport {\n normalize01,\n quatFromAxisAngle,\n vCross,\n vDot,\n vNormalize,\n vSub,\n worldToLocal,\n} from \"../math.js\";\nimport { holderPoint, type Behaviour, type BehaviourContext, type InteractorInfo } from \"./behaviour.js\";\n\nexport interface DialConfig {\n kind: \"dial\";\n /** Local twist axis. */\n axis?: Vec3Tuple;\n /** Max rotation from rest (radians). */\n maxAngle?: number;\n}\n\nconst VALUE_EPSILON = 1e-3;\n\nexport class DialBehaviour implements Behaviour {\n readonly kind = \"dial\";\n readonly ownership = \"handDriven\" as const;\n readonly requires = [\"grabs\"] as const;\n readonly grabbable = true;\n\n private readonly axis: Vec3Tuple;\n private maxAngle: number;\n private angle = 0;\n private angleAtGrab = 0;\n private refBearing: Vec3Tuple | null = null;\n private lastEmitted = -1;\n\n constructor(config: Omit<DialConfig, \"kind\"> = {}) {\n this.axis = config.axis ?? [0, 1, 0];\n this.maxAngle = config.maxAngle ?? (3 * Math.PI) / 2;\n }\n\n private bearingOf(ctx: BehaviourContext, holder: InteractorInfo): Vec3Tuple | null {\n const hand = holderPoint(holder);\n const transform = ctx.transform;\n if (!hand || !transform) return null;\n const rest = transform.getRestWorldPose();\n const local = worldToLocal(hand, rest.position, rest.quaternion);\n // Project onto the plane perpendicular to the axis.\n const along = vDot(local, this.axis);\n const planar = vSub(local, [\n this.axis[0] * along,\n this.axis[1] * along,\n this.axis[2] * along,\n ]);\n const n = vNormalize(planar);\n return n[0] === 0 && n[1] === 0 && n[2] === 0 ? null : n;\n }\n\n onGrabStart(ctx: BehaviourContext, interactor: InteractorInfo): void {\n this.refBearing = this.bearingOf(ctx, interactor);\n this.angleAtGrab = this.angle;\n ctx.emit({ type: \"grabStart\", behaviourKind: this.kind, interactorId: interactor.id });\n ctx.feedback({\n cue: \"grab\",\n behaviourKind: this.kind,\n sourceId: interactor.id,\n intensity: 0.4,\n durationMs: 25,\n });\n }\n\n onGrabEnd(ctx: BehaviourContext, interactor: InteractorInfo): void {\n this.refBearing = null;\n ctx.emit({ type: \"grabEnd\", behaviourKind: this.kind, interactorId: interactor.id });\n ctx.feedback({\n cue: \"drop\",\n behaviourKind: this.kind,\n sourceId: interactor.id,\n intensity: 0.2,\n durationMs: 15,\n });\n }\n\n update(ctx: BehaviourContext, holder?: InteractorInfo): void {\n if (holder && this.refBearing && ctx.transform) {\n const current = this.bearingOf(ctx, holder);\n if (current) {\n const cross = vCross(this.refBearing, current);\n const delta = Math.atan2(vDot(cross, this.axis), vDot(this.refBearing, current));\n const next = Math.min(this.maxAngle, Math.max(0, this.angleAtGrab + delta));\n this.angle = next;\n ctx.transform.setLocalRotation(quatFromAxisAngle(this.axis, this.angle));\n }\n }\n const value = this.getValue();\n if (Math.abs(value - this.lastEmitted) > VALUE_EPSILON) {\n this.lastEmitted = value;\n ctx.emit({ type: \"valueChanged\", behaviourKind: this.kind, value });\n }\n }\n\n getValue(): number {\n return normalize01(this.angle, this.maxAngle);\n }\n\n setMaxAngle(maxAngle: number): void {\n this.maxAngle = maxAngle;\n if (this.angle > maxAngle) this.angle = maxAngle;\n }\n\n getMaxAngle(): number {\n return this.maxAngle;\n }\n}\n"]}
@@ -42,7 +42,7 @@ export class HingeBehaviour {
42
42
  if (holder && ctx.transform) {
43
43
  const hand = holderPoint(holder);
44
44
  if (hand) {
45
- const rest = ctx.transform.getWorldPose();
45
+ const rest = ctx.transform.getRestWorldPose();
46
46
  const local = worldToLocal(hand, rest.position, rest.quaternion);
47
47
  const alongRest = vDot(local, this.restDir);
48
48
  const alongSwing = vDot(local, this.swingDir);
@@ -1 +1 @@
1
- {"version":3,"file":"hinge.js","sourceRoot":"","sources":["../../src/behaviours/hinge.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Hinge - a position-driven lever. `handDriven`: grabbing only marks the\n * hold; the behaviour reads the holder's hand position each frame and\n * swings the arm to point at it (movement, not wrist), bidirectional about\n * rest, clamped to ±maxAngle. Stays where left on release. Publishes a\n * centred 0..1 value (rest = 0.5).\n *\n * All math happens in the interactable's REST local frame, so the mount\n * (table / wall vertical / wall horizontal / any) is simply the parent\n * orientation of the object - no faceYaw plumbing needed.\n */\nimport type { Vec3Tuple } from \"@realitycollective/webxr-input\";\nimport {\n centeredUnit,\n hingeAngleToHand,\n quatFromAxisAngle,\n vCross,\n vDot,\n worldToLocal,\n} from \"../math.js\";\nimport { holderPoint, type Behaviour, type BehaviourContext, type InteractorInfo } from \"./behaviour.js\";\n\nexport interface HingeConfig {\n kind: \"hinge\";\n /** Local hinge axis. */\n axis?: Vec3Tuple;\n /** Local rest direction of the arm. */\n restDir?: Vec3Tuple;\n /** Max swing (radians) either side of rest. */\n maxAngle?: number;\n}\n\nconst VALUE_EPSILON = 1e-3;\n\nexport class HingeBehaviour implements Behaviour {\n readonly kind = \"hinge\";\n readonly ownership = \"handDriven\" as const;\n readonly requires = [\"grabs\"] as const;\n readonly grabbable = true;\n\n private readonly axis: Vec3Tuple;\n private readonly restDir: Vec3Tuple;\n private readonly swingDir: Vec3Tuple;\n private maxAngle: number;\n private angle = 0;\n private lastEmitted = -1;\n\n constructor(config: Omit<HingeConfig, \"kind\"> = {}) {\n this.axis = config.axis ?? [1, 0, 0];\n this.restDir = config.restDir ?? [0, 1, 0];\n this.swingDir = vCross(this.axis, this.restDir);\n this.maxAngle = config.maxAngle ?? Math.PI / 4;\n }\n\n onGrabStart(ctx: BehaviourContext, interactor: InteractorInfo): void {\n ctx.emit({ type: \"grabStart\", behaviourKind: this.kind, interactorId: interactor.id });\n ctx.feedback({\n cue: \"grab\",\n behaviourKind: this.kind,\n sourceId: interactor.id,\n intensity: 0.4,\n durationMs: 25,\n });\n }\n\n onGrabEnd(ctx: BehaviourContext, interactor: InteractorInfo): void {\n ctx.emit({ type: \"grabEnd\", behaviourKind: this.kind, interactorId: interactor.id });\n ctx.feedback({\n cue: \"drop\",\n behaviourKind: this.kind,\n sourceId: interactor.id,\n intensity: 0.2,\n durationMs: 15,\n });\n }\n\n update(ctx: BehaviourContext, holder?: InteractorInfo): void {\n if (holder && ctx.transform) {\n const hand = holderPoint(holder);\n if (hand) {\n const rest = ctx.transform.getWorldPose();\n const local = worldToLocal(hand, rest.position, rest.quaternion);\n const alongRest = vDot(local, this.restDir);\n const alongSwing = vDot(local, this.swingDir);\n this.angle = hingeAngleToHand(alongRest, alongSwing, this.maxAngle);\n ctx.transform.setLocalRotation(quatFromAxisAngle(this.axis, this.angle));\n }\n }\n const value = this.getValue();\n if (Math.abs(value - this.lastEmitted) > VALUE_EPSILON) {\n this.lastEmitted = value;\n ctx.emit({ type: \"valueChanged\", behaviourKind: this.kind, value });\n }\n }\n\n getValue(): number {\n return centeredUnit(this.angle, this.maxAngle);\n }\n\n /** Live-tune the swing range (playground steppers). */\n setMaxAngle(maxAngle: number): void {\n this.maxAngle = maxAngle;\n }\n\n getMaxAngle(): number {\n return this.maxAngle;\n }\n}\n"]}
1
+ {"version":3,"file":"hinge.js","sourceRoot":"","sources":["../../src/behaviours/hinge.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Hinge - a position-driven lever. `handDriven`: grabbing only marks the\n * hold; the behaviour reads the holder's hand position each frame and\n * swings the arm to point at it (movement, not wrist), bidirectional about\n * rest, clamped to ±maxAngle. Stays where left on release. Publishes a\n * centred 0..1 value (rest = 0.5).\n *\n * All math happens in the interactable's REST local frame, so the mount\n * (table / wall vertical / wall horizontal / any) is simply the parent\n * orientation of the object - no faceYaw plumbing needed.\n */\nimport type { Vec3Tuple } from \"@realitycollective/webxr-input\";\nimport {\n centeredUnit,\n hingeAngleToHand,\n quatFromAxisAngle,\n vCross,\n vDot,\n worldToLocal,\n} from \"../math.js\";\nimport { holderPoint, type Behaviour, type BehaviourContext, type InteractorInfo } from \"./behaviour.js\";\n\nexport interface HingeConfig {\n kind: \"hinge\";\n /** Local hinge axis. */\n axis?: Vec3Tuple;\n /** Local rest direction of the arm. */\n restDir?: Vec3Tuple;\n /** Max swing (radians) either side of rest. */\n maxAngle?: number;\n}\n\nconst VALUE_EPSILON = 1e-3;\n\nexport class HingeBehaviour implements Behaviour {\n readonly kind = \"hinge\";\n readonly ownership = \"handDriven\" as const;\n readonly requires = [\"grabs\"] as const;\n readonly grabbable = true;\n\n private readonly axis: Vec3Tuple;\n private readonly restDir: Vec3Tuple;\n private readonly swingDir: Vec3Tuple;\n private maxAngle: number;\n private angle = 0;\n private lastEmitted = -1;\n\n constructor(config: Omit<HingeConfig, \"kind\"> = {}) {\n this.axis = config.axis ?? [1, 0, 0];\n this.restDir = config.restDir ?? [0, 1, 0];\n this.swingDir = vCross(this.axis, this.restDir);\n this.maxAngle = config.maxAngle ?? Math.PI / 4;\n }\n\n onGrabStart(ctx: BehaviourContext, interactor: InteractorInfo): void {\n ctx.emit({ type: \"grabStart\", behaviourKind: this.kind, interactorId: interactor.id });\n ctx.feedback({\n cue: \"grab\",\n behaviourKind: this.kind,\n sourceId: interactor.id,\n intensity: 0.4,\n durationMs: 25,\n });\n }\n\n onGrabEnd(ctx: BehaviourContext, interactor: InteractorInfo): void {\n ctx.emit({ type: \"grabEnd\", behaviourKind: this.kind, interactorId: interactor.id });\n ctx.feedback({\n cue: \"drop\",\n behaviourKind: this.kind,\n sourceId: interactor.id,\n intensity: 0.2,\n durationMs: 15,\n });\n }\n\n update(ctx: BehaviourContext, holder?: InteractorInfo): void {\n if (holder && ctx.transform) {\n const hand = holderPoint(holder);\n if (hand) {\n const rest = ctx.transform.getRestWorldPose();\n const local = worldToLocal(hand, rest.position, rest.quaternion);\n const alongRest = vDot(local, this.restDir);\n const alongSwing = vDot(local, this.swingDir);\n this.angle = hingeAngleToHand(alongRest, alongSwing, this.maxAngle);\n ctx.transform.setLocalRotation(quatFromAxisAngle(this.axis, this.angle));\n }\n }\n const value = this.getValue();\n if (Math.abs(value - this.lastEmitted) > VALUE_EPSILON) {\n this.lastEmitted = value;\n ctx.emit({ type: \"valueChanged\", behaviourKind: this.kind, value });\n }\n }\n\n getValue(): number {\n return centeredUnit(this.angle, this.maxAngle);\n }\n\n /** Live-tune the swing range (playground steppers). */\n setMaxAngle(maxAngle: number): void {\n this.maxAngle = maxAngle;\n }\n\n getMaxAngle(): number {\n return this.maxAngle;\n }\n}\n"]}
@@ -28,7 +28,7 @@ export class SlideBehaviour {
28
28
  const transform = ctx.transform;
29
29
  if (!hand || !transform)
30
30
  return null;
31
- const rest = transform.getWorldPose();
31
+ const rest = transform.getRestWorldPose();
32
32
  const local = worldToLocal(hand, rest.position, rest.quaternion);
33
33
  // Pull is measured along −axis.
34
34
  return -vDot(local, this.axis);
@@ -1 +1 @@
1
- {"version":3,"file":"slide.js","sourceRoot":"","sources":["../../src/behaviours/slide.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Slide - a pulley/handle pulled along one local axis. `handDriven` while\n * held (the pull follows the hand's displacement along −axis), `driven`\n * on release: a spring returns the handle home while the value keeps\n * publishing. Value = pull / travel.\n */\nimport type { Vec3Tuple } from \"@realitycollective/webxr-input\";\nimport { springStep, vDot, vScale, worldToLocal } from \"../math.js\";\nimport { holderPoint, type Behaviour, type BehaviourContext, type InteractorInfo } from \"./behaviour.js\";\n\nexport interface SlideConfig {\n kind: \"slide\";\n /** Local rail axis; the handle is pulled along −axis. */\n axis?: Vec3Tuple;\n /** Full pull travel in meters. */\n travel?: number;\n stiffness?: number;\n damping?: number;\n}\n\nconst VALUE_EPSILON = 1e-3;\n\nexport class SlideBehaviour implements Behaviour {\n readonly kind = \"slide\";\n readonly ownership = \"handDriven\" as const;\n readonly requires = [\"grabs\"] as const;\n readonly grabbable = true;\n\n private readonly axis: Vec3Tuple;\n private travel: number;\n private stiffness: number;\n private damping: number;\n\n private pull = 0; // meters along −axis, 0..travel\n private velocity = 0;\n private pullAtGrab = 0;\n private handAlongAtGrab = 0;\n private held = false;\n private lastEmitted = -1;\n private readonly spring = new Float32Array(2);\n\n constructor(config: Omit<SlideConfig, \"kind\"> = {}) {\n this.axis = config.axis ?? [0, 1, 0];\n this.travel = config.travel ?? 0.3;\n this.stiffness = config.stiffness ?? 180;\n this.damping = config.damping ?? 14;\n }\n\n private handAlong(ctx: BehaviourContext, holder: InteractorInfo): number | null {\n const hand = holderPoint(holder);\n const transform = ctx.transform;\n if (!hand || !transform) return null;\n const rest = transform.getWorldPose();\n const local = worldToLocal(hand, rest.position, rest.quaternion);\n // Pull is measured along −axis.\n return -vDot(local, this.axis);\n }\n\n onGrabStart(ctx: BehaviourContext, interactor: InteractorInfo): void {\n this.held = true;\n this.pullAtGrab = this.pull;\n this.handAlongAtGrab = this.handAlong(ctx, interactor) ?? 0;\n this.velocity = 0;\n ctx.emit({ type: \"grabStart\", behaviourKind: this.kind, interactorId: interactor.id });\n ctx.feedback({\n cue: \"grab\",\n behaviourKind: this.kind,\n sourceId: interactor.id,\n intensity: 0.4,\n durationMs: 25,\n });\n }\n\n onGrabEnd(ctx: BehaviourContext, interactor: InteractorInfo): void {\n this.held = false;\n ctx.emit({ type: \"grabEnd\", behaviourKind: this.kind, interactorId: interactor.id });\n ctx.feedback({\n cue: \"drop\",\n behaviourKind: this.kind,\n sourceId: interactor.id,\n intensity: 0.2,\n durationMs: 15,\n });\n }\n\n update(ctx: BehaviourContext, holder?: InteractorInfo): void {\n if (this.held && holder) {\n const along = this.handAlong(ctx, holder);\n if (along !== null) {\n const next = this.pullAtGrab + (along - this.handAlongAtGrab);\n this.pull = Math.min(this.travel, Math.max(0, next));\n }\n } else if (this.pull > 0 || Math.abs(this.velocity) > 1e-4) {\n springStep(this.pull, this.velocity, 0, this.stiffness, this.damping, ctx.dt, this.spring);\n this.pull = Math.max(0, this.spring[0] ?? 0);\n this.velocity = this.spring[1] ?? 0;\n if (this.pull < 1e-4 && Math.abs(this.velocity) < 1e-3) {\n this.pull = 0;\n this.velocity = 0;\n }\n }\n ctx.transform?.setLocalOffset(vScale(this.axis, -this.pull));\n const value = this.getValue();\n if (Math.abs(value - this.lastEmitted) > VALUE_EPSILON) {\n this.lastEmitted = value;\n ctx.emit({ type: \"valueChanged\", behaviourKind: this.kind, value });\n }\n }\n\n getValue(): number {\n return this.travel < 1e-9 ? 0 : Math.min(1, this.pull / this.travel);\n }\n\n setTravel(travel: number): void {\n this.travel = travel;\n if (this.pull > travel) this.pull = travel;\n }\n\n getTravel(): number {\n return this.travel;\n }\n\n setSpring(stiffness: number, damping: number): void {\n this.stiffness = stiffness;\n this.damping = damping;\n }\n\n getSpring(): { stiffness: number; damping: number } {\n return { stiffness: this.stiffness, damping: this.damping };\n }\n}\n"]}
1
+ {"version":3,"file":"slide.js","sourceRoot":"","sources":["../../src/behaviours/slide.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Slide - a pulley/handle pulled along one local axis. `handDriven` while\n * held (the pull follows the hand's displacement along −axis), `driven`\n * on release: a spring returns the handle home while the value keeps\n * publishing. Value = pull / travel.\n */\nimport type { Vec3Tuple } from \"@realitycollective/webxr-input\";\nimport { springStep, vDot, vScale, worldToLocal } from \"../math.js\";\nimport { holderPoint, type Behaviour, type BehaviourContext, type InteractorInfo } from \"./behaviour.js\";\n\nexport interface SlideConfig {\n kind: \"slide\";\n /** Local rail axis; the handle is pulled along −axis. */\n axis?: Vec3Tuple;\n /** Full pull travel in meters. */\n travel?: number;\n stiffness?: number;\n damping?: number;\n}\n\nconst VALUE_EPSILON = 1e-3;\n\nexport class SlideBehaviour implements Behaviour {\n readonly kind = \"slide\";\n readonly ownership = \"handDriven\" as const;\n readonly requires = [\"grabs\"] as const;\n readonly grabbable = true;\n\n private readonly axis: Vec3Tuple;\n private travel: number;\n private stiffness: number;\n private damping: number;\n\n private pull = 0; // meters along −axis, 0..travel\n private velocity = 0;\n private pullAtGrab = 0;\n private handAlongAtGrab = 0;\n private held = false;\n private lastEmitted = -1;\n private readonly spring = new Float32Array(2);\n\n constructor(config: Omit<SlideConfig, \"kind\"> = {}) {\n this.axis = config.axis ?? [0, 1, 0];\n this.travel = config.travel ?? 0.3;\n this.stiffness = config.stiffness ?? 180;\n this.damping = config.damping ?? 14;\n }\n\n private handAlong(ctx: BehaviourContext, holder: InteractorInfo): number | null {\n const hand = holderPoint(holder);\n const transform = ctx.transform;\n if (!hand || !transform) return null;\n const rest = transform.getRestWorldPose();\n const local = worldToLocal(hand, rest.position, rest.quaternion);\n // Pull is measured along −axis.\n return -vDot(local, this.axis);\n }\n\n onGrabStart(ctx: BehaviourContext, interactor: InteractorInfo): void {\n this.held = true;\n this.pullAtGrab = this.pull;\n this.handAlongAtGrab = this.handAlong(ctx, interactor) ?? 0;\n this.velocity = 0;\n ctx.emit({ type: \"grabStart\", behaviourKind: this.kind, interactorId: interactor.id });\n ctx.feedback({\n cue: \"grab\",\n behaviourKind: this.kind,\n sourceId: interactor.id,\n intensity: 0.4,\n durationMs: 25,\n });\n }\n\n onGrabEnd(ctx: BehaviourContext, interactor: InteractorInfo): void {\n this.held = false;\n ctx.emit({ type: \"grabEnd\", behaviourKind: this.kind, interactorId: interactor.id });\n ctx.feedback({\n cue: \"drop\",\n behaviourKind: this.kind,\n sourceId: interactor.id,\n intensity: 0.2,\n durationMs: 15,\n });\n }\n\n update(ctx: BehaviourContext, holder?: InteractorInfo): void {\n if (this.held && holder) {\n const along = this.handAlong(ctx, holder);\n if (along !== null) {\n const next = this.pullAtGrab + (along - this.handAlongAtGrab);\n this.pull = Math.min(this.travel, Math.max(0, next));\n }\n } else if (this.pull > 0 || Math.abs(this.velocity) > 1e-4) {\n springStep(this.pull, this.velocity, 0, this.stiffness, this.damping, ctx.dt, this.spring);\n this.pull = Math.max(0, this.spring[0] ?? 0);\n this.velocity = this.spring[1] ?? 0;\n if (this.pull < 1e-4 && Math.abs(this.velocity) < 1e-3) {\n this.pull = 0;\n this.velocity = 0;\n }\n }\n ctx.transform?.setLocalOffset(vScale(this.axis, -this.pull));\n const value = this.getValue();\n if (Math.abs(value - this.lastEmitted) > VALUE_EPSILON) {\n this.lastEmitted = value;\n ctx.emit({ type: \"valueChanged\", behaviourKind: this.kind, value });\n }\n }\n\n getValue(): number {\n return this.travel < 1e-9 ? 0 : Math.min(1, this.pull / this.travel);\n }\n\n setTravel(travel: number): void {\n this.travel = travel;\n if (this.pull > travel) this.pull = travel;\n }\n\n getTravel(): number {\n return this.travel;\n }\n\n setSpring(stiffness: number, damping: number): void {\n this.stiffness = stiffness;\n this.damping = damping;\n }\n\n getSpring(): { stiffness: number; damping: number } {\n return { stiffness: this.stiffness, damping: this.damping };\n }\n}\n"]}
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Shared `HitTester` and `TransformPort` conformance suites, shipped as data
3
+ * rather than as tests - the same idea as `windowHostContractCases()` in
4
+ * WebXR-UIExtensions and `inputProviderContractCases()` in
5
+ * `@realitycollective/webxr-input`. Five adapters implement these two ports
6
+ * against five different scene graphs; running the same checks against every
7
+ * one is what stops an adapter quietly dropping a documented rule.
8
+ *
9
+ * The suites are runner-free on purpose. Every adapter repository already has
10
+ * its own test runner, and an adapter written outside this repository cannot
11
+ * reach into this one's `test/` folder, so the checks ship as plain objects
12
+ * that throw an `Error` on failure and the adapter iterates them.
13
+ *
14
+ * Both suites assert only what `ports.ts` documents, including two rules
15
+ * every platform now shares:
16
+ * - `getWorldPose()` is the LIVE pose and `getRestWorldPose()` the rest pose
17
+ * offsets are measured from.
18
+ * - Every tuple a port returns is fresh and owned by the caller, and a tuple
19
+ * passed in is not kept.
20
+ */
21
+ import type { PoseTuple, Vec3Tuple } from "@realitycollective/webxr-input";
22
+ import type { HitTester, TransformPort } from "./ports.js";
23
+ /**
24
+ * Places (or replaces) one target for a case to query against. Build a FRESH
25
+ * subject per case: cases place targets of their own and do not clean up.
26
+ */
27
+ export interface HitTesterContractDriver {
28
+ /**
29
+ * Register a target at `position` with an interaction radius of `radius`,
30
+ * however the platform registers a target - a mesh, a node, an entity.
31
+ */
32
+ place(id: string, position: Vec3Tuple, radius: number): void;
33
+ }
34
+ /** The tester under test, plus the means of putting targets into its scene. */
35
+ export interface HitTesterContractSubject {
36
+ hitTester: HitTester;
37
+ driver: HitTesterContractDriver;
38
+ }
39
+ /** One check a {@link HitTester} implementation must pass. */
40
+ export interface HitTesterContractCase {
41
+ name: string;
42
+ run(subject: HitTesterContractSubject): void;
43
+ }
44
+ /**
45
+ * The shared `HitTester` conformance suite. An adapter's test file is a loop:
46
+ *
47
+ * ```ts
48
+ * for (const contractCase of hitTesterContractCases()) {
49
+ * it(contractCase.name, () => contractCase.run(makeSubject()));
50
+ * }
51
+ * ```
52
+ */
53
+ export declare function hitTesterContractCases(): readonly HitTesterContractCase[];
54
+ /** The port under test, over one object. Build a FRESH one per case. */
55
+ export interface TransformPortContractSubject {
56
+ port: TransformPort;
57
+ /**
58
+ * The world pose the object sat at when the port captured its rest. Place
59
+ * it away from the origin and turned, under no parent or an unscaled one,
60
+ * so a port that ignores its rest frame cannot pass by accident.
61
+ */
62
+ rest: PoseTuple;
63
+ }
64
+ /** One check a {@link TransformPort} implementation must pass. */
65
+ export interface TransformPortContractCase {
66
+ name: string;
67
+ run(subject: TransformPortContractSubject): void;
68
+ }
69
+ /**
70
+ * The shared `TransformPort` conformance suite. An adapter's test file is a
71
+ * loop, the same shape as {@link hitTesterContractCases}.
72
+ */
73
+ export declare function transformPortContractCases(): readonly TransformPortContractCase[];
@@ -0,0 +1,256 @@
1
+ import { quatMultiply, vAdd, vApplyQuat } from "./math.js";
2
+ /**
3
+ * The shared `HitTester` conformance suite. An adapter's test file is a loop:
4
+ *
5
+ * ```ts
6
+ * for (const contractCase of hitTesterContractCases()) {
7
+ * it(contractCase.name, () => contractCase.run(makeSubject()));
8
+ * }
9
+ * ```
10
+ */
11
+ export function hitTesterContractCases() {
12
+ return HIT_TESTER_CASES;
13
+ }
14
+ const DOWN_RAY = { origin: [0, 0, 0], direction: [0, 0, -1] };
15
+ const AWAY_RAY = { origin: [0, 0, 0], direction: [0, 0, 1] };
16
+ /**
17
+ * A ray-based query only has to land WITHIN the target's own radius:
18
+ * `ports.ts` documents `point` as "world-space hit or closest point", so a
19
+ * mesh-accurate ray tester (three.js) reporting the surface it actually hit
20
+ * is as conforming as a sphere tester (Babylon, IWSDK) reporting the target's
21
+ * centre. Placing the target exactly on the ray's line makes both answers
22
+ * the same point when a tester has no radius-shaped slack, and this is the
23
+ * tolerance for testers that do.
24
+ */
25
+ function slack(radius) {
26
+ return radius + 0.05;
27
+ }
28
+ const HIT_TESTER_CASES = [
29
+ {
30
+ name: "an empty scene answers a ray with null",
31
+ run({ hitTester }) {
32
+ assert(hitTester.hitRay(DOWN_RAY) === null, "hitRay over an empty scene must return null");
33
+ },
34
+ },
35
+ {
36
+ name: "a ray through a placed target returns its id, a positive distance and a point near the ray",
37
+ run({ hitTester, driver }) {
38
+ const radius = 0.15;
39
+ driver.place("target", [0, 0, -2], radius);
40
+ const hit = hitTester.hitRay(DOWN_RAY);
41
+ assert(hit !== null, "a ray through a placed target must not miss");
42
+ assert(hit.interactableId === "target", `expected interactableId "target", got "${hit.interactableId}"`);
43
+ assert(hit.distance > 0, `distance must be positive, got ${hit.distance}`);
44
+ const eps = slack(radius);
45
+ assert(closeTo(hit.distance, 2, eps), `distance must be close to 2, got ${hit.distance}`);
46
+ assertVec3Close(hit.point, [0, 0, -2], eps, "point");
47
+ },
48
+ },
49
+ {
50
+ name: "a ray pointing away from the target returns null",
51
+ run({ hitTester, driver }) {
52
+ driver.place("target", [0, 0, -2], 0.15);
53
+ assert(hitTester.hitRay(AWAY_RAY) === null, "a ray pointing away from every target must return null");
54
+ },
55
+ },
56
+ {
57
+ name: "hitProximity finds a target inside its radius and misses outside it",
58
+ run({ hitTester, driver }) {
59
+ driver.place("target", [0, 0, -2], 0.1);
60
+ const inside = hitTester.hitProximity([0, 0, -2], 0.5);
61
+ assert(inside !== null, "hitProximity at the target's own position must find it");
62
+ assert(inside.interactableId === "target", `expected interactableId "target", got "${inside.interactableId}"`);
63
+ const outside = hitTester.hitProximity([5, 5, 5], 0.05);
64
+ assert(outside === null, "hitProximity far from every target must return null");
65
+ },
66
+ },
67
+ {
68
+ name: "with two targets on one ray, the nearer one wins",
69
+ run({ hitTester, driver }) {
70
+ const radius = 0.1;
71
+ driver.place("far", [0, 0, -3], radius);
72
+ driver.place("near", [0, 0, -1], radius);
73
+ const hit = hitTester.hitRay(DOWN_RAY);
74
+ assert(hit !== null, "a ray through two targets must not miss");
75
+ assert(hit.interactableId === "near", `the nearer target must win, got "${hit.interactableId}"`);
76
+ assert(closeTo(hit.distance, 1, slack(radius)), `distance must be close to 1, got ${hit.distance}`);
77
+ },
78
+ },
79
+ {
80
+ name: "each hit and its point are fresh values the caller owns",
81
+ run({ hitTester, driver }) {
82
+ driver.place("target", [0, 0, -2], 0.15);
83
+ for (const [label, query] of [
84
+ ["hitRay", () => hitTester.hitRay(DOWN_RAY)],
85
+ ["hitProximity", () => hitTester.hitProximity([0, 0, -2], 0.5)],
86
+ ]) {
87
+ const first = query();
88
+ const second = query();
89
+ assert(first !== null && second !== null, `${label} must find the placed target`);
90
+ assert(second !== first && second.point !== first.point, `${label} must return a new hit and point each call`);
91
+ const expected = first.point[2];
92
+ first.point[2] = 1e6;
93
+ const third = query();
94
+ assert(third !== null && closeTo(third.point[2], expected, POSE_EPS), `writing to a returned point must not change the next ${label} answer`);
95
+ }
96
+ },
97
+ },
98
+ ];
99
+ /**
100
+ * The shared `TransformPort` conformance suite. An adapter's test file is a
101
+ * loop, the same shape as {@link hitTesterContractCases}.
102
+ */
103
+ export function transformPortContractCases() {
104
+ return TRANSFORM_PORT_CASES;
105
+ }
106
+ const POSE_EPS = 1e-3;
107
+ // A quarter turn about Y - a non-identity unit quaternion, so the case
108
+ // cannot pass by accident on a port that ignores its argument.
109
+ const QUARTER_TURN_Y = [0, Math.SQRT1_2, 0, Math.SQRT1_2];
110
+ const TRANSFORM_PORT_CASES = [
111
+ {
112
+ name: "setLocalOffset then getLocalOffset returns the same offset",
113
+ run({ port }) {
114
+ const offset = [0.3, -0.1, 0.05];
115
+ port.setLocalOffset(offset);
116
+ assertVec3Close(port.getLocalOffset(), offset, POSE_EPS, "getLocalOffset()");
117
+ },
118
+ },
119
+ {
120
+ name: "getWorldPose returns finite numbers and a unit quaternion",
121
+ run({ port }) {
122
+ const pose = port.getWorldPose();
123
+ for (const value of [...pose.position, ...pose.quaternion]) {
124
+ assert(Number.isFinite(value), `getWorldPose() must return finite numbers, got ${value}`);
125
+ }
126
+ const length = quatLength(pose.quaternion);
127
+ assert(closeTo(length, 1, POSE_EPS), `getWorldPose().quaternion must be a unit quaternion, length was ${length}`);
128
+ },
129
+ },
130
+ {
131
+ name: "setLocalRotation with a unit quaternion does not throw",
132
+ run({ port }) {
133
+ try {
134
+ port.setLocalRotation(QUARTER_TURN_Y);
135
+ }
136
+ catch (error) {
137
+ throw new Error(`setLocalRotation(unit quaternion) must not throw, it threw: ${String(error)}`);
138
+ }
139
+ },
140
+ },
141
+ {
142
+ name: "getRestWorldPose returns the pose captured at registration",
143
+ run({ port, rest }) {
144
+ assertPoseClose(port.getRestWorldPose(), rest, "getRestWorldPose()");
145
+ },
146
+ },
147
+ {
148
+ name: "before any write, the live pose is the rest pose",
149
+ run({ port, rest }) {
150
+ assertPoseClose(port.getWorldPose(), rest, "getWorldPose()");
151
+ },
152
+ },
153
+ {
154
+ name: "getWorldPose follows setLocalOffset while getRestWorldPose stays put",
155
+ run({ port, rest }) {
156
+ const offset = [0.3, -0.1, 0.05];
157
+ port.setLocalOffset(offset);
158
+ assertVec3Close(port.getWorldPose().position, vAdd(rest.position, vApplyQuat(offset, rest.quaternion)), POSE_EPS, "getWorldPose().position after setLocalOffset");
159
+ assertPoseClose(port.getRestWorldPose(), rest, "getRestWorldPose() after setLocalOffset");
160
+ },
161
+ },
162
+ {
163
+ name: "getWorldPose follows setLocalRotation while getRestWorldPose stays put",
164
+ run({ port, rest }) {
165
+ port.setLocalRotation(QUARTER_TURN_Y);
166
+ assertQuatClose(port.getWorldPose().quaternion, quatMultiply(rest.quaternion, QUARTER_TURN_Y), "getWorldPose().quaternion after setLocalRotation");
167
+ assertPoseClose(port.getRestWorldPose(), rest, "getRestWorldPose() after setLocalRotation");
168
+ },
169
+ },
170
+ {
171
+ name: "setWorldPose, when present, is what getWorldPose reads back",
172
+ run({ port, rest }) {
173
+ if (!port.setWorldPose)
174
+ return;
175
+ const target = { position: [0.5, 1.2, -0.4], quaternion: QUARTER_TURN_Y };
176
+ port.setWorldPose(target);
177
+ assertPoseClose(port.getWorldPose(), target, "getWorldPose() after setWorldPose");
178
+ assertPoseClose(port.getRestWorldPose(), rest, "getRestWorldPose() after setWorldPose");
179
+ },
180
+ },
181
+ {
182
+ name: "every returned tuple is a fresh value the caller owns",
183
+ run({ port }) {
184
+ const pose = port.getWorldPose();
185
+ const restPose = port.getRestWorldPose();
186
+ const offset = port.getLocalOffset();
187
+ pose.position[0] = 1e6;
188
+ pose.quaternion[3] = 1e6;
189
+ restPose.position[0] = 1e6;
190
+ offset[0] = 1e6;
191
+ assert(port.getWorldPose().position[0] !== 1e6, "writing to a returned getWorldPose() tuple must not change the port");
192
+ assert(port.getWorldPose().quaternion[3] !== 1e6, "writing to a returned getWorldPose() quaternion must not change the port");
193
+ assert(port.getRestWorldPose().position[0] !== 1e6, "writing to a returned getRestWorldPose() tuple must not change the port");
194
+ assert(port.getLocalOffset()[0] !== 1e6, "writing to a returned getLocalOffset() tuple must not change the port");
195
+ },
196
+ },
197
+ {
198
+ name: "a tuple passed in is not kept",
199
+ run({ port }) {
200
+ const offset = [0.3, -0.1, 0.05];
201
+ port.setLocalOffset(offset);
202
+ offset[0] = 9;
203
+ assertVec3Close(port.getLocalOffset(), [0.3, -0.1, 0.05], POSE_EPS, "getLocalOffset() after the caller reused its tuple");
204
+ if (!port.setWorldPose)
205
+ return;
206
+ const target = { position: [0.5, 1.2, -0.4], quaternion: [0, 0, 0, 1] };
207
+ port.setWorldPose(target);
208
+ target.position[0] = 9;
209
+ assertVec3Close(port.getWorldPose().position, [0.5, 1.2, -0.4], POSE_EPS, "getWorldPose() after the caller reused its tuple");
210
+ },
211
+ },
212
+ {
213
+ name: "setEffect, when present, accepts scale and emissive without throwing",
214
+ run({ port }) {
215
+ if (!port.setEffect)
216
+ return;
217
+ try {
218
+ port.setEffect({ scale: 1.2, emissive: 0.3 });
219
+ }
220
+ catch (error) {
221
+ throw new Error(`setEffect({ scale, emissive }) must not throw, it threw: ${String(error)}`);
222
+ }
223
+ },
224
+ },
225
+ ];
226
+ // ---------------------------------------------------------------------------
227
+ // Shared assertion helpers
228
+ // ---------------------------------------------------------------------------
229
+ function assert(condition, message) {
230
+ if (!condition)
231
+ throw new Error(message);
232
+ }
233
+ function closeTo(a, b, eps) {
234
+ return Math.abs(a - b) <= eps;
235
+ }
236
+ // Indexed one component at a time, rather than in a loop, so each index is a
237
+ // literal: `noUncheckedIndexedAccess` cannot narrow a tuple read through a
238
+ // variable index back to `number`.
239
+ function assertVec3Close(value, expected, eps, label) {
240
+ assert(closeTo(value[0], expected[0], eps), `${label}[0] must be close to ${expected[0]}, got ${value[0]}`);
241
+ assert(closeTo(value[1], expected[1], eps), `${label}[1] must be close to ${expected[1]}, got ${value[1]}`);
242
+ assert(closeTo(value[2], expected[2], eps), `${label}[2] must be close to ${expected[2]}, got ${value[2]}`);
243
+ }
244
+ // q and -q are the same rotation, so compare by the size of the dot product.
245
+ function assertQuatClose(value, expected, label) {
246
+ const dot = value[0] * expected[0] + value[1] * expected[1] + value[2] * expected[2] + value[3] * expected[3];
247
+ assert(closeTo(Math.abs(dot), 1, POSE_EPS), `${label} must be close to [${expected.join(", ")}], got [${value.join(", ")}]`);
248
+ }
249
+ function assertPoseClose(value, expected, label) {
250
+ assertVec3Close(value.position, expected.position, POSE_EPS, `${label}.position`);
251
+ assertQuatClose(value.quaternion, expected.quaternion, `${label}.quaternion`);
252
+ }
253
+ function quatLength(q) {
254
+ return Math.sqrt(q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]);
255
+ }
256
+ //# sourceMappingURL=contract-cases.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract-cases.js","sourceRoot":"","sources":["../src/contract-cases.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Shared `HitTester` and `TransformPort` conformance suites, shipped as data\n * rather than as tests - the same idea as `windowHostContractCases()` in\n * WebXR-UIExtensions and `inputProviderContractCases()` in\n * `@realitycollective/webxr-input`. Five adapters implement these two ports\n * against five different scene graphs; running the same checks against every\n * one is what stops an adapter quietly dropping a documented rule.\n *\n * The suites are runner-free on purpose. Every adapter repository already has\n * its own test runner, and an adapter written outside this repository cannot\n * reach into this one's `test/` folder, so the checks ship as plain objects\n * that throw an `Error` on failure and the adapter iterates them.\n *\n * Both suites assert only what `ports.ts` documents, including two rules\n * every platform now shares:\n * - `getWorldPose()` is the LIVE pose and `getRestWorldPose()` the rest pose\n * offsets are measured from.\n * - Every tuple a port returns is fresh and owned by the caller, and a tuple\n * passed in is not kept.\n */\nimport type { PoseTuple, QuatTuple, RayTuple, Vec3Tuple } from \"@realitycollective/webxr-input\";\nimport { quatMultiply, vAdd, vApplyQuat } from \"./math.js\";\nimport type { HitTester, TransformPort } from \"./ports.js\";\n\n// ---------------------------------------------------------------------------\n// HitTester\n// ---------------------------------------------------------------------------\n\n/**\n * Places (or replaces) one target for a case to query against. Build a FRESH\n * subject per case: cases place targets of their own and do not clean up.\n */\nexport interface HitTesterContractDriver {\n /**\n * Register a target at `position` with an interaction radius of `radius`,\n * however the platform registers a target - a mesh, a node, an entity.\n */\n place(id: string, position: Vec3Tuple, radius: number): void;\n}\n\n/** The tester under test, plus the means of putting targets into its scene. */\nexport interface HitTesterContractSubject {\n hitTester: HitTester;\n driver: HitTesterContractDriver;\n}\n\n/** One check a {@link HitTester} implementation must pass. */\nexport interface HitTesterContractCase {\n name: string;\n run(subject: HitTesterContractSubject): void;\n}\n\n/**\n * The shared `HitTester` conformance suite. An adapter's test file is a loop:\n *\n * ```ts\n * for (const contractCase of hitTesterContractCases()) {\n * it(contractCase.name, () => contractCase.run(makeSubject()));\n * }\n * ```\n */\nexport function hitTesterContractCases(): readonly HitTesterContractCase[] {\n return HIT_TESTER_CASES;\n}\n\nconst DOWN_RAY: RayTuple = { origin: [0, 0, 0], direction: [0, 0, -1] };\nconst AWAY_RAY: RayTuple = { origin: [0, 0, 0], direction: [0, 0, 1] };\n\n/**\n * A ray-based query only has to land WITHIN the target's own radius:\n * `ports.ts` documents `point` as \"world-space hit or closest point\", so a\n * mesh-accurate ray tester (three.js) reporting the surface it actually hit\n * is as conforming as a sphere tester (Babylon, IWSDK) reporting the target's\n * centre. Placing the target exactly on the ray's line makes both answers\n * the same point when a tester has no radius-shaped slack, and this is the\n * tolerance for testers that do.\n */\nfunction slack(radius: number): number {\n return radius + 0.05;\n}\n\nconst HIT_TESTER_CASES: readonly HitTesterContractCase[] = [\n {\n name: \"an empty scene answers a ray with null\",\n run({ hitTester }) {\n assert(hitTester.hitRay(DOWN_RAY) === null, \"hitRay over an empty scene must return null\");\n },\n },\n {\n name: \"a ray through a placed target returns its id, a positive distance and a point near the ray\",\n run({ hitTester, driver }) {\n const radius = 0.15;\n driver.place(\"target\", [0, 0, -2], radius);\n const hit = hitTester.hitRay(DOWN_RAY);\n assert(hit !== null, \"a ray through a placed target must not miss\");\n assert(hit.interactableId === \"target\", `expected interactableId \"target\", got \"${hit.interactableId}\"`);\n assert(hit.distance > 0, `distance must be positive, got ${hit.distance}`);\n const eps = slack(radius);\n assert(closeTo(hit.distance, 2, eps), `distance must be close to 2, got ${hit.distance}`);\n assertVec3Close(hit.point, [0, 0, -2], eps, \"point\");\n },\n },\n {\n name: \"a ray pointing away from the target returns null\",\n run({ hitTester, driver }) {\n driver.place(\"target\", [0, 0, -2], 0.15);\n assert(hitTester.hitRay(AWAY_RAY) === null, \"a ray pointing away from every target must return null\");\n },\n },\n {\n name: \"hitProximity finds a target inside its radius and misses outside it\",\n run({ hitTester, driver }) {\n driver.place(\"target\", [0, 0, -2], 0.1);\n const inside = hitTester.hitProximity([0, 0, -2], 0.5);\n assert(inside !== null, \"hitProximity at the target's own position must find it\");\n assert(\n inside.interactableId === \"target\",\n `expected interactableId \"target\", got \"${inside.interactableId}\"`,\n );\n const outside = hitTester.hitProximity([5, 5, 5], 0.05);\n assert(outside === null, \"hitProximity far from every target must return null\");\n },\n },\n {\n name: \"with two targets on one ray, the nearer one wins\",\n run({ hitTester, driver }) {\n const radius = 0.1;\n driver.place(\"far\", [0, 0, -3], radius);\n driver.place(\"near\", [0, 0, -1], radius);\n const hit = hitTester.hitRay(DOWN_RAY);\n assert(hit !== null, \"a ray through two targets must not miss\");\n assert(hit.interactableId === \"near\", `the nearer target must win, got \"${hit.interactableId}\"`);\n assert(closeTo(hit.distance, 1, slack(radius)), `distance must be close to 1, got ${hit.distance}`);\n },\n },\n {\n name: \"each hit and its point are fresh values the caller owns\",\n run({ hitTester, driver }) {\n driver.place(\"target\", [0, 0, -2], 0.15);\n for (const [label, query] of [\n [\"hitRay\", () => hitTester.hitRay(DOWN_RAY)],\n [\"hitProximity\", () => hitTester.hitProximity([0, 0, -2], 0.5)],\n ] as const) {\n const first = query();\n const second = query();\n assert(first !== null && second !== null, `${label} must find the placed target`);\n assert(second !== first && second.point !== first.point, `${label} must return a new hit and point each call`);\n const expected = first.point[2];\n first.point[2] = 1e6;\n const third = query();\n assert(\n third !== null && closeTo(third.point[2], expected, POSE_EPS),\n `writing to a returned point must not change the next ${label} answer`,\n );\n }\n },\n },\n];\n\n// ---------------------------------------------------------------------------\n// TransformPort\n// ---------------------------------------------------------------------------\n\n/** The port under test, over one object. Build a FRESH one per case. */\nexport interface TransformPortContractSubject {\n port: TransformPort;\n /**\n * The world pose the object sat at when the port captured its rest. Place\n * it away from the origin and turned, under no parent or an unscaled one,\n * so a port that ignores its rest frame cannot pass by accident.\n */\n rest: PoseTuple;\n}\n\n/** One check a {@link TransformPort} implementation must pass. */\nexport interface TransformPortContractCase {\n name: string;\n run(subject: TransformPortContractSubject): void;\n}\n\n/**\n * The shared `TransformPort` conformance suite. An adapter's test file is a\n * loop, the same shape as {@link hitTesterContractCases}.\n */\nexport function transformPortContractCases(): readonly TransformPortContractCase[] {\n return TRANSFORM_PORT_CASES;\n}\n\nconst POSE_EPS = 1e-3;\n// A quarter turn about Y - a non-identity unit quaternion, so the case\n// cannot pass by accident on a port that ignores its argument.\nconst QUARTER_TURN_Y: QuatTuple = [0, Math.SQRT1_2, 0, Math.SQRT1_2];\n\nconst TRANSFORM_PORT_CASES: readonly TransformPortContractCase[] = [\n {\n name: \"setLocalOffset then getLocalOffset returns the same offset\",\n run({ port }) {\n const offset: Vec3Tuple = [0.3, -0.1, 0.05];\n port.setLocalOffset(offset);\n assertVec3Close(port.getLocalOffset(), offset, POSE_EPS, \"getLocalOffset()\");\n },\n },\n {\n name: \"getWorldPose returns finite numbers and a unit quaternion\",\n run({ port }) {\n const pose = port.getWorldPose();\n for (const value of [...pose.position, ...pose.quaternion]) {\n assert(Number.isFinite(value), `getWorldPose() must return finite numbers, got ${value}`);\n }\n const length = quatLength(pose.quaternion);\n assert(\n closeTo(length, 1, POSE_EPS),\n `getWorldPose().quaternion must be a unit quaternion, length was ${length}`,\n );\n },\n },\n {\n name: \"setLocalRotation with a unit quaternion does not throw\",\n run({ port }) {\n try {\n port.setLocalRotation(QUARTER_TURN_Y);\n } catch (error) {\n throw new Error(`setLocalRotation(unit quaternion) must not throw, it threw: ${String(error)}`);\n }\n },\n },\n {\n name: \"getRestWorldPose returns the pose captured at registration\",\n run({ port, rest }) {\n assertPoseClose(port.getRestWorldPose(), rest, \"getRestWorldPose()\");\n },\n },\n {\n name: \"before any write, the live pose is the rest pose\",\n run({ port, rest }) {\n assertPoseClose(port.getWorldPose(), rest, \"getWorldPose()\");\n },\n },\n {\n name: \"getWorldPose follows setLocalOffset while getRestWorldPose stays put\",\n run({ port, rest }) {\n const offset: Vec3Tuple = [0.3, -0.1, 0.05];\n port.setLocalOffset(offset);\n assertVec3Close(\n port.getWorldPose().position,\n vAdd(rest.position, vApplyQuat(offset, rest.quaternion)),\n POSE_EPS,\n \"getWorldPose().position after setLocalOffset\",\n );\n assertPoseClose(port.getRestWorldPose(), rest, \"getRestWorldPose() after setLocalOffset\");\n },\n },\n {\n name: \"getWorldPose follows setLocalRotation while getRestWorldPose stays put\",\n run({ port, rest }) {\n port.setLocalRotation(QUARTER_TURN_Y);\n assertQuatClose(\n port.getWorldPose().quaternion,\n quatMultiply(rest.quaternion, QUARTER_TURN_Y),\n \"getWorldPose().quaternion after setLocalRotation\",\n );\n assertPoseClose(port.getRestWorldPose(), rest, \"getRestWorldPose() after setLocalRotation\");\n },\n },\n {\n name: \"setWorldPose, when present, is what getWorldPose reads back\",\n run({ port, rest }) {\n if (!port.setWorldPose) return;\n const target: PoseTuple = { position: [0.5, 1.2, -0.4], quaternion: QUARTER_TURN_Y };\n port.setWorldPose(target);\n assertPoseClose(port.getWorldPose(), target, \"getWorldPose() after setWorldPose\");\n assertPoseClose(port.getRestWorldPose(), rest, \"getRestWorldPose() after setWorldPose\");\n },\n },\n {\n name: \"every returned tuple is a fresh value the caller owns\",\n run({ port }) {\n const pose = port.getWorldPose();\n const restPose = port.getRestWorldPose();\n const offset = port.getLocalOffset();\n pose.position[0] = 1e6;\n pose.quaternion[3] = 1e6;\n restPose.position[0] = 1e6;\n offset[0] = 1e6;\n assert(port.getWorldPose().position[0] !== 1e6, \"writing to a returned getWorldPose() tuple must not change the port\");\n assert(port.getWorldPose().quaternion[3] !== 1e6, \"writing to a returned getWorldPose() quaternion must not change the port\");\n assert(port.getRestWorldPose().position[0] !== 1e6, \"writing to a returned getRestWorldPose() tuple must not change the port\");\n assert(port.getLocalOffset()[0] !== 1e6, \"writing to a returned getLocalOffset() tuple must not change the port\");\n },\n },\n {\n name: \"a tuple passed in is not kept\",\n run({ port }) {\n const offset: Vec3Tuple = [0.3, -0.1, 0.05];\n port.setLocalOffset(offset);\n offset[0] = 9;\n assertVec3Close(port.getLocalOffset(), [0.3, -0.1, 0.05], POSE_EPS, \"getLocalOffset() after the caller reused its tuple\");\n if (!port.setWorldPose) return;\n const target: PoseTuple = { position: [0.5, 1.2, -0.4], quaternion: [0, 0, 0, 1] };\n port.setWorldPose(target);\n target.position[0] = 9;\n assertVec3Close(port.getWorldPose().position, [0.5, 1.2, -0.4], POSE_EPS, \"getWorldPose() after the caller reused its tuple\");\n },\n },\n {\n name: \"setEffect, when present, accepts scale and emissive without throwing\",\n run({ port }) {\n if (!port.setEffect) return;\n try {\n port.setEffect({ scale: 1.2, emissive: 0.3 });\n } catch (error) {\n throw new Error(`setEffect({ scale, emissive }) must not throw, it threw: ${String(error)}`);\n }\n },\n },\n];\n\n// ---------------------------------------------------------------------------\n// Shared assertion helpers\n// ---------------------------------------------------------------------------\n\nfunction assert(condition: boolean, message: string): asserts condition {\n if (!condition) throw new Error(message);\n}\n\nfunction closeTo(a: number, b: number, eps: number): boolean {\n return Math.abs(a - b) <= eps;\n}\n\n// Indexed one component at a time, rather than in a loop, so each index is a\n// literal: `noUncheckedIndexedAccess` cannot narrow a tuple read through a\n// variable index back to `number`.\nfunction assertVec3Close(value: Vec3Tuple, expected: Vec3Tuple, eps: number, label: string): void {\n assert(closeTo(value[0], expected[0], eps), `${label}[0] must be close to ${expected[0]}, got ${value[0]}`);\n assert(closeTo(value[1], expected[1], eps), `${label}[1] must be close to ${expected[1]}, got ${value[1]}`);\n assert(closeTo(value[2], expected[2], eps), `${label}[2] must be close to ${expected[2]}, got ${value[2]}`);\n}\n\n// q and -q are the same rotation, so compare by the size of the dot product.\nfunction assertQuatClose(value: QuatTuple, expected: QuatTuple, label: string): void {\n const dot = value[0] * expected[0] + value[1] * expected[1] + value[2] * expected[2] + value[3] * expected[3];\n assert(\n closeTo(Math.abs(dot), 1, POSE_EPS),\n `${label} must be close to [${expected.join(\", \")}], got [${value.join(\", \")}]`,\n );\n}\n\nfunction assertPoseClose(value: PoseTuple, expected: PoseTuple, label: string): void {\n assertVec3Close(value.position, expected.position, POSE_EPS, `${label}.position`);\n assertQuatClose(value.quaternion, expected.quaternion, `${label}.quaternion`);\n}\n\nfunction quatLength(q: QuatTuple): number {\n return Math.sqrt(q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]);\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -19,6 +19,7 @@ export * from "./math.js";
19
19
  export * from "./events.js";
20
20
  export * from "./feedback.js";
21
21
  export * from "./ports.js";
22
+ export * from "./contract-cases.js";
22
23
  export * from "./gaze.js";
23
24
  export * from "./descriptor.js";
24
25
  export * from "./velocity-tracker.js";
package/dist/index.js CHANGED
@@ -19,6 +19,7 @@ export * from "./math.js";
19
19
  export * from "./events.js";
20
20
  export * from "./feedback.js";
21
21
  export * from "./ports.js";
22
+ export * from "./contract-cases.js";
22
23
  export * from "./gaze.js";
23
24
  export * from "./descriptor.js";
24
25
  export * from "./velocity-tracker.js";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,cAAc,gCAAgC,CAAC;AAE/C,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,iBAAiB,CAAC;AAChC,cAAc,uBAAuB,CAAC;AACtC,cAAc,cAAc,CAAC;AAC7B,cAAc,qBAAqB,CAAC;AAEpC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,sBAAsB,CAAC;AACrC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,yBAAyB,CAAC","sourcesContent":["/**\n * @realitycollective/webxr-interactions - the engine-free interaction core.\n *\n * Layers (each strictly on the one below):\n * - behaviours (press, pulse, hinge, dial, slide, grab, tossScore) -\n * pure state machines driven by `update(dt)`;\n * - the runtime/binder - targeting, press/grab transitions, gaze gating\n * + dwell, capability negotiation, events, feedback intents;\n * - the shared input contracts, re-exported from\n * `@realitycollective/webxr-input`.\n *\n * Pair with an engine adapter: `@realitycollective/threejs-interactions`\n * (standalone WebXR + three.js), `@realitycollective/iwsdk-interactions`\n * (Meta IWSDK) or `@realitycollective/xrblocks-interactions` (Google XR\n * Blocks, experimental).\n */\nexport * from \"@realitycollective/webxr-input\";\n\nexport * from \"./math.js\";\nexport * from \"./events.js\";\nexport * from \"./feedback.js\";\nexport * from \"./ports.js\";\nexport * from \"./gaze.js\";\nexport * from \"./descriptor.js\";\nexport * from \"./velocity-tracker.js\";\nexport * from \"./runtime.js\";\nexport * from \"./pointer-bridge.js\";\n\nexport * from \"./behaviours/behaviour.js\";\nexport * from \"./behaviours/press.js\";\nexport * from \"./behaviours/pulse.js\";\nexport * from \"./behaviours/hinge.js\";\nexport * from \"./behaviours/dial.js\";\nexport * from \"./behaviours/slide.js\";\nexport * from \"./behaviours/grab.js\";\nexport * from \"./behaviours/toss-score.js\";\nexport * from \"./behaviours/factory.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,cAAc,gCAAgC,CAAC;AAE/C,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,qBAAqB,CAAC;AACpC,cAAc,WAAW,CAAC;AAC1B,cAAc,iBAAiB,CAAC;AAChC,cAAc,uBAAuB,CAAC;AACtC,cAAc,cAAc,CAAC;AAC7B,cAAc,qBAAqB,CAAC;AAEpC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,sBAAsB,CAAC;AACrC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,yBAAyB,CAAC","sourcesContent":["/**\n * @realitycollective/webxr-interactions - the engine-free interaction core.\n *\n * Layers (each strictly on the one below):\n * - behaviours (press, pulse, hinge, dial, slide, grab, tossScore) -\n * pure state machines driven by `update(dt)`;\n * - the runtime/binder - targeting, press/grab transitions, gaze gating\n * + dwell, capability negotiation, events, feedback intents;\n * - the shared input contracts, re-exported from\n * `@realitycollective/webxr-input`.\n *\n * Pair with an engine adapter: `@realitycollective/threejs-interactions`\n * (standalone WebXR + three.js), `@realitycollective/iwsdk-interactions`\n * (Meta IWSDK) or `@realitycollective/xrblocks-interactions` (Google XR\n * Blocks, experimental).\n */\nexport * from \"@realitycollective/webxr-input\";\n\nexport * from \"./math.js\";\nexport * from \"./events.js\";\nexport * from \"./feedback.js\";\nexport * from \"./ports.js\";\nexport * from \"./contract-cases.js\";\nexport * from \"./gaze.js\";\nexport * from \"./descriptor.js\";\nexport * from \"./velocity-tracker.js\";\nexport * from \"./runtime.js\";\nexport * from \"./pointer-bridge.js\";\n\nexport * from \"./behaviours/behaviour.js\";\nexport * from \"./behaviours/press.js\";\nexport * from \"./behaviours/pulse.js\";\nexport * from \"./behaviours/hinge.js\";\nexport * from \"./behaviours/dial.js\";\nexport * from \"./behaviours/slide.js\";\nexport * from \"./behaviours/grab.js\";\nexport * from \"./behaviours/toss-score.js\";\nexport * from \"./behaviours/factory.js\";\n"]}
package/dist/ports.d.ts CHANGED
@@ -16,6 +16,9 @@ export interface InteractableHit {
16
16
  * Implemented by the adapter (three.js Raycaster, engine BVH, …). A
17
17
  * provider that pre-resolves targeting (IWSDK) may make this redundant -
18
18
  * provider hints always take precedence over hit-tester results.
19
+ *
20
+ * Each hit, and the `point` tuple in it, is a fresh value the caller owns.
21
+ * A ray or point passed in is read during the call and not kept.
19
22
  */
20
23
  export interface HitTester {
21
24
  hitRay(ray: RayTuple): InteractableHit | null;
@@ -30,10 +33,23 @@ export interface HitTester {
30
33
  * registration and applies these relative to it, so behaviours compose
31
34
  * with anything else animating the same object (the pale-signal
32
35
  * non-destructive-offset rule).
36
+ *
37
+ * Every tuple a port returns is a fresh value the caller owns. The port
38
+ * keeps no reference to it and never writes to it again. A tuple passed in
39
+ * is read during the call and not kept, so the caller may reuse it.
33
40
  */
34
41
  export interface TransformPort {
35
- /** Current world pose. */
42
+ /**
43
+ * The LIVE world pose: where the object is now, after every offset,
44
+ * rotation and world-pose write, including a write made by anything else.
45
+ */
36
46
  getWorldPose(): PoseTuple;
47
+ /**
48
+ * The rest pose captured at registration, in world space. Offsets and
49
+ * rotations are measured from it, so it stays put while the port writes.
50
+ * It follows a parent that moves.
51
+ */
52
+ getRestWorldPose(): PoseTuple;
37
53
  /** Local-space pose relative to the captured rest (dragged observation). */
38
54
  getLocalOffset(): Vec3Tuple;
39
55
  /** Additive local-space offset from rest (driven press / slide). */
package/dist/ports.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"ports.js","sourceRoot":"","sources":["../src/ports.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * Engine-side ports - the space queries and transform writes the core\n * cannot perform itself. Adapters implement these against their scene\n * graph; the core only ever speaks tuples and interactable ids.\n */\nimport type { PoseTuple, QuatTuple, RayTuple, Vec3Tuple } from \"@realitycollective/webxr-input\";\n\nexport interface InteractableHit {\n interactableId: string;\n /** Distance from the query origin (ray origin / probe point). */\n distance: number;\n /** World-space hit or closest point. */\n point: Vec3Tuple;\n}\n\n/**\n * Resolves which interactable a ray or a proximity probe touches.\n * Implemented by the adapter (three.js Raycaster, engine BVH, …). A\n * provider that pre-resolves targeting (IWSDK) may make this redundant -\n * provider hints always take precedence over hit-tester results.\n */\nexport interface HitTester {\n hitRay(ray: RayTuple): InteractableHit | null;\n hitProximity(point: Vec3Tuple, radius: number): InteractableHit | null;\n}\n\n/**\n * The transform/effect surface of ONE interactable's scene object.\n * Behaviours write through this; the adapter owns how writes land\n * (Object3D fields, ECS components, …).\n *\n * Offsets/rotations are FROM REST: the adapter captures the rest pose at\n * registration and applies these relative to it, so behaviours compose\n * with anything else animating the same object (the pale-signal\n * non-destructive-offset rule).\n */\nexport interface TransformPort {\n /** Current world pose. */\n getWorldPose(): PoseTuple;\n /** Local-space pose relative to the captured rest (dragged observation). */\n getLocalOffset(): Vec3Tuple;\n /** Additive local-space offset from rest (driven press / slide). */\n setLocalOffset(offset: Vec3Tuple): void;\n /** Local rotation about the rest orientation (hinge / dial output). */\n setLocalRotation(quaternion: QuatTuple): void;\n /** Follow a world pose while grabbed (poseOnly grab fulfilment). */\n setWorldPose?(pose: PoseTuple): void;\n /** Additive presentation effect (pulse): scale multiplier & emissive boost. */\n setEffect?(effect: { scale?: number; emissive?: number }): void;\n}\n"]}
1
+ {"version":3,"file":"ports.js","sourceRoot":"","sources":["../src/ports.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * Engine-side ports - the space queries and transform writes the core\n * cannot perform itself. Adapters implement these against their scene\n * graph; the core only ever speaks tuples and interactable ids.\n */\nimport type { PoseTuple, QuatTuple, RayTuple, Vec3Tuple } from \"@realitycollective/webxr-input\";\n\nexport interface InteractableHit {\n interactableId: string;\n /** Distance from the query origin (ray origin / probe point). */\n distance: number;\n /** World-space hit or closest point. */\n point: Vec3Tuple;\n}\n\n/**\n * Resolves which interactable a ray or a proximity probe touches.\n * Implemented by the adapter (three.js Raycaster, engine BVH, …). A\n * provider that pre-resolves targeting (IWSDK) may make this redundant -\n * provider hints always take precedence over hit-tester results.\n *\n * Each hit, and the `point` tuple in it, is a fresh value the caller owns.\n * A ray or point passed in is read during the call and not kept.\n */\nexport interface HitTester {\n hitRay(ray: RayTuple): InteractableHit | null;\n hitProximity(point: Vec3Tuple, radius: number): InteractableHit | null;\n}\n\n/**\n * The transform/effect surface of ONE interactable's scene object.\n * Behaviours write through this; the adapter owns how writes land\n * (Object3D fields, ECS components, …).\n *\n * Offsets/rotations are FROM REST: the adapter captures the rest pose at\n * registration and applies these relative to it, so behaviours compose\n * with anything else animating the same object (the pale-signal\n * non-destructive-offset rule).\n *\n * Every tuple a port returns is a fresh value the caller owns. The port\n * keeps no reference to it and never writes to it again. A tuple passed in\n * is read during the call and not kept, so the caller may reuse it.\n */\nexport interface TransformPort {\n /**\n * The LIVE world pose: where the object is now, after every offset,\n * rotation and world-pose write, including a write made by anything else.\n */\n getWorldPose(): PoseTuple;\n /**\n * The rest pose captured at registration, in world space. Offsets and\n * rotations are measured from it, so it stays put while the port writes.\n * It follows a parent that moves.\n */\n getRestWorldPose(): PoseTuple;\n /** Local-space pose relative to the captured rest (dragged observation). */\n getLocalOffset(): Vec3Tuple;\n /** Additive local-space offset from rest (driven press / slide). */\n setLocalOffset(offset: Vec3Tuple): void;\n /** Local rotation about the rest orientation (hinge / dial output). */\n setLocalRotation(quaternion: QuatTuple): void;\n /** Follow a world pose while grabbed (poseOnly grab fulfilment). */\n setWorldPose?(pose: PoseTuple): void;\n /** Additive presentation effect (pulse): scale multiplier & emissive boost. */\n setEffect?(effect: { scale?: number; emissive?: number }): void;\n}\n"]}
package/dist/runtime.d.ts CHANGED
@@ -111,6 +111,8 @@ export declare class InteractionRuntime {
111
111
  /** Resolve this source's target: hints beat poke beats ray. */
112
112
  private resolveTarget;
113
113
  private targetable;
114
+ /** The largest poke radius among enabled interactables; the default when none is registered. */
115
+ private maxPokeRadius;
114
116
  private flags;
115
117
  private updateSource;
116
118
  private gazeAllows;
package/dist/runtime.js CHANGED
@@ -249,9 +249,18 @@ export class InteractionRuntime {
249
249
  if (hint && this.targetable(hint.targetId))
250
250
  return hint.targetId;
251
251
  if (source.indexTip && this.hitTester) {
252
- const poke = this.hitTester.hitProximity(source.indexTip, DEFAULT_POKE_RADIUS);
253
- if (poke && this.targetable(poke.interactableId))
254
- return poke.interactableId;
252
+ // One proximity query at the largest radius any interactable asked for,
253
+ // then the nearest hit must be inside ITS OWN radius. A per-interactable
254
+ // query would be a hit test per registration per source per frame; this
255
+ // keeps it to one. The trade: a farther interactable with a bigger radius
256
+ // is not found behind a nearer one with a smaller radius, because the
257
+ // tester returns only the nearest.
258
+ const poke = this.hitTester.hitProximity(source.indexTip, this.maxPokeRadius());
259
+ if (poke && this.targetable(poke.interactableId)) {
260
+ const registered = this.interactables.get(poke.interactableId);
261
+ if (poke.distance <= registered.pokeRadius)
262
+ return poke.interactableId;
263
+ }
255
264
  }
256
265
  if (source.ray && this.hitTester) {
257
266
  const hit = this.hitTester.hitRay(source.ray);
@@ -264,6 +273,15 @@ export class InteractionRuntime {
264
273
  const registered = this.interactables.get(id);
265
274
  return registered !== undefined && registered.enabled;
266
275
  }
276
+ /** The largest poke radius among enabled interactables; the default when none is registered. */
277
+ maxPokeRadius() {
278
+ let radius = 0;
279
+ for (const registered of this.interactables.values()) {
280
+ if (registered.enabled && registered.pokeRadius > radius)
281
+ radius = registered.pokeRadius;
282
+ }
283
+ return radius > 0 ? radius : DEFAULT_POKE_RADIUS;
284
+ }
267
285
  flags(registered) {
268
286
  let pressable = false;
269
287
  let grabbable = false;
@@ -1 +1 @@
1
- {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * InteractionRuntime - the engine-free orchestrator.\n *\n * Once per host frame the runtime pulls the input provider, resolves\n * targeting (provider hints first - provider power wins - then poke\n * proximity, then ray hits), applies press/grab transitions with\n * hysteresis, runs gaze gating + dwell, ticks every behaviour, and emits\n * interaction events and feedback intents. It never touches an engine:\n * space queries and transform writes go through the adapter-implemented\n * {@link HitTester} and {@link TransformPort}.\n *\n * Lifecycle is first-class (the pale-signal gap): interactables register,\n * unregister, enable and disable; behaviours disabled by capability\n * negotiation announce themselves with a `behaviourDisabled` event.\n */\nimport {\n SELECT_PRESS_THRESHOLD,\n SELECT_RELEASE_THRESHOLD,\n unmetRequirements,\n type InputCapabilities,\n type InputHitHint,\n type InputProvider,\n type InputSourceSnapshot,\n type PoseTuple,\n type RayTuple,\n type Unsubscribe,\n} from \"@realitycollective/webxr-input\";\nimport type { Behaviour, InteractorInfo } from \"./behaviours/behaviour.js\";\nimport {\n createBehaviour,\n type BehaviourConfig,\n type BehaviourFactoryContext,\n} from \"./behaviours/factory.js\";\nimport { GrabBehaviour } from \"./behaviours/grab.js\";\nimport type { InteractableDescriptor } from \"./descriptor.js\";\nimport { Emitter, type InteractionEvent, type InteractionEventListener } from \"./events.js\";\nimport type { FeedbackIntent, FeedbackListener } from \"./feedback.js\";\nimport { DwellState, resolveDwellConfig, DWELL_DEFAULTS, type DwellConfig } from \"./gaze.js\";\nimport { vApplyQuat } from \"./math.js\";\nimport type { HitTester, TransformPort } from \"./ports.js\";\nimport { VelocityTracker, type VelocityTrackerOptions } from \"./velocity-tracker.js\";\n\nconst DEFAULT_POKE_RADIUS = 0.05;\n/** Interactor id used when gaze is synthesized from the head pose. */\nexport const HEAD_GAZE_INTERACTOR_ID = \"head-gaze\";\n\nexport interface InteractableState {\n id: string;\n enabled: boolean;\n hovered: boolean;\n gazeHovered: boolean;\n pressed: boolean;\n grabbed: boolean;\n /** Primary behaviour's logical value. */\n value: number;\n /** Gaze-dwell meter 0..1 (0 when dwell is not enabled). */\n dwell: number;\n}\n\ninterface BehaviourSlot {\n instance: Behaviour;\n config: BehaviourConfig;\n disabled: boolean;\n disabledReason: string;\n}\n\ninterface Registered {\n def: InteractableDescriptor;\n behaviours: BehaviourSlot[];\n transform: TransformPort | undefined;\n enabled: boolean;\n pokeRadius: number;\n gazeRequired: boolean;\n dwellConfig: Required<DwellConfig> | null;\n dwell: DwellState;\n gazeHovered: boolean;\n hoveredBy: Set<string>;\n pressedBy: Set<string>;\n grabbedBy: string | null;\n}\n\ninterface SourceRuntimeState {\n hoverTarget: string | null;\n pressed: boolean;\n pressTarget: string | null;\n grabbed: boolean;\n grabTarget: string | null;\n}\n\nexport interface InteractionRuntimeOptions {\n provider: InputProvider;\n hitTester?: HitTester;\n dwellDefaults?: DwellConfig;\n /**\n * Per-source velocity tracking. On by default with no smoothing; pass\n * `false` to skip it, or options to smooth. The tracker only fills in\n * what the provider did not supply.\n */\n velocity?: VelocityTrackerOptions | false;\n}\n\nexport interface RegisterPorts {\n transform?: TransformPort;\n}\n\nexport class InteractionRuntime {\n private readonly provider: InputProvider;\n private hitTester: HitTester | null;\n private readonly dwellDefaults: Required<DwellConfig>;\n\n private readonly interactables = new Map<string, Registered>();\n private readonly sourceStates = new Map<string, SourceRuntimeState>();\n private readonly events = new Emitter<InteractionEvent>();\n private readonly feedbackEmitter = new Emitter<FeedbackIntent>();\n private readonly afterSample = new Emitter<readonly InputSourceSnapshot[]>();\n private readonly velocityTracker: VelocityTracker | null;\n private lastSources = new Map<string, InputSourceSnapshot>();\n private capabilities: InputCapabilities;\n private readonly unsubscribeCaps: Unsubscribe;\n private disposed = false;\n\n constructor(options: InteractionRuntimeOptions) {\n this.provider = options.provider;\n this.hitTester = options.hitTester ?? null;\n const d = options.dwellDefaults;\n this.dwellDefaults = {\n holdSeconds: d?.holdSeconds ?? DWELL_DEFAULTS.holdSeconds,\n decayFactor: d?.decayFactor ?? DWELL_DEFAULTS.decayFactor,\n rearmBelow: d?.rearmBelow ?? DWELL_DEFAULTS.rearmBelow,\n };\n this.velocityTracker =\n options.velocity === false ? null : new VelocityTracker(options.velocity ?? {});\n this.capabilities = this.provider.getCapabilities();\n this.unsubscribeCaps = this.provider.onCapabilitiesChanged((caps) => {\n this.capabilities = caps;\n this.renegotiateAll();\n });\n }\n\n // -- registry -------------------------------------------------------------\n\n registerInteractable(def: InteractableDescriptor, ports: RegisterPorts = {}): void {\n if (this.interactables.has(def.id)) {\n throw new Error(`Interactable \"${def.id}\" is already registered`);\n }\n const context: BehaviourFactoryContext = {\n grabFulfilment: this.capabilities.grabs === \"native\" ? \"native\" : \"poseOnly\",\n };\n const registered: Registered = {\n def,\n behaviours: def.behaviours.map((config) => ({\n instance: createBehaviour(config, context),\n config,\n disabled: false,\n disabledReason: \"\",\n })),\n transform: ports.transform,\n enabled: def.enabled !== false,\n pokeRadius: def.pokeRadius ?? DEFAULT_POKE_RADIUS,\n gazeRequired: def.gaze?.required === true,\n dwellConfig: resolveDwellConfig(def.gaze?.dwell, this.dwellDefaults),\n dwell: new DwellState(),\n gazeHovered: false,\n hoveredBy: new Set(),\n pressedBy: new Set(),\n grabbedBy: null,\n };\n this.interactables.set(def.id, registered);\n this.renegotiate(def.id, registered);\n }\n\n unregisterInteractable(id: string): void {\n const registered = this.interactables.get(id);\n if (!registered) return;\n this.releaseAllHolds(id);\n this.interactables.delete(id);\n }\n\n setInteractableEnabled(id: string, enabled: boolean): void {\n const registered = this.interactables.get(id);\n if (!registered || registered.enabled === enabled) return;\n registered.enabled = enabled;\n if (!enabled) this.releaseAllHolds(id);\n this.events.emit({\n type: enabled ? \"interactableEnabled\" : \"interactableDisabled\",\n interactableId: id,\n });\n }\n\n setHitTester(hitTester: HitTester | null): void {\n this.hitTester = hitTester;\n }\n\n /** Reach a live behaviour instance for tuning (playground steppers). */\n getBehaviour<T extends Behaviour = Behaviour>(interactableId: string, kind: string): T | undefined {\n const registered = this.interactables.get(interactableId);\n return registered?.behaviours.find((b) => b.instance.kind === kind)?.instance as T | undefined;\n }\n\n getState(id: string): InteractableState | undefined {\n const r = this.interactables.get(id);\n if (!r) return undefined;\n const primary = r.behaviours.find((b) => !b.disabled)?.instance;\n return {\n id,\n enabled: r.enabled,\n hovered: r.hoveredBy.size > 0 || r.gazeHovered,\n gazeHovered: r.gazeHovered,\n pressed: r.pressedBy.size > 0,\n grabbed: r.grabbedBy !== null,\n value: primary?.getValue() ?? 0,\n dwell: r.dwell.getProgress(),\n };\n }\n\n getInteractableIds(): string[] {\n return [...this.interactables.keys()];\n }\n\n // -- subscriptions ----------------------------------------------------------\n\n onEvent(listener: InteractionEventListener): Unsubscribe {\n return this.events.subscribe(listener);\n }\n\n onFeedback(listener: FeedbackListener): Unsubscribe {\n return this.feedbackEmitter.subscribe(listener);\n }\n\n /**\n * Sampled sources for this frame - the UI Extensions pointer bridge hook.\n *\n * @deprecated Use {@link onSample}. Both deliver the same stream: since\n * `@realitycollective/webxr-input` 0.1.1 carries `linearVelocity` and\n * `angularVelocity` on `InputSourceSnapshot`, the two signatures are\n * identical and only one name is needed. This one is kept so existing\n * callers keep working and will be removed in a later major release.\n */\n onSourcesSampled(listener: (sources: readonly InputSourceSnapshot[]) => void): Unsubscribe {\n return this.afterSample.subscribe(listener);\n }\n\n /**\n * This frame's sources, velocity included.\n *\n * The snapshots are the provider's own objects for this frame and are\n * never written to again once delivered (the ownership rule on\n * `InputSourceSnapshot`), so a listener may keep one, or a tuple inside\n * it, across frames without copying.\n */\n onSample(listener: (sources: readonly InputSourceSnapshot[]) => void): Unsubscribe {\n return this.afterSample.subscribe(listener);\n }\n\n /**\n * The last sample of one source, velocity included. Undefined before the\n * first update, and once the source stops reporting.\n *\n * The snapshot is the caller's to keep: it is never mutated after the\n * frame it was sampled in, and it is not refreshed in place, so call\n * again for a newer one.\n */\n getSource(id: string): InputSourceSnapshot | undefined {\n return this.lastSources.get(id);\n }\n\n getCapabilities(): InputCapabilities {\n return this.capabilities;\n }\n\n getProvider(): InputProvider {\n return this.provider;\n }\n\n // -- frame ------------------------------------------------------------------\n\n update(dt: number): void {\n if (this.disposed) return;\n const sampled = this.provider.sample();\n const sources = this.velocityTracker ? this.velocityTracker.update(sampled, dt) : sampled;\n const hints = this.provider.sampleHints?.() ?? [];\n this.lastSources = new Map(sources.map((source) => [source.id, source]));\n this.afterSample.emit(sources);\n\n const hintBySource = new Map<string, InputHitHint[]>();\n for (const hint of hints) {\n const list = hintBySource.get(hint.sourceId);\n if (list) list.push(hint);\n else hintBySource.set(hint.sourceId, [hint]);\n }\n\n const seen = new Set<string>();\n for (const source of sources) {\n seen.add(source.id);\n this.updateSource(source, hintBySource.get(source.id) ?? []);\n }\n // Sources that vanished mid-hold release everything they held.\n for (const [id, state] of this.sourceStates) {\n if (!seen.has(id)) {\n this.forceRelease(id, state);\n this.sourceStates.delete(id);\n }\n }\n\n this.updateGaze(sources, dt);\n this.tickBehaviours(sources, dt);\n }\n\n dispose(): void {\n this.disposed = true;\n this.unsubscribeCaps();\n this.interactables.clear();\n this.sourceStates.clear();\n this.lastSources.clear();\n this.velocityTracker?.reset();\n }\n\n // -- internals ---------------------------------------------------------------\n\n private sourceState(id: string): SourceRuntimeState {\n let state = this.sourceStates.get(id);\n if (!state) {\n state = { hoverTarget: null, pressed: false, pressTarget: null, grabbed: false, grabTarget: null };\n this.sourceStates.set(id, state);\n }\n return state;\n }\n\n private interactorInfo(source: InputSourceSnapshot): InteractorInfo {\n const info: InteractorInfo = {\n id: source.id,\n kind: source.kind,\n select: source.select,\n squeeze: source.squeeze,\n };\n if (source.ray) info.ray = source.ray;\n if (source.gripPose) info.gripPose = source.gripPose;\n if (source.indexTip) info.indexTip = source.indexTip;\n return info;\n }\n\n /** Resolve this source's target: hints beat poke beats ray. */\n private resolveTarget(source: InputSourceSnapshot, hints: InputHitHint[]): string | null {\n const hint =\n hints.find((h) => h.state === \"grab\") ??\n hints.find((h) => h.state === \"press\") ??\n hints.find((h) => h.state === \"hover\");\n if (hint && this.targetable(hint.targetId)) return hint.targetId;\n if (source.indexTip && this.hitTester) {\n const poke = this.hitTester.hitProximity(source.indexTip, DEFAULT_POKE_RADIUS);\n if (poke && this.targetable(poke.interactableId)) return poke.interactableId;\n }\n if (source.ray && this.hitTester) {\n const hit = this.hitTester.hitRay(source.ray);\n if (hit && this.targetable(hit.interactableId)) return hit.interactableId;\n }\n return null;\n }\n\n private targetable(id: string): boolean {\n const registered = this.interactables.get(id);\n return registered !== undefined && registered.enabled;\n }\n\n private flags(registered: Registered): { pressable: boolean; grabbable: boolean } {\n let pressable = false;\n let grabbable = false;\n for (const slot of registered.behaviours) {\n if (slot.disabled) continue;\n if (slot.instance.onPressStart) pressable = true;\n if (slot.instance.grabbable) grabbable = true;\n }\n return { pressable, grabbable };\n }\n\n private updateSource(source: InputSourceSnapshot, hints: InputHitHint[]): void {\n const state = this.sourceState(source.id);\n const target = this.resolveTarget(source, hints);\n\n // Hover transitions.\n if (state.hoverTarget !== target) {\n if (state.hoverTarget) this.setHover(state.hoverTarget, source.id, false);\n if (target) this.setHover(target, source.id, true);\n state.hoverTarget = target;\n }\n\n const registered = target ? this.interactables.get(target) : undefined;\n const roles = registered ? this.flags(registered) : { pressable: false, grabbable: false };\n\n // Signal resolution. A pinching hand (or a 2D pointer click) acts as a\n // grab when the target is grab-only; controllers keep trigger=press,\n // squeeze=grab.\n const selectAsGrab = roles.grabbable && !roles.pressable && source.kind !== \"controller\";\n const pressSignal = selectAsGrab ? 0 : source.select;\n const grabSignal = Math.max(source.squeeze, selectAsGrab ? source.select : 0);\n\n const hintPress = hints.some((h) => h.state === \"press\");\n const hintGrab = hints.some((h) => h.state === \"grab\") || source.nativeGrabbing === true;\n\n // Press with hysteresis (hints override).\n const pressActive = hintPress || (state.pressed\n ? pressSignal > SELECT_RELEASE_THRESHOLD\n : pressSignal >= SELECT_PRESS_THRESHOLD);\n if (pressActive && !state.pressed) {\n state.pressed = true;\n if (target && registered && roles.pressable && this.gazeAllows(registered)) {\n state.pressTarget = target;\n this.routePressStart(target, registered, this.interactorInfo(source));\n }\n } else if (!pressActive && state.pressed) {\n state.pressed = false;\n if (state.pressTarget) {\n this.routePressEnd(state.pressTarget, source.id, this.interactorInfo(source));\n state.pressTarget = null;\n }\n }\n\n // Grab with hysteresis (hints / native grabbing override).\n const grabActive = hintGrab || (state.grabbed\n ? grabSignal > SELECT_RELEASE_THRESHOLD\n : grabSignal >= SELECT_PRESS_THRESHOLD);\n if (grabActive && !state.grabbed) {\n state.grabbed = true;\n const hintTargetId = hints.find((h) => h.state === \"grab\")?.targetId;\n const grabTargetId = hintTargetId && this.targetable(hintTargetId) ? hintTargetId : target;\n const grabTarget = grabTargetId ? this.interactables.get(grabTargetId) : undefined;\n if (\n grabTargetId &&\n grabTarget &&\n this.flags(grabTarget).grabbable &&\n grabTarget.grabbedBy === null &&\n this.gazeAllows(grabTarget)\n ) {\n state.grabTarget = grabTargetId;\n grabTarget.grabbedBy = source.id;\n const info = this.interactorInfo(source);\n for (const slot of grabTarget.behaviours) {\n if (!slot.disabled && slot.instance.grabbable) {\n slot.instance.onGrabStart?.(this.contextFor(grabTargetId, grabTarget, 0), info);\n }\n }\n }\n } else if (!grabActive && state.grabbed) {\n state.grabbed = false;\n if (state.grabTarget) {\n this.endGrab(state.grabTarget, source.id, this.interactorInfo(source));\n state.grabTarget = null;\n }\n }\n }\n\n private gazeAllows(registered: Registered): boolean {\n return !registered.gazeRequired || registered.gazeHovered;\n }\n\n private setHover(id: string, sourceId: string, hovered: boolean): void {\n const registered = this.interactables.get(id);\n if (!registered) return;\n if (hovered) {\n registered.hoveredBy.add(sourceId);\n if (registered.hoveredBy.size === 1) {\n this.events.emit({ type: \"hoverEnter\", interactableId: id, interactorId: sourceId });\n this.feedbackEmitter.emit({\n cue: \"hover\",\n interactableId: id,\n sourceId,\n intensity: 0.1,\n durationMs: 10,\n });\n }\n } else {\n registered.hoveredBy.delete(sourceId);\n if (registered.hoveredBy.size === 0) {\n this.events.emit({ type: \"hoverExit\", interactableId: id, interactorId: sourceId });\n }\n }\n }\n\n private routePressStart(id: string, registered: Registered, info: InteractorInfo): void {\n registered.pressedBy.add(info.id);\n this.events.emit({ type: \"pressStart\", interactableId: id, interactorId: info.id });\n const ctx = this.contextFor(id, registered, 0);\n for (const slot of registered.behaviours) {\n if (!slot.disabled) slot.instance.onPressStart?.(ctx, info);\n }\n }\n\n private routePressEnd(id: string, sourceId: string, info: InteractorInfo): void {\n const registered = this.interactables.get(id);\n if (!registered) return;\n registered.pressedBy.delete(sourceId);\n this.events.emit({ type: \"pressEnd\", interactableId: id, interactorId: sourceId });\n const ctx = this.contextFor(id, registered, 0);\n for (const slot of registered.behaviours) {\n if (!slot.disabled) slot.instance.onPressEnd?.(ctx, info);\n }\n }\n\n private endGrab(id: string, sourceId: string, info: InteractorInfo): void {\n const registered = this.interactables.get(id);\n if (!registered || registered.grabbedBy !== sourceId) return;\n registered.grabbedBy = null;\n const ctx = this.contextFor(id, registered, 0);\n for (const slot of registered.behaviours) {\n if (!slot.disabled && slot.instance.grabbable) slot.instance.onGrabEnd?.(ctx, info);\n }\n }\n\n private forceRelease(sourceId: string, state: SourceRuntimeState): void {\n const info: InteractorInfo = { id: sourceId, kind: \"other\", select: 0, squeeze: 0 };\n if (state.pressTarget) this.routePressEnd(state.pressTarget, sourceId, info);\n if (state.grabTarget) this.endGrab(state.grabTarget, sourceId, info);\n if (state.hoverTarget) this.setHover(state.hoverTarget, sourceId, false);\n }\n\n private releaseAllHolds(id: string): void {\n for (const [sourceId, state] of this.sourceStates) {\n if (state.pressTarget === id) {\n this.routePressEnd(id, sourceId, { id: sourceId, kind: \"other\", select: 0, squeeze: 0 });\n state.pressTarget = null;\n state.pressed = false;\n }\n if (state.grabTarget === id) {\n this.endGrab(id, sourceId, { id: sourceId, kind: \"other\", select: 0, squeeze: 0 });\n state.grabTarget = null;\n state.grabbed = false;\n }\n if (state.hoverTarget === id) {\n this.setHover(id, sourceId, false);\n state.hoverTarget = null;\n }\n }\n }\n\n private updateGaze(sources: readonly InputSourceSnapshot[], dt: number): void {\n let gazeRay: RayTuple | null = null;\n let gazeId = HEAD_GAZE_INTERACTOR_ID;\n const gazeSource = sources.find((s) => s.kind === \"gaze\");\n if (gazeSource?.ray) {\n gazeRay = gazeSource.ray;\n gazeId = gazeSource.id;\n } else if (this.capabilities.headPose && this.provider.getHeadPose) {\n const head: PoseTuple = this.provider.getHeadPose();\n gazeRay = {\n origin: head.position,\n direction: vApplyQuat([0, 0, -1], head.quaternion),\n };\n }\n\n const gazeTarget =\n gazeRay && this.hitTester ? this.hitTester.hitRay(gazeRay)?.interactableId ?? null : null;\n\n for (const [id, registered] of this.interactables) {\n const hovered = registered.enabled && gazeTarget === id;\n if (hovered !== registered.gazeHovered) {\n registered.gazeHovered = hovered;\n this.events.emit({\n type: hovered ? \"hoverEnter\" : \"hoverExit\",\n interactableId: id,\n interactorId: gazeId,\n });\n }\n if (registered.dwellConfig && registered.enabled) {\n const tick = registered.dwell.update(hovered, dt, registered.dwellConfig);\n if (tick.changed) {\n this.events.emit({\n type: \"dwellProgress\",\n interactableId: id,\n interactorId: gazeId,\n value: tick.progress,\n });\n }\n if (tick.fired) {\n // A completed dwell is a synthesized click: press + release.\n const info: InteractorInfo = { id: gazeId, kind: \"gaze\", select: 1, squeeze: 0 };\n this.routePressStart(id, registered, info);\n this.routePressEnd(id, gazeId, { ...info, select: 0 });\n this.feedbackEmitter.emit({\n cue: \"dwellComplete\",\n interactableId: id,\n intensity: 0.6,\n durationMs: 50,\n });\n }\n }\n }\n }\n\n private tickBehaviours(sources: readonly InputSourceSnapshot[], dt: number): void {\n const bySourceId = new Map(sources.map((s) => [s.id, s]));\n for (const [id, registered] of this.interactables) {\n if (!registered.enabled) continue;\n const holderSource = registered.grabbedBy ? bySourceId.get(registered.grabbedBy) : undefined;\n const holder = holderSource ? this.interactorInfo(holderSource) : undefined;\n const ctx = this.contextFor(id, registered, dt);\n for (const slot of registered.behaviours) {\n if (slot.disabled) continue;\n slot.instance.update(ctx, slot.instance.grabbable ? holder : undefined);\n }\n }\n }\n\n private contextFor(id: string, registered: Registered, dt: number) {\n return {\n dt,\n interactableId: id,\n transform: registered.transform,\n emit: (event: Omit<InteractionEvent, \"interactableId\">) =>\n this.events.emit({ ...event, interactableId: id }),\n feedback: (intent: Omit<FeedbackIntent, \"interactableId\">) =>\n this.feedbackEmitter.emit({ ...intent, interactableId: id }),\n getWorldPose: (otherId: string) =>\n this.interactables.get(otherId)?.transform?.getWorldPose(),\n };\n }\n\n private renegotiate(id: string, registered: Registered): void {\n const fulfilment = this.capabilities.grabs === \"native\" ? \"native\" : \"poseOnly\";\n for (const slot of registered.behaviours) {\n if (slot.instance instanceof GrabBehaviour) {\n slot.instance.setFulfilment(fulfilment);\n }\n const unmet = [...unmetRequirements(this.capabilities, slot.instance.requires)];\n for (const group of slot.instance.requiresAnyOf ?? []) {\n if (group.length > 0 && unmetRequirements(this.capabilities, group).length === group.length) {\n unmet.push(group[0]!);\n }\n }\n const disabled = unmet.length > 0;\n if (disabled !== slot.disabled) {\n slot.disabled = disabled;\n slot.disabledReason = disabled ? `unmet input capabilities: ${unmet.join(\", \")}` : \"\";\n this.events.emit({\n type: disabled ? \"behaviourDisabled\" : \"behaviourEnabled\",\n interactableId: id,\n behaviourKind: slot.instance.kind,\n ...(disabled ? { reason: slot.disabledReason } : {}),\n });\n }\n }\n }\n\n private renegotiateAll(): void {\n for (const [id, registered] of this.interactables) {\n this.renegotiate(id, registered);\n }\n }\n}\n\n/** Build a runtime's interactables from a portable descriptor. */\nexport function registerDescriptor(\n runtime: InteractionRuntime,\n descriptor: { interactables: InteractableDescriptor[] },\n portsFor: (id: string) => RegisterPorts = () => ({}),\n): void {\n for (const interactable of descriptor.interactables) {\n runtime.registerInteractable(interactable, portsFor(interactable.id));\n }\n}\n"]}
1
+ {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * InteractionRuntime - the engine-free orchestrator.\n *\n * Once per host frame the runtime pulls the input provider, resolves\n * targeting (provider hints first - provider power wins - then poke\n * proximity, then ray hits), applies press/grab transitions with\n * hysteresis, runs gaze gating + dwell, ticks every behaviour, and emits\n * interaction events and feedback intents. It never touches an engine:\n * space queries and transform writes go through the adapter-implemented\n * {@link HitTester} and {@link TransformPort}.\n *\n * Lifecycle is first-class (the pale-signal gap): interactables register,\n * unregister, enable and disable; behaviours disabled by capability\n * negotiation announce themselves with a `behaviourDisabled` event.\n */\nimport {\n SELECT_PRESS_THRESHOLD,\n SELECT_RELEASE_THRESHOLD,\n unmetRequirements,\n type InputCapabilities,\n type InputHitHint,\n type InputProvider,\n type InputSourceSnapshot,\n type PoseTuple,\n type RayTuple,\n type Unsubscribe,\n} from \"@realitycollective/webxr-input\";\nimport type { Behaviour, InteractorInfo } from \"./behaviours/behaviour.js\";\nimport {\n createBehaviour,\n type BehaviourConfig,\n type BehaviourFactoryContext,\n} from \"./behaviours/factory.js\";\nimport { GrabBehaviour } from \"./behaviours/grab.js\";\nimport type { InteractableDescriptor } from \"./descriptor.js\";\nimport { Emitter, type InteractionEvent, type InteractionEventListener } from \"./events.js\";\nimport type { FeedbackIntent, FeedbackListener } from \"./feedback.js\";\nimport { DwellState, resolveDwellConfig, DWELL_DEFAULTS, type DwellConfig } from \"./gaze.js\";\nimport { vApplyQuat } from \"./math.js\";\nimport type { HitTester, TransformPort } from \"./ports.js\";\nimport { VelocityTracker, type VelocityTrackerOptions } from \"./velocity-tracker.js\";\n\nconst DEFAULT_POKE_RADIUS = 0.05;\n/** Interactor id used when gaze is synthesized from the head pose. */\nexport const HEAD_GAZE_INTERACTOR_ID = \"head-gaze\";\n\nexport interface InteractableState {\n id: string;\n enabled: boolean;\n hovered: boolean;\n gazeHovered: boolean;\n pressed: boolean;\n grabbed: boolean;\n /** Primary behaviour's logical value. */\n value: number;\n /** Gaze-dwell meter 0..1 (0 when dwell is not enabled). */\n dwell: number;\n}\n\ninterface BehaviourSlot {\n instance: Behaviour;\n config: BehaviourConfig;\n disabled: boolean;\n disabledReason: string;\n}\n\ninterface Registered {\n def: InteractableDescriptor;\n behaviours: BehaviourSlot[];\n transform: TransformPort | undefined;\n enabled: boolean;\n pokeRadius: number;\n gazeRequired: boolean;\n dwellConfig: Required<DwellConfig> | null;\n dwell: DwellState;\n gazeHovered: boolean;\n hoveredBy: Set<string>;\n pressedBy: Set<string>;\n grabbedBy: string | null;\n}\n\ninterface SourceRuntimeState {\n hoverTarget: string | null;\n pressed: boolean;\n pressTarget: string | null;\n grabbed: boolean;\n grabTarget: string | null;\n}\n\nexport interface InteractionRuntimeOptions {\n provider: InputProvider;\n hitTester?: HitTester;\n dwellDefaults?: DwellConfig;\n /**\n * Per-source velocity tracking. On by default with no smoothing; pass\n * `false` to skip it, or options to smooth. The tracker only fills in\n * what the provider did not supply.\n */\n velocity?: VelocityTrackerOptions | false;\n}\n\nexport interface RegisterPorts {\n transform?: TransformPort;\n}\n\nexport class InteractionRuntime {\n private readonly provider: InputProvider;\n private hitTester: HitTester | null;\n private readonly dwellDefaults: Required<DwellConfig>;\n\n private readonly interactables = new Map<string, Registered>();\n private readonly sourceStates = new Map<string, SourceRuntimeState>();\n private readonly events = new Emitter<InteractionEvent>();\n private readonly feedbackEmitter = new Emitter<FeedbackIntent>();\n private readonly afterSample = new Emitter<readonly InputSourceSnapshot[]>();\n private readonly velocityTracker: VelocityTracker | null;\n private lastSources = new Map<string, InputSourceSnapshot>();\n private capabilities: InputCapabilities;\n private readonly unsubscribeCaps: Unsubscribe;\n private disposed = false;\n\n constructor(options: InteractionRuntimeOptions) {\n this.provider = options.provider;\n this.hitTester = options.hitTester ?? null;\n const d = options.dwellDefaults;\n this.dwellDefaults = {\n holdSeconds: d?.holdSeconds ?? DWELL_DEFAULTS.holdSeconds,\n decayFactor: d?.decayFactor ?? DWELL_DEFAULTS.decayFactor,\n rearmBelow: d?.rearmBelow ?? DWELL_DEFAULTS.rearmBelow,\n };\n this.velocityTracker =\n options.velocity === false ? null : new VelocityTracker(options.velocity ?? {});\n this.capabilities = this.provider.getCapabilities();\n this.unsubscribeCaps = this.provider.onCapabilitiesChanged((caps) => {\n this.capabilities = caps;\n this.renegotiateAll();\n });\n }\n\n // -- registry -------------------------------------------------------------\n\n registerInteractable(def: InteractableDescriptor, ports: RegisterPorts = {}): void {\n if (this.interactables.has(def.id)) {\n throw new Error(`Interactable \"${def.id}\" is already registered`);\n }\n const context: BehaviourFactoryContext = {\n grabFulfilment: this.capabilities.grabs === \"native\" ? \"native\" : \"poseOnly\",\n };\n const registered: Registered = {\n def,\n behaviours: def.behaviours.map((config) => ({\n instance: createBehaviour(config, context),\n config,\n disabled: false,\n disabledReason: \"\",\n })),\n transform: ports.transform,\n enabled: def.enabled !== false,\n pokeRadius: def.pokeRadius ?? DEFAULT_POKE_RADIUS,\n gazeRequired: def.gaze?.required === true,\n dwellConfig: resolveDwellConfig(def.gaze?.dwell, this.dwellDefaults),\n dwell: new DwellState(),\n gazeHovered: false,\n hoveredBy: new Set(),\n pressedBy: new Set(),\n grabbedBy: null,\n };\n this.interactables.set(def.id, registered);\n this.renegotiate(def.id, registered);\n }\n\n unregisterInteractable(id: string): void {\n const registered = this.interactables.get(id);\n if (!registered) return;\n this.releaseAllHolds(id);\n this.interactables.delete(id);\n }\n\n setInteractableEnabled(id: string, enabled: boolean): void {\n const registered = this.interactables.get(id);\n if (!registered || registered.enabled === enabled) return;\n registered.enabled = enabled;\n if (!enabled) this.releaseAllHolds(id);\n this.events.emit({\n type: enabled ? \"interactableEnabled\" : \"interactableDisabled\",\n interactableId: id,\n });\n }\n\n setHitTester(hitTester: HitTester | null): void {\n this.hitTester = hitTester;\n }\n\n /** Reach a live behaviour instance for tuning (playground steppers). */\n getBehaviour<T extends Behaviour = Behaviour>(interactableId: string, kind: string): T | undefined {\n const registered = this.interactables.get(interactableId);\n return registered?.behaviours.find((b) => b.instance.kind === kind)?.instance as T | undefined;\n }\n\n getState(id: string): InteractableState | undefined {\n const r = this.interactables.get(id);\n if (!r) return undefined;\n const primary = r.behaviours.find((b) => !b.disabled)?.instance;\n return {\n id,\n enabled: r.enabled,\n hovered: r.hoveredBy.size > 0 || r.gazeHovered,\n gazeHovered: r.gazeHovered,\n pressed: r.pressedBy.size > 0,\n grabbed: r.grabbedBy !== null,\n value: primary?.getValue() ?? 0,\n dwell: r.dwell.getProgress(),\n };\n }\n\n getInteractableIds(): string[] {\n return [...this.interactables.keys()];\n }\n\n // -- subscriptions ----------------------------------------------------------\n\n onEvent(listener: InteractionEventListener): Unsubscribe {\n return this.events.subscribe(listener);\n }\n\n onFeedback(listener: FeedbackListener): Unsubscribe {\n return this.feedbackEmitter.subscribe(listener);\n }\n\n /**\n * Sampled sources for this frame - the UI Extensions pointer bridge hook.\n *\n * @deprecated Use {@link onSample}. Both deliver the same stream: since\n * `@realitycollective/webxr-input` 0.1.1 carries `linearVelocity` and\n * `angularVelocity` on `InputSourceSnapshot`, the two signatures are\n * identical and only one name is needed. This one is kept so existing\n * callers keep working and will be removed in a later major release.\n */\n onSourcesSampled(listener: (sources: readonly InputSourceSnapshot[]) => void): Unsubscribe {\n return this.afterSample.subscribe(listener);\n }\n\n /**\n * This frame's sources, velocity included.\n *\n * The snapshots are the provider's own objects for this frame and are\n * never written to again once delivered (the ownership rule on\n * `InputSourceSnapshot`), so a listener may keep one, or a tuple inside\n * it, across frames without copying.\n */\n onSample(listener: (sources: readonly InputSourceSnapshot[]) => void): Unsubscribe {\n return this.afterSample.subscribe(listener);\n }\n\n /**\n * The last sample of one source, velocity included. Undefined before the\n * first update, and once the source stops reporting.\n *\n * The snapshot is the caller's to keep: it is never mutated after the\n * frame it was sampled in, and it is not refreshed in place, so call\n * again for a newer one.\n */\n getSource(id: string): InputSourceSnapshot | undefined {\n return this.lastSources.get(id);\n }\n\n getCapabilities(): InputCapabilities {\n return this.capabilities;\n }\n\n getProvider(): InputProvider {\n return this.provider;\n }\n\n // -- frame ------------------------------------------------------------------\n\n update(dt: number): void {\n if (this.disposed) return;\n const sampled = this.provider.sample();\n const sources = this.velocityTracker ? this.velocityTracker.update(sampled, dt) : sampled;\n const hints = this.provider.sampleHints?.() ?? [];\n this.lastSources = new Map(sources.map((source) => [source.id, source]));\n this.afterSample.emit(sources);\n\n const hintBySource = new Map<string, InputHitHint[]>();\n for (const hint of hints) {\n const list = hintBySource.get(hint.sourceId);\n if (list) list.push(hint);\n else hintBySource.set(hint.sourceId, [hint]);\n }\n\n const seen = new Set<string>();\n for (const source of sources) {\n seen.add(source.id);\n this.updateSource(source, hintBySource.get(source.id) ?? []);\n }\n // Sources that vanished mid-hold release everything they held.\n for (const [id, state] of this.sourceStates) {\n if (!seen.has(id)) {\n this.forceRelease(id, state);\n this.sourceStates.delete(id);\n }\n }\n\n this.updateGaze(sources, dt);\n this.tickBehaviours(sources, dt);\n }\n\n dispose(): void {\n this.disposed = true;\n this.unsubscribeCaps();\n this.interactables.clear();\n this.sourceStates.clear();\n this.lastSources.clear();\n this.velocityTracker?.reset();\n }\n\n // -- internals ---------------------------------------------------------------\n\n private sourceState(id: string): SourceRuntimeState {\n let state = this.sourceStates.get(id);\n if (!state) {\n state = { hoverTarget: null, pressed: false, pressTarget: null, grabbed: false, grabTarget: null };\n this.sourceStates.set(id, state);\n }\n return state;\n }\n\n private interactorInfo(source: InputSourceSnapshot): InteractorInfo {\n const info: InteractorInfo = {\n id: source.id,\n kind: source.kind,\n select: source.select,\n squeeze: source.squeeze,\n };\n if (source.ray) info.ray = source.ray;\n if (source.gripPose) info.gripPose = source.gripPose;\n if (source.indexTip) info.indexTip = source.indexTip;\n return info;\n }\n\n /** Resolve this source's target: hints beat poke beats ray. */\n private resolveTarget(source: InputSourceSnapshot, hints: InputHitHint[]): string | null {\n const hint =\n hints.find((h) => h.state === \"grab\") ??\n hints.find((h) => h.state === \"press\") ??\n hints.find((h) => h.state === \"hover\");\n if (hint && this.targetable(hint.targetId)) return hint.targetId;\n if (source.indexTip && this.hitTester) {\n // One proximity query at the largest radius any interactable asked for,\n // then the nearest hit must be inside ITS OWN radius. A per-interactable\n // query would be a hit test per registration per source per frame; this\n // keeps it to one. The trade: a farther interactable with a bigger radius\n // is not found behind a nearer one with a smaller radius, because the\n // tester returns only the nearest.\n const poke = this.hitTester.hitProximity(source.indexTip, this.maxPokeRadius());\n if (poke && this.targetable(poke.interactableId)) {\n const registered = this.interactables.get(poke.interactableId)!;\n if (poke.distance <= registered.pokeRadius) return poke.interactableId;\n }\n }\n if (source.ray && this.hitTester) {\n const hit = this.hitTester.hitRay(source.ray);\n if (hit && this.targetable(hit.interactableId)) return hit.interactableId;\n }\n return null;\n }\n\n private targetable(id: string): boolean {\n const registered = this.interactables.get(id);\n return registered !== undefined && registered.enabled;\n }\n\n /** The largest poke radius among enabled interactables; the default when none is registered. */\n private maxPokeRadius(): number {\n let radius = 0;\n for (const registered of this.interactables.values()) {\n if (registered.enabled && registered.pokeRadius > radius) radius = registered.pokeRadius;\n }\n return radius > 0 ? radius : DEFAULT_POKE_RADIUS;\n }\n\n private flags(registered: Registered): { pressable: boolean; grabbable: boolean } {\n let pressable = false;\n let grabbable = false;\n for (const slot of registered.behaviours) {\n if (slot.disabled) continue;\n if (slot.instance.onPressStart) pressable = true;\n if (slot.instance.grabbable) grabbable = true;\n }\n return { pressable, grabbable };\n }\n\n private updateSource(source: InputSourceSnapshot, hints: InputHitHint[]): void {\n const state = this.sourceState(source.id);\n const target = this.resolveTarget(source, hints);\n\n // Hover transitions.\n if (state.hoverTarget !== target) {\n if (state.hoverTarget) this.setHover(state.hoverTarget, source.id, false);\n if (target) this.setHover(target, source.id, true);\n state.hoverTarget = target;\n }\n\n const registered = target ? this.interactables.get(target) : undefined;\n const roles = registered ? this.flags(registered) : { pressable: false, grabbable: false };\n\n // Signal resolution. A pinching hand (or a 2D pointer click) acts as a\n // grab when the target is grab-only; controllers keep trigger=press,\n // squeeze=grab.\n const selectAsGrab = roles.grabbable && !roles.pressable && source.kind !== \"controller\";\n const pressSignal = selectAsGrab ? 0 : source.select;\n const grabSignal = Math.max(source.squeeze, selectAsGrab ? source.select : 0);\n\n const hintPress = hints.some((h) => h.state === \"press\");\n const hintGrab = hints.some((h) => h.state === \"grab\") || source.nativeGrabbing === true;\n\n // Press with hysteresis (hints override).\n const pressActive = hintPress || (state.pressed\n ? pressSignal > SELECT_RELEASE_THRESHOLD\n : pressSignal >= SELECT_PRESS_THRESHOLD);\n if (pressActive && !state.pressed) {\n state.pressed = true;\n if (target && registered && roles.pressable && this.gazeAllows(registered)) {\n state.pressTarget = target;\n this.routePressStart(target, registered, this.interactorInfo(source));\n }\n } else if (!pressActive && state.pressed) {\n state.pressed = false;\n if (state.pressTarget) {\n this.routePressEnd(state.pressTarget, source.id, this.interactorInfo(source));\n state.pressTarget = null;\n }\n }\n\n // Grab with hysteresis (hints / native grabbing override).\n const grabActive = hintGrab || (state.grabbed\n ? grabSignal > SELECT_RELEASE_THRESHOLD\n : grabSignal >= SELECT_PRESS_THRESHOLD);\n if (grabActive && !state.grabbed) {\n state.grabbed = true;\n const hintTargetId = hints.find((h) => h.state === \"grab\")?.targetId;\n const grabTargetId = hintTargetId && this.targetable(hintTargetId) ? hintTargetId : target;\n const grabTarget = grabTargetId ? this.interactables.get(grabTargetId) : undefined;\n if (\n grabTargetId &&\n grabTarget &&\n this.flags(grabTarget).grabbable &&\n grabTarget.grabbedBy === null &&\n this.gazeAllows(grabTarget)\n ) {\n state.grabTarget = grabTargetId;\n grabTarget.grabbedBy = source.id;\n const info = this.interactorInfo(source);\n for (const slot of grabTarget.behaviours) {\n if (!slot.disabled && slot.instance.grabbable) {\n slot.instance.onGrabStart?.(this.contextFor(grabTargetId, grabTarget, 0), info);\n }\n }\n }\n } else if (!grabActive && state.grabbed) {\n state.grabbed = false;\n if (state.grabTarget) {\n this.endGrab(state.grabTarget, source.id, this.interactorInfo(source));\n state.grabTarget = null;\n }\n }\n }\n\n private gazeAllows(registered: Registered): boolean {\n return !registered.gazeRequired || registered.gazeHovered;\n }\n\n private setHover(id: string, sourceId: string, hovered: boolean): void {\n const registered = this.interactables.get(id);\n if (!registered) return;\n if (hovered) {\n registered.hoveredBy.add(sourceId);\n if (registered.hoveredBy.size === 1) {\n this.events.emit({ type: \"hoverEnter\", interactableId: id, interactorId: sourceId });\n this.feedbackEmitter.emit({\n cue: \"hover\",\n interactableId: id,\n sourceId,\n intensity: 0.1,\n durationMs: 10,\n });\n }\n } else {\n registered.hoveredBy.delete(sourceId);\n if (registered.hoveredBy.size === 0) {\n this.events.emit({ type: \"hoverExit\", interactableId: id, interactorId: sourceId });\n }\n }\n }\n\n private routePressStart(id: string, registered: Registered, info: InteractorInfo): void {\n registered.pressedBy.add(info.id);\n this.events.emit({ type: \"pressStart\", interactableId: id, interactorId: info.id });\n const ctx = this.contextFor(id, registered, 0);\n for (const slot of registered.behaviours) {\n if (!slot.disabled) slot.instance.onPressStart?.(ctx, info);\n }\n }\n\n private routePressEnd(id: string, sourceId: string, info: InteractorInfo): void {\n const registered = this.interactables.get(id);\n if (!registered) return;\n registered.pressedBy.delete(sourceId);\n this.events.emit({ type: \"pressEnd\", interactableId: id, interactorId: sourceId });\n const ctx = this.contextFor(id, registered, 0);\n for (const slot of registered.behaviours) {\n if (!slot.disabled) slot.instance.onPressEnd?.(ctx, info);\n }\n }\n\n private endGrab(id: string, sourceId: string, info: InteractorInfo): void {\n const registered = this.interactables.get(id);\n if (!registered || registered.grabbedBy !== sourceId) return;\n registered.grabbedBy = null;\n const ctx = this.contextFor(id, registered, 0);\n for (const slot of registered.behaviours) {\n if (!slot.disabled && slot.instance.grabbable) slot.instance.onGrabEnd?.(ctx, info);\n }\n }\n\n private forceRelease(sourceId: string, state: SourceRuntimeState): void {\n const info: InteractorInfo = { id: sourceId, kind: \"other\", select: 0, squeeze: 0 };\n if (state.pressTarget) this.routePressEnd(state.pressTarget, sourceId, info);\n if (state.grabTarget) this.endGrab(state.grabTarget, sourceId, info);\n if (state.hoverTarget) this.setHover(state.hoverTarget, sourceId, false);\n }\n\n private releaseAllHolds(id: string): void {\n for (const [sourceId, state] of this.sourceStates) {\n if (state.pressTarget === id) {\n this.routePressEnd(id, sourceId, { id: sourceId, kind: \"other\", select: 0, squeeze: 0 });\n state.pressTarget = null;\n state.pressed = false;\n }\n if (state.grabTarget === id) {\n this.endGrab(id, sourceId, { id: sourceId, kind: \"other\", select: 0, squeeze: 0 });\n state.grabTarget = null;\n state.grabbed = false;\n }\n if (state.hoverTarget === id) {\n this.setHover(id, sourceId, false);\n state.hoverTarget = null;\n }\n }\n }\n\n private updateGaze(sources: readonly InputSourceSnapshot[], dt: number): void {\n let gazeRay: RayTuple | null = null;\n let gazeId = HEAD_GAZE_INTERACTOR_ID;\n const gazeSource = sources.find((s) => s.kind === \"gaze\");\n if (gazeSource?.ray) {\n gazeRay = gazeSource.ray;\n gazeId = gazeSource.id;\n } else if (this.capabilities.headPose && this.provider.getHeadPose) {\n const head: PoseTuple = this.provider.getHeadPose();\n gazeRay = {\n origin: head.position,\n direction: vApplyQuat([0, 0, -1], head.quaternion),\n };\n }\n\n const gazeTarget =\n gazeRay && this.hitTester ? this.hitTester.hitRay(gazeRay)?.interactableId ?? null : null;\n\n for (const [id, registered] of this.interactables) {\n const hovered = registered.enabled && gazeTarget === id;\n if (hovered !== registered.gazeHovered) {\n registered.gazeHovered = hovered;\n this.events.emit({\n type: hovered ? \"hoverEnter\" : \"hoverExit\",\n interactableId: id,\n interactorId: gazeId,\n });\n }\n if (registered.dwellConfig && registered.enabled) {\n const tick = registered.dwell.update(hovered, dt, registered.dwellConfig);\n if (tick.changed) {\n this.events.emit({\n type: \"dwellProgress\",\n interactableId: id,\n interactorId: gazeId,\n value: tick.progress,\n });\n }\n if (tick.fired) {\n // A completed dwell is a synthesized click: press + release.\n const info: InteractorInfo = { id: gazeId, kind: \"gaze\", select: 1, squeeze: 0 };\n this.routePressStart(id, registered, info);\n this.routePressEnd(id, gazeId, { ...info, select: 0 });\n this.feedbackEmitter.emit({\n cue: \"dwellComplete\",\n interactableId: id,\n intensity: 0.6,\n durationMs: 50,\n });\n }\n }\n }\n }\n\n private tickBehaviours(sources: readonly InputSourceSnapshot[], dt: number): void {\n const bySourceId = new Map(sources.map((s) => [s.id, s]));\n for (const [id, registered] of this.interactables) {\n if (!registered.enabled) continue;\n const holderSource = registered.grabbedBy ? bySourceId.get(registered.grabbedBy) : undefined;\n const holder = holderSource ? this.interactorInfo(holderSource) : undefined;\n const ctx = this.contextFor(id, registered, dt);\n for (const slot of registered.behaviours) {\n if (slot.disabled) continue;\n slot.instance.update(ctx, slot.instance.grabbable ? holder : undefined);\n }\n }\n }\n\n private contextFor(id: string, registered: Registered, dt: number) {\n return {\n dt,\n interactableId: id,\n transform: registered.transform,\n emit: (event: Omit<InteractionEvent, \"interactableId\">) =>\n this.events.emit({ ...event, interactableId: id }),\n feedback: (intent: Omit<FeedbackIntent, \"interactableId\">) =>\n this.feedbackEmitter.emit({ ...intent, interactableId: id }),\n getWorldPose: (otherId: string) =>\n this.interactables.get(otherId)?.transform?.getWorldPose(),\n };\n }\n\n private renegotiate(id: string, registered: Registered): void {\n const fulfilment = this.capabilities.grabs === \"native\" ? \"native\" : \"poseOnly\";\n for (const slot of registered.behaviours) {\n if (slot.instance instanceof GrabBehaviour) {\n slot.instance.setFulfilment(fulfilment);\n }\n const unmet = [...unmetRequirements(this.capabilities, slot.instance.requires)];\n for (const group of slot.instance.requiresAnyOf ?? []) {\n if (group.length > 0 && unmetRequirements(this.capabilities, group).length === group.length) {\n unmet.push(group[0]!);\n }\n }\n const disabled = unmet.length > 0;\n if (disabled !== slot.disabled) {\n slot.disabled = disabled;\n slot.disabledReason = disabled ? `unmet input capabilities: ${unmet.join(\", \")}` : \"\";\n this.events.emit({\n type: disabled ? \"behaviourDisabled\" : \"behaviourEnabled\",\n interactableId: id,\n behaviourKind: slot.instance.kind,\n ...(disabled ? { reason: slot.disabledReason } : {}),\n });\n }\n }\n }\n\n private renegotiateAll(): void {\n for (const [id, registered] of this.interactables) {\n this.renegotiate(id, registered);\n }\n }\n}\n\n/** Build a runtime's interactables from a portable descriptor. */\nexport function registerDescriptor(\n runtime: InteractionRuntime,\n descriptor: { interactables: InteractableDescriptor[] },\n portsFor: (id: string) => RegisterPorts = () => ({}),\n): void {\n for (const interactable of descriptor.interactables) {\n runtime.registerInteractable(interactable, portsFor(interactable.id));\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@realitycollective/webxr-interactions",
3
- "version": "0.1.0-preview.6",
3
+ "version": "0.1.1-preview.0",
4
4
  "description": "Engine-free core of the Reality Collective Interaction Extensions: interactables, interactors, behaviours (press, hinge, dial, slide, grab, toss-scoring, pulse, gaze-dwell), capability negotiation and feedback hooks - driven through the shared @realitycollective/webxr-input contracts. Pair with an engine adapter: threejs-interactions, iwsdk-interactions or xrblocks-interactions.",
5
5
  "keywords": [
6
6
  "realitycollective",
@@ -31,7 +31,7 @@
31
31
  "typecheck": "tsc -p tsconfig.json --noEmit"
32
32
  },
33
33
  "dependencies": {
34
- "@realitycollective/webxr-input": "^0.1.1"
34
+ "@realitycollective/webxr-input": "^0.1.4"
35
35
  },
36
36
  "repository": {
37
37
  "type": "git",