@realitycollective/service-framework-three 1.0.1-preview.2 → 1.0.1-preview.4

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
@@ -6,7 +6,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
6
6
 
7
7
  ## [1.0.1]
8
8
 
9
- Packaging, tooling and documentation, plus additive runtime API: the engine-free adapter contracts move into the core, the IWSDK adapter derives capabilities from the live session and gains a session facet, and the three.js and Babylon.js packages gain runtime adapters so a three.js, Babylon or desktop app reaches the same seam. Nothing that existed was removed or changed in signature.
9
+ Packaging, tooling and documentation, plus additive runtime API: the engine-free adapter contracts move into the core, the IWSDK adapter derives capabilities from the live session and gains a session facet, and the three.js and Babylon.js packages gain runtime adapters so a three.js, Babylon or desktop app reaches the same seam. Nothing that existed was removed, and one interface changed shape: `AdapterCapabilities` gains a required `environmentBlendMode` key, so code that builds the whole object by hand - an adapter of your own, or a test fake - has one more field to supply. Code that only reads capabilities is unaffected.
10
10
 
11
11
  ### Added
12
12
 
@@ -17,11 +17,16 @@ Packaging, tooling and documentation, plus additive runtime API: the engine-free
17
17
  - `BabylonRuntimeAdapter` in `@realitycollective/service-framework-babylon`, with the structural host types it is written against: `BabylonXRExperienceLike` (`WebXRDefaultExperience.baseExperience`), `BabylonSessionManagerLike`, `BabylonXRSessionLike`, `BabylonObservableLike`, `BabylonObserverLike`, `BabylonRuntimeAdapterOptions`, `BabylonXRSessionEventType`, `BabylonXREventListener` and `BabylonWebXRState`, plus the `BABYLON_WEBXR_STATE` and `DEFAULT_REFERENCE_SPACE_TYPE` constants. The package shipped a render-loop bridge and nothing else, so a Babylon app could emit `renderTick` but could not host a service written against `RuntimeAdapter`. The adapter orchestrates the entry points Babylon already provides - the experience helper to negotiate a session, the session manager for the live `XRSession`, `runRenderLoop` for frames - and publishes them through the same interface, with the same members and the same semantics as `WebXRRuntimeAdapter`, so a consumer moving between renderers sees no difference at this seam. Given a `host` it owns the render loop, and given a `scheduler` as well it emits `renderTick` with `source: "babylon"` exactly as `BabylonRenderLoopBridge` does, so an app needs one loop owner rather than two; given no host, the app drives `emitFrame` itself. `referenceSpaceType` defaults to `"local-floor"`, and `sessionInit` supplies the session creation options per mode. Capabilities come from the core's `deriveCapabilities` and are re-derived on `onXRSessionInit`, `onXRSessionEnded` and the session's `inputsourceschange`, with the same override layer, `refreshCapabilities()` and `clearCapabilityOverrides()` the other adapters have. A session started outside the adapter - by Babylon's own enter-XR UI, for instance - is followed through `onStateChangedObservable`, so the facet is correct either way. The same adapter serves a desktop build with no headset: `session.request` returns `{ ok: false, reason: "unsupported" }` where there is no experience or the mode is not supported, and capabilities stay at the all-false defaults. `@babylonjs/core` is still neither imported nor installed: every Babylon shape is structural, and anything a version might move or drop - the session manager, the observables, `isSessionSupportedAsync` - is optional and read through a guard.
18
18
  - `FIRST_FRAME_DELTA_MS` from the Babylon.js package - the same 16 ms, now shared by `BabylonRenderLoopBridge` and `BabylonRuntimeAdapter` instead of sitting inline in the bridge.
19
19
  - Capability derivation in `IWSDKAdapter`. It reads the live session on construction and on every visibility change: `immersive` when a session exists, `handTracking` from the `hand-tracking` enabled feature or any input source carrying a hand, `planeDetection` from the `plane-detection` enabled feature, and `passthrough` when `environmentBlendMode` is present and is not `opaque`. Subscribers are notified only when a flag actually changes. `setCapabilities` becomes a manual override layer on top, dropped by the new `clearCapabilityOverrides()` or by the new `dispose()`. `refreshCapabilities()` re-derives on demand, for a host whose visibility signal cannot push. Before this, the adapter reported all-false until the app called `setCapabilities` by hand.
20
+ - `inputsourceschange` handling in `IWSDKAdapter`, closing a parity gap with the three.js and Babylon.js adapters, which both bound the event already. The IWSDK adapter re-derived only on the world's visibility signal and at session start and end, so an app that wanted `handTracking` to update when the player put the controllers down had to attach the listener itself. It now attaches one listener to the live session, moves it when a session is replaced, and drops it when the session goes or the adapter is disposed. `IWSDKSessionLike` gains optional `addEventListener` and `removeEventListener` for it, with the two new types `IWSDKSessionEventType` and `IWSDKSessionEventListener`. Both members are optional, so a world faked in a test still satisfies the type, and the adapter guards for their absence: such a host pushes nothing and `refreshCapabilities()` remains the way to nudge it. IWSDK's own world carries a real `XRSession`, so the event arrives there.
21
+ - `requiredFeatures` and `optionalFeatures` on `SessionRequestOptions`, both optional arrays of WebXR feature strings, plus the `mergeSessionInit(init, options)` helper and its `SessionInitLike` type in the core. `SessionRequestOptions` carried only `timeoutMs`, so an app that swapped from `"immersive-vr"` to `"immersive-ar"` mid-session got whatever defaults the host binding was built with, which were chosen for the mode it was leaving. Each binding now folds the request's features over its own: the host's entries come first, the request's are appended, and a feature named twice appears once. A request that names none passes the host's init through untouched, identity included. The three.js and Babylon.js bindings share `mergeSessionInit` rather than writing the same merge twice.
22
+ - `toIWSDKFeatures(options)`, `IWSDK_FEATURE_KEYS` and `IWSDKFeatureMapping` in `@realitycollective/service-framework-iwsdk`, with the structural `IWSDKXRFeatureOptionsLike`, `IWSDKFeatureFlagLike` and `IWSDKDepthSensingFlagLike` types and a `features` member on `IWSDKXROptionsLike`. IWSDK does not take WebXR feature strings: `launchXR` takes an `XROptions` whose `features` is a structured object, one key per feature. The adapter therefore maps `hand-tracking`, `anchors`, `hit-test`, `plane-detection`, `mesh-detection`, `depth-sensing`, `layers` and `unbounded` onto `handTracking`, `anchors`, `hitTest`, `planeDetection`, `meshDetection`, `depthSensing`, `layers` and `unbounded`, setting a required feature as `{ required: true }` and an optional one as `true`; a feature named in both lists comes out required. A string IWSDK has no key for - `local-floor` and `bounded-floor`, which it configures through `referenceSpace`, or `dom-overlay`, which it does not model - is dropped from the request rather than thrown, because the session is still one the host can serve. `toIWSDKFeatures` is exported so an app can read the `unmapped` list and decide for itself before asking. `launchXR` merges what it is given over the world's `xrDefaults`, so a request naming no features leaves the app's defaults alone.
23
+ - `environmentBlendMode` on `AdapterCapabilities`, typed `EnvironmentBlendMode | null` where `EnvironmentBlendMode` is the new `"opaque" | "alpha-blend" | "additive"` union, plus `environmentBlendMode: null` in `DEFAULT_CAPABILITIES`. `deriveCapabilities` reduced the session's blend mode to the boolean `passthrough` and threw the string away, which says whether the world shows through but not how. The two passthrough modes behave oppositely: `"alpha-blend"` is video passthrough, as on a Quest, and composites normally, so black stays black; `"additive"` is a see-through optical display and adds the rendered image to the light already reaching the eye, so black is fully transparent. A service that dims the world must draw brighter on an additive display, not darker, and could not tell the two apart. The key is required rather than optional, mirroring the `presence` decision in `@realitycollective/webxr-input`, so the conformance suite's capability-key check stays meaningful. A value WebXR does not define derives as `null` rather than passing through, so a consumer switching on the mode never meets a string it has no rule for. `passthrough` keeps its exact meaning, still `true` for any blend mode other than `"opaque"`, including one that derives as `null`. Every adapter reports the new key and every adapter's change detection compares it.
24
+ - `IWSDKXROptionsLike` is now exported from `@realitycollective/service-framework-iwsdk`. It was already the declared parameter type of `IWSDKWorldLike.launchXR`, so a consumer typing a world by hand could not name it.
20
25
  - An optional session facet on `RuntimeAdapter`: `session?: SessionFacet`, with `getState`, `request(mode, options)`, `end()`, `onStateChange` and `onVisibilityChange`, plus the `SessionMode`, `SessionState`, `SessionResult`, `SessionFailureReason`, `SessionVisibility` and `SessionRequestOptions` types and the `DEFAULT_SESSION_TIMEOUT_MS` constant. A request resolves with a result rather than throwing, because a host that cannot start a session is a normal runtime condition. `IWSDKAdapter` implements it over the world's `launchXR` and `exitXR`; `MockRuntimeAdapter` implements it in memory, driven by `simulateSessionStart()`, `simulateSessionEnd()` and `simulateVisibility()`.
21
26
  - `RUNTIME_ADAPTER_FACETS`, the list of optional facets an adapter can carry, and the `RuntimeAdapterFacet` type. A conformance test walks the list, so a facet added without a mock implementation fails the suite instead of being found by a consumer.
22
27
  - `renderTick` under IWSDK. `makeServiceBridgeSystem` now emits the scheduler's `renderTick` channel on every focused frame, with `source: "iwsdk"`, alongside the adapter's `onFrame` fan-out it already drove. A service written against the scheduler now runs under IWSDK exactly as it does under the three.js and Babylon.js bridges. IWSDK reports its frame delta in seconds and the scheduler's `LifecycleContext` is in milliseconds, so the bridge converts.
23
28
  - Structural contracts for the parts of an IWSDK world the adapter now reads: `IWSDKSessionLike`, `IWSDKInputSourceLike`, an optional `session` and `launchXR` / `exitXR` on `IWSDKWorldLike`, and an optional `subscribe` on `IWSDKSignalLike`. Every addition is optional, so a world carrying nothing but the visibility signal still type-checks and still works. `@iwsdk/core` remains undeclared as a dependency of any kind.
24
- - A shared runtime-adapter conformance suite, run against `MockRuntimeAdapter`, `IWSDKAdapter`, `WebXRRuntimeAdapter` and `BabylonRuntimeAdapter`. It is an in-repo test helper, not a published entry point. Test coverage now measures `@realitycollective/service-framework-iwsdk`, `@realitycollective/service-framework-three` and `@realitycollective/service-framework-babylon` as well, at the same 100% thresholds as the core and client packages.
29
+ - A shared runtime-adapter conformance suite, run against `MockRuntimeAdapter`, `IWSDKAdapter`, `WebXRRuntimeAdapter` and `BabylonRuntimeAdapter`, and published as `runtimeAdapterContractCases()` from `@realitycollective/service-framework` with the `RuntimeAdapterContractCase`, `RuntimeAdapterSubject` and `RuntimeAdapterDriver` types. It began as an in-repo vitest helper, which meant an adapter written outside this repository could not prove it conformed: the checks existed but were not shipped. Following `inputProviderContractCases()` in `@realitycollective/webxr-input`, the suite is now data rather than a runner - each case is a `{ name, run }` pair that returns silently on success and throws a plain `Error` on failure - so an adapter repository hosts the whole thing in three lines of its own runner. Some cases are asynchronous, so a runner must await what `run` returns, and each case needs a fresh subject because the session cases drive a session through its whole lifecycle. A subject whose adapter has no session facet passes the session cases without running them. Test coverage now measures `@realitycollective/service-framework-iwsdk`, `@realitycollective/service-framework-three` and `@realitycollective/service-framework-babylon` as well, at the same 100% thresholds as the core and client packages.
25
30
  - Updated documentation packs for all projects.
26
31
  - Improved release scripting and validation to improve delivery coherance.
27
32
  - `scripts/release.config.json` - names the core package and the publish order, so the release tooling is identical across every Reality Collective TypeScript repository.
package/README.md CHANGED
@@ -88,9 +88,20 @@ Omit `host` to keep the loop yourself and call `adapter.emitFrame(timestamp, del
88
88
 
89
89
  `sessionInit` supplies the `XRSessionInit` per mode - required and optional features - and is called once per request. The default sends none.
90
90
 
91
+ One request can add features of its own through `SessionRequestOptions`, which matters when an app swaps mode mid-session and the host's defaults were chosen for the mode it is leaving:
92
+
93
+ ```typescript
94
+ await adapter.session.request("immersive-ar", {
95
+ requiredFeatures: ["hit-test"],
96
+ optionalFeatures: ["plane-detection"],
97
+ });
98
+ ```
99
+
100
+ They are merged over what `sessionInit` returned rather than replacing it: host entries come first, the request's are appended, and a feature named twice appears once. A request that names none passes the hook's result through untouched. The merge is the core's `mergeSessionInit`, shared with the Babylon binding so the two cannot drift.
101
+
91
102
  ### Capabilities
92
103
 
93
- The adapter derives `immersive`, `handTracking`, `planeDetection` and `passthrough` from the live session with the core's shared `deriveCapabilities`, which is the same derivation the IWSDK adapter uses. It re-derives when the renderer raises `sessionstart` or `sessionend` and when the session raises `inputsourceschange`, and notifies subscribers only when a flag actually changes. `refreshCapabilities()` re-reads the renderer on demand, for a host that changes what it presents without raising anything.
104
+ The adapter derives `immersive`, `handTracking`, `planeDetection`, `passthrough` and `environmentBlendMode` from the live session with the core's shared `deriveCapabilities`, which is the same derivation the IWSDK adapter uses. It re-derives when the renderer raises `sessionstart` or `sessionend` and when the session raises `inputsourceschange`, and notifies subscribers only when a flag actually changes. `refreshCapabilities()` re-reads the renderer on demand, for a host that changes what it presents without raising anything.
94
105
 
95
106
  `setCapabilities({ ... })` is a manual override layer on top: an override wins for as long as it is set, survives every later derivation, and is dropped by `clearCapabilityOverrides()` or `dispose()`.
96
107
 
@@ -166,6 +166,11 @@ export declare class WebXRRuntimeAdapter implements RuntimeAdapter {
166
166
  * Negotiate the session and hand it to the renderer. Nothing here throws at
167
167
  * the caller: a headset that is absent, refused or broken is a normal runtime
168
168
  * condition, so every outcome comes back as a {@link SessionResult}.
169
+ *
170
+ * The request's own features are merged over the `sessionInit` hook's result
171
+ * by the core's `mergeSessionInit`, so they add to the app's defaults rather
172
+ * than replacing them. A request that names none passes the hook's result
173
+ * through untouched.
169
174
  */
170
175
  private openSession;
171
176
  private endSession;
@@ -14,7 +14,7 @@
14
14
  * Every host type here is structural, so this package still imports neither
15
15
  * `three` nor any WebXR type, and the adapter unit-tests headless.
16
16
  */
17
- import { DEFAULT_CAPABILITIES, DEFAULT_SESSION_TIMEOUT_MS, deriveCapabilities, } from "@realitycollective/service-framework";
17
+ import { DEFAULT_CAPABILITIES, DEFAULT_SESSION_TIMEOUT_MS, deriveCapabilities, mergeSessionInit, } from "@realitycollective/service-framework";
18
18
  import { FIRST_FRAME_DELTA_MS } from "./three-render-loop-bridge.js";
19
19
  const SESSION_VISIBILITY_VALUES = new Set([
20
20
  "visible",
@@ -293,7 +293,8 @@ export class WebXRRuntimeAdapter {
293
293
  if (next.immersive === current.immersive &&
294
294
  next.handTracking === current.handTracking &&
295
295
  next.planeDetection === current.planeDetection &&
296
- next.passthrough === current.passthrough) {
296
+ next.passthrough === current.passthrough &&
297
+ next.environmentBlendMode === current.environmentBlendMode) {
297
298
  return;
298
299
  }
299
300
  this.capabilities = next;
@@ -313,7 +314,7 @@ export class WebXRRuntimeAdapter {
313
314
  const timeout = new Promise((resolve) => {
314
315
  timer = setTimeout(() => resolve({ ok: false, reason: "timeout" }), timeoutMs);
315
316
  });
316
- const result = await Promise.race([this.openSession(system, mode), timeout]);
317
+ const result = await Promise.race([this.openSession(system, mode, options), timeout]);
317
318
  clearTimeout(timer);
318
319
  if (!result.ok) {
319
320
  this.setSessionState("none");
@@ -326,14 +327,20 @@ export class WebXRRuntimeAdapter {
326
327
  * Negotiate the session and hand it to the renderer. Nothing here throws at
327
328
  * the caller: a headset that is absent, refused or broken is a normal runtime
328
329
  * condition, so every outcome comes back as a {@link SessionResult}.
330
+ *
331
+ * The request's own features are merged over the `sessionInit` hook's result
332
+ * by the core's `mergeSessionInit`, so they add to the app's defaults rather
333
+ * than replacing them. A request that names none passes the hook's result
334
+ * through untouched.
329
335
  */
330
- async openSession(system, mode) {
336
+ async openSession(system, mode, options) {
331
337
  try {
332
338
  const supported = await system.isSessionSupported(mode);
333
339
  if (!supported) {
334
340
  return { ok: false, reason: "unsupported" };
335
341
  }
336
- const session = await system.requestSession(mode, this.sessionInit?.(mode));
342
+ const init = mergeSessionInit(this.sessionInit?.(mode), options);
343
+ const session = await system.requestSession(mode, init);
337
344
  await this.xr.setSession(session);
338
345
  // A manager that raises `sessionstart` has already attached this session;
339
346
  // one that does not is attached here, so both host styles behave alike.
@@ -1 +1 @@
1
- {"version":3,"file":"webxr-runtime-adapter.js","sourceRoot":"","sources":["../src/webxr-runtime-adapter.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * WebXR implementation of {@link RuntimeAdapter}, for a three.js app or any\n * other page that owns its own renderer.\n *\n * The IWSDK binding gets its adapter from IWSDK. Everything else - a plain\n * three.js app, a desktop build with no headset - had nothing, so a service\n * written against `RuntimeAdapter` could not be hosted there. This adapter\n * closes that gap: it orchestrates the entry points the platform already\n * provides (`navigator.xr` for session negotiation, `renderer.xr` for the\n * session the renderer presents, `setAnimationLoop` for frames) and publishes\n * them through the same seam. It renders nothing, plays nothing and owns no\n * scene state.\n *\n * Every host type here is structural, so this package still imports neither\n * `three` nor any WebXR type, and the adapter unit-tests headless.\n */\nimport {\n DEFAULT_CAPABILITIES,\n DEFAULT_SESSION_TIMEOUT_MS,\n deriveCapabilities,\n type AdapterCapabilities,\n type CapabilitiesListener,\n type CapabilitySessionLike,\n type FrameInfo,\n type FrameListener,\n type IScheduler,\n type LifecycleContext,\n type RuntimeAdapter,\n type SessionFacet,\n type SessionFailureReason,\n type SessionMode,\n type SessionRequestOptions,\n type SessionResult,\n type SessionState,\n type SessionVisibility,\n type Unsubscribe,\n} from \"@realitycollective/service-framework\";\nimport { FIRST_FRAME_DELTA_MS, type AnimationLoopHostLike } from \"./three-render-loop-bridge.js\";\n\n/** The events an `XRSession` raises that this adapter listens for. */\nexport type WebXRSessionEventType = \"end\" | \"visibilitychange\" | \"inputsourceschange\";\n\n/** The events three.js's `WebXRManager` raises that this adapter listens for. */\nexport type WebXRManagerEventType = \"sessionstart\" | \"sessionend\";\n\n/** Host event callback. The adapter reads the host, not the event object. */\nexport type WebXREventListener = (event?: unknown) => void;\n\n/**\n * The slice of an `XRSession` the adapter reads. It extends the core's\n * {@link CapabilitySessionLike}, so a live session goes straight to\n * `deriveCapabilities` with no mapping.\n */\nexport interface WebXRSessionLike extends CapabilitySessionLike {\n /** Ends the session. The `end` event is what the adapter acts on. */\n end(): Promise<void>;\n /** `\"visible\"`, `\"visible-blurred\"` or `\"hidden\"` on a live session. */\n readonly visibilityState?: string;\n addEventListener(type: WebXRSessionEventType, listener: WebXREventListener): void;\n removeEventListener(type: WebXRSessionEventType, listener: WebXREventListener): void;\n}\n\n/** The slice of `navigator.xr` the session facet negotiates through. */\nexport interface WebXRSystemLike {\n isSessionSupported(mode: string): Promise<boolean>;\n requestSession(mode: string, init?: unknown): Promise<WebXRSessionLike>;\n}\n\n/** The slice of a three.js `renderer.xr` (`WebXRManager`) the adapter drives. */\nexport interface WebXRManagerLike {\n /** Hands the renderer the session it should present. */\n setSession(session: WebXRSessionLike): Promise<void> | void;\n /** The session the renderer is presenting, or null in 2D. */\n getSession(): WebXRSessionLike | null;\n addEventListener(type: WebXRManagerEventType, listener: WebXREventListener): void;\n removeEventListener(type: WebXRManagerEventType, listener: WebXREventListener): void;\n}\n\nexport interface WebXRRuntimeAdapterOptions {\n /** `renderer.xr` - the renderer's WebXR manager. */\n readonly xr: WebXRManagerLike;\n /**\n * `navigator.xr`. Defaults to the global when omitted, and to `null` where\n * there is no global, which is what a Node test sees. A null system reports\n * every session request as `\"unsupported\"`.\n */\n readonly xrSystem?: WebXRSystemLike | null;\n /**\n * The renderer, or anything else with `setAnimationLoop`. Given one, the\n * adapter owns the loop: {@link WebXRRuntimeAdapter.start} binds it and each\n * callback becomes a frame. Omit it to drive frames yourself with\n * {@link WebXRRuntimeAdapter.emitFrame}.\n */\n readonly host?: AnimationLoopHostLike;\n /**\n * Given a scheduler, each owned frame also emits the `renderTick` channel\n * with `source: \"three\"`, exactly as `ThreeRenderLoopBridge` does, so an app\n * needs one loop owner rather than two.\n */\n readonly scheduler?: IScheduler;\n /**\n * Supplies the `XRSessionInit` for a mode - required and optional features.\n * Called once per request; the default sends no init at all.\n */\n readonly sessionInit?: (mode: SessionMode) => unknown;\n}\n\ntype SessionStateListener = (state: SessionState) => void;\ntype SessionVisibilityListener = (visibility: SessionVisibility) => void;\n\nconst SESSION_VISIBILITY_VALUES: ReadonlySet<string> = new Set([\n \"visible\",\n \"visible-blurred\",\n \"hidden\",\n]);\n\n/**\n * Map `XRSession.visibilityState` onto the facet's vocabulary. An unrecognised\n * value is reported as `\"hidden\"`, because treating an unknown state as visible\n * would keep game logic running when it should not.\n */\nfunction toSessionVisibility(value: unknown): SessionVisibility {\n if (typeof value === \"string\" && SESSION_VISIBILITY_VALUES.has(value)) {\n return value as SessionVisibility;\n }\n\n return \"hidden\";\n}\n\n/**\n * WebXR reports a blocked request through the error name: `NotAllowedError`\n * when the user or the permission prompt refused, `SecurityError` when the\n * permissions policy did. Both are a refusal rather than a fault.\n */\nfunction toFailureReason(error: unknown): SessionFailureReason {\n const name = (error as { readonly name?: unknown } | null | undefined)?.name;\n\n if (name === \"NotAllowedError\" || name === \"SecurityError\") {\n return \"denied\";\n }\n\n return \"error\";\n}\n\n/** `navigator.xr`, read defensively: there is no navigator in a Node test. */\nfunction defaultXRSystem(): WebXRSystemLike | null {\n const globalNavigator = (globalThis as { navigator?: { xr?: WebXRSystemLike } }).navigator;\n\n return globalNavigator?.xr ?? null;\n}\n\nexport class WebXRRuntimeAdapter implements RuntimeAdapter {\n private readonly frameListeners = new Set<FrameListener>();\n private readonly capabilitiesListeners = new Set<CapabilitiesListener>();\n private readonly stateListeners = new Set<SessionStateListener>();\n private readonly visibilityListeners = new Set<SessionVisibilityListener>();\n private readonly endWaiters = new Set<() => void>();\n\n private readonly xr: WebXRManagerLike;\n private readonly xrSystem: WebXRSystemLike | null;\n private readonly host: AnimationLoopHostLike | undefined;\n private readonly scheduler: IScheduler | undefined;\n private readonly sessionInit: ((mode: SessionMode) => unknown) | undefined;\n\n private derived: AdapterCapabilities = DEFAULT_CAPABILITIES;\n private overrides: Partial<AdapterCapabilities> = {};\n private capabilities: AdapterCapabilities = DEFAULT_CAPABILITIES;\n private sessionState: SessionState = \"none\";\n private boundSession: WebXRSessionLike | null = null;\n private sessionListeners: {\n readonly type: WebXRSessionEventType;\n readonly listener: WebXREventListener;\n }[] = [];\n private animationLoopBound = false;\n private frame = 0;\n private lastTimestamp = 0;\n\n private readonly onManagerSessionStart: WebXREventListener = () => {\n this.handleSessionStart();\n };\n\n private readonly onManagerSessionEnd: WebXREventListener = () => {\n this.handleSessionEnd();\n };\n\n /** Session lifecycle over `navigator.xr` and the renderer's XR manager. */\n public readonly session: SessionFacet = {\n getState: () => this.sessionState,\n request: (mode, options) => this.requestSession(mode, options),\n end: () => this.endSession(),\n onStateChange: (listener) => {\n this.stateListeners.add(listener);\n return () => {\n this.stateListeners.delete(listener);\n };\n },\n onVisibilityChange: (listener) => {\n this.visibilityListeners.add(listener);\n return () => {\n this.visibilityListeners.delete(listener);\n };\n },\n };\n\n public constructor(options: WebXRRuntimeAdapterOptions) {\n this.xr = options.xr;\n this.xrSystem = options.xrSystem === undefined ? defaultXRSystem() : options.xrSystem;\n this.host = options.host;\n this.scheduler = options.scheduler;\n this.sessionInit = options.sessionInit;\n\n this.xr.addEventListener(\"sessionstart\", this.onManagerSessionStart);\n this.xr.addEventListener(\"sessionend\", this.onManagerSessionEnd);\n\n const session = this.xr.getSession();\n\n if (session) {\n this.attachSession(session);\n this.sessionState = \"active\";\n }\n\n this.derived = deriveCapabilities(session);\n this.capabilities = this.derived;\n }\n\n public onFrame(listener: FrameListener): Unsubscribe {\n this.frameListeners.add(listener);\n return () => {\n this.frameListeners.delete(listener);\n };\n }\n\n public getCapabilities(): AdapterCapabilities {\n return this.capabilities;\n }\n\n public onCapabilitiesChange(listener: CapabilitiesListener): Unsubscribe {\n this.capabilitiesListeners.add(listener);\n return () => {\n this.capabilitiesListeners.delete(listener);\n };\n }\n\n /** The session the renderer is presenting, or null in 2D. */\n public getSession(): WebXRSessionLike | null {\n return this.boundSession;\n }\n\n /**\n * Bind the animation loop, if this adapter was given a host. Each callback\n * becomes one {@link FrameInfo} and, where a scheduler was supplied, one\n * `renderTick`. With no host this does nothing: the app owns the loop and\n * calls {@link WebXRRuntimeAdapter.emitFrame} itself.\n *\n * three.js routes `setAnimationLoop` through the session's own\n * `requestAnimationFrame` while presenting, so one call covers both the 2D\n * page and the headset.\n */\n public start(): void {\n if (this.animationLoopBound) {\n return;\n }\n\n this.animationLoopBound = true;\n this.host?.setAnimationLoop((timestamp) => this.handleAnimationFrame(timestamp));\n }\n\n /** Release the animation loop. Safe to call when it was never bound. */\n public stop(): void {\n if (!this.animationLoopBound) {\n return;\n }\n\n this.animationLoopBound = false;\n this.host?.setAnimationLoop(null);\n }\n\n /** Push one frame to every subscriber. Call this when you own the loop. */\n public emitFrame(timestamp: number, delta: number): void {\n const frame: FrameInfo = { timestamp, delta };\n this.frameListeners.forEach((listener) => listener(frame));\n }\n\n /**\n * Re-read the renderer's session and publish any capability change. The\n * adapter does this itself on every session and input-source event; call it\n * directly after the host enables a feature mid-session, or after a host that\n * raises no events changes what it presents.\n */\n public refreshCapabilities(): void {\n const session = this.xr.getSession();\n\n if (session) {\n this.attachSession(session);\n this.updateDerived(session);\n return;\n }\n\n if (this.boundSession) {\n this.handleSessionEnd();\n return;\n }\n\n this.updateDerived(null);\n }\n\n /**\n * Force capability flags regardless of what the session reports. Overrides\n * are a layer on top of the derived values: they win for as long as they are\n * set, survive every later derivation, and are dropped only by\n * {@link WebXRRuntimeAdapter.clearCapabilityOverrides} or\n * {@link WebXRRuntimeAdapter.dispose}.\n */\n public setCapabilities(capabilities: Partial<AdapterCapabilities>): void {\n this.overrides = { ...this.overrides, ...capabilities };\n this.publishCapabilities();\n }\n\n /** Drop every manual override and fall back to the derived capabilities. */\n public clearCapabilityOverrides(): void {\n this.overrides = {};\n this.publishCapabilities();\n }\n\n /**\n * Release everything the adapter holds: the animation loop, both sets of host\n * event listeners, any in-flight `end()`, the manual overrides and every\n * listener. It does not end the session - the app owns that decision.\n */\n public dispose(): void {\n this.stop();\n this.xr.removeEventListener(\"sessionstart\", this.onManagerSessionStart);\n this.xr.removeEventListener(\"sessionend\", this.onManagerSessionEnd);\n this.detachSession();\n this.settleEndWaiters();\n\n this.overrides = {};\n this.frameListeners.clear();\n this.capabilitiesListeners.clear();\n this.stateListeners.clear();\n this.visibilityListeners.clear();\n }\n\n private handleAnimationFrame(timestamp: number): void {\n const deltaMs =\n this.lastTimestamp === 0 ? FIRST_FRAME_DELTA_MS : timestamp - this.lastTimestamp;\n\n this.lastTimestamp = timestamp;\n this.frame += 1;\n\n // `FrameInfo.delta` is seconds; the scheduler's `LifecycleContext` is in\n // milliseconds, which is the unit `ThreeRenderLoopBridge` already emits.\n this.emitFrame(timestamp, deltaMs / 1000);\n\n const context: LifecycleContext = {\n timestamp,\n deltaTime: deltaMs,\n frame: this.frame,\n source: \"three\",\n };\n\n this.scheduler?.emit(\"renderTick\", context);\n }\n\n /**\n * Subscribe to one session's own events. The listeners close over the session\n * they belong to, so nothing here has to re-check which session is live.\n */\n private attachSession(session: WebXRSessionLike): void {\n if (session === this.boundSession) {\n return;\n }\n\n this.detachSession();\n this.boundSession = session;\n this.sessionListeners = [\n { type: \"end\", listener: () => this.handleSessionEnd() },\n {\n type: \"visibilitychange\",\n listener: () => this.notifyVisibility(toSessionVisibility(session.visibilityState)),\n },\n { type: \"inputsourceschange\", listener: () => this.updateDerived(session) },\n ];\n\n for (const entry of this.sessionListeners) {\n session.addEventListener(entry.type, entry.listener);\n }\n }\n\n private detachSession(): void {\n const session = this.boundSession;\n\n if (!session) {\n return;\n }\n\n for (const entry of this.sessionListeners) {\n session.removeEventListener(entry.type, entry.listener);\n }\n\n this.sessionListeners = [];\n this.boundSession = null;\n }\n\n private handleSessionStart(): void {\n const session = this.xr.getSession();\n\n if (!session) {\n return;\n }\n\n this.attachSession(session);\n this.setSessionState(\"active\");\n this.updateDerived(session);\n this.notifyVisibility(toSessionVisibility(session.visibilityState));\n }\n\n /**\n * One session ending raises both the session's `end` event and the manager's\n * `sessionend`, so this runs twice per session and is written to be\n * idempotent.\n */\n private handleSessionEnd(): void {\n if (!this.boundSession && this.sessionState === \"none\") {\n return;\n }\n\n this.detachSession();\n this.setSessionState(\"none\");\n this.updateDerived(null);\n this.notifyVisibility(\"non-immersive\");\n this.settleEndWaiters();\n }\n\n private updateDerived(session: CapabilitySessionLike | null): void {\n this.derived = deriveCapabilities(session);\n this.publishCapabilities();\n }\n\n private publishCapabilities(): void {\n const next: AdapterCapabilities = { ...this.derived, ...this.overrides };\n const current = this.capabilities;\n\n if (\n next.immersive === current.immersive &&\n next.handTracking === current.handTracking &&\n next.planeDetection === current.planeDetection &&\n next.passthrough === current.passthrough\n ) {\n return;\n }\n\n this.capabilities = next;\n this.capabilitiesListeners.forEach((listener) => listener(next));\n }\n\n private async requestSession(\n mode: SessionMode,\n options?: SessionRequestOptions,\n ): Promise<SessionResult> {\n if (this.sessionState === \"active\") {\n return { ok: true };\n }\n\n const system = this.xrSystem;\n\n if (!system) {\n return { ok: false, reason: \"unsupported\" };\n }\n\n const timeoutMs = options?.timeoutMs ?? DEFAULT_SESSION_TIMEOUT_MS;\n this.setSessionState(\"requesting\");\n\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<SessionResult>((resolve) => {\n timer = setTimeout(() => resolve({ ok: false, reason: \"timeout\" }), timeoutMs);\n });\n\n const result = await Promise.race([this.openSession(system, mode), timeout]);\n clearTimeout(timer);\n\n if (!result.ok) {\n this.setSessionState(\"none\");\n return result;\n }\n\n this.setSessionState(\"active\");\n return result;\n }\n\n /**\n * Negotiate the session and hand it to the renderer. Nothing here throws at\n * the caller: a headset that is absent, refused or broken is a normal runtime\n * condition, so every outcome comes back as a {@link SessionResult}.\n */\n private async openSession(system: WebXRSystemLike, mode: SessionMode): Promise<SessionResult> {\n try {\n const supported = await system.isSessionSupported(mode);\n\n if (!supported) {\n return { ok: false, reason: \"unsupported\" };\n }\n\n const session = await system.requestSession(mode, this.sessionInit?.(mode));\n await this.xr.setSession(session);\n\n // A manager that raises `sessionstart` has already attached this session;\n // one that does not is attached here, so both host styles behave alike.\n this.attachSession(session);\n this.updateDerived(session);\n\n return { ok: true };\n } catch (error) {\n return { ok: false, reason: toFailureReason(error), error };\n }\n }\n\n private async endSession(): Promise<void> {\n const session = this.boundSession;\n\n if (!session) {\n return;\n }\n\n this.setSessionState(\"ending\");\n\n const ended = new Promise<void>((resolve) => {\n this.endWaiters.add(resolve);\n });\n\n try {\n await session.end();\n } catch {\n // Ending is best effort. The `end` event, not this promise, is what the\n // adapter acts on, and a host that refuses still has to be reported.\n }\n\n // The event has normally arrived by now and this is a no-op. A host that\n // raises none would otherwise leave the caller waiting forever, so the\n // settled `end()` call is the fallback signal.\n this.handleSessionEnd();\n\n await ended;\n }\n\n private settleEndWaiters(): void {\n const waiters = Array.from(this.endWaiters);\n this.endWaiters.clear();\n waiters.forEach((resolve) => resolve());\n }\n\n private notifyVisibility(visibility: SessionVisibility): void {\n this.visibilityListeners.forEach((listener) => listener(visibility));\n }\n\n private setSessionState(state: SessionState): void {\n if (this.sessionState === state) {\n return;\n }\n\n this.sessionState = state;\n this.stateListeners.forEach((listener) => listener(state));\n }\n}\n"]}
1
+ {"version":3,"file":"webxr-runtime-adapter.js","sourceRoot":"","sources":["../src/webxr-runtime-adapter.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * WebXR implementation of {@link RuntimeAdapter}, for a three.js app or any\n * other page that owns its own renderer.\n *\n * The IWSDK binding gets its adapter from IWSDK. Everything else - a plain\n * three.js app, a desktop build with no headset - had nothing, so a service\n * written against `RuntimeAdapter` could not be hosted there. This adapter\n * closes that gap: it orchestrates the entry points the platform already\n * provides (`navigator.xr` for session negotiation, `renderer.xr` for the\n * session the renderer presents, `setAnimationLoop` for frames) and publishes\n * them through the same seam. It renders nothing, plays nothing and owns no\n * scene state.\n *\n * Every host type here is structural, so this package still imports neither\n * `three` nor any WebXR type, and the adapter unit-tests headless.\n */\nimport {\n DEFAULT_CAPABILITIES,\n DEFAULT_SESSION_TIMEOUT_MS,\n deriveCapabilities,\n mergeSessionInit,\n type AdapterCapabilities,\n type CapabilitiesListener,\n type CapabilitySessionLike,\n type FrameInfo,\n type FrameListener,\n type IScheduler,\n type LifecycleContext,\n type RuntimeAdapter,\n type SessionFacet,\n type SessionFailureReason,\n type SessionMode,\n type SessionRequestOptions,\n type SessionResult,\n type SessionState,\n type SessionVisibility,\n type Unsubscribe,\n} from \"@realitycollective/service-framework\";\nimport { FIRST_FRAME_DELTA_MS, type AnimationLoopHostLike } from \"./three-render-loop-bridge.js\";\n\n/** The events an `XRSession` raises that this adapter listens for. */\nexport type WebXRSessionEventType = \"end\" | \"visibilitychange\" | \"inputsourceschange\";\n\n/** The events three.js's `WebXRManager` raises that this adapter listens for. */\nexport type WebXRManagerEventType = \"sessionstart\" | \"sessionend\";\n\n/** Host event callback. The adapter reads the host, not the event object. */\nexport type WebXREventListener = (event?: unknown) => void;\n\n/**\n * The slice of an `XRSession` the adapter reads. It extends the core's\n * {@link CapabilitySessionLike}, so a live session goes straight to\n * `deriveCapabilities` with no mapping.\n */\nexport interface WebXRSessionLike extends CapabilitySessionLike {\n /** Ends the session. The `end` event is what the adapter acts on. */\n end(): Promise<void>;\n /** `\"visible\"`, `\"visible-blurred\"` or `\"hidden\"` on a live session. */\n readonly visibilityState?: string;\n addEventListener(type: WebXRSessionEventType, listener: WebXREventListener): void;\n removeEventListener(type: WebXRSessionEventType, listener: WebXREventListener): void;\n}\n\n/** The slice of `navigator.xr` the session facet negotiates through. */\nexport interface WebXRSystemLike {\n isSessionSupported(mode: string): Promise<boolean>;\n requestSession(mode: string, init?: unknown): Promise<WebXRSessionLike>;\n}\n\n/** The slice of a three.js `renderer.xr` (`WebXRManager`) the adapter drives. */\nexport interface WebXRManagerLike {\n /** Hands the renderer the session it should present. */\n setSession(session: WebXRSessionLike): Promise<void> | void;\n /** The session the renderer is presenting, or null in 2D. */\n getSession(): WebXRSessionLike | null;\n addEventListener(type: WebXRManagerEventType, listener: WebXREventListener): void;\n removeEventListener(type: WebXRManagerEventType, listener: WebXREventListener): void;\n}\n\nexport interface WebXRRuntimeAdapterOptions {\n /** `renderer.xr` - the renderer's WebXR manager. */\n readonly xr: WebXRManagerLike;\n /**\n * `navigator.xr`. Defaults to the global when omitted, and to `null` where\n * there is no global, which is what a Node test sees. A null system reports\n * every session request as `\"unsupported\"`.\n */\n readonly xrSystem?: WebXRSystemLike | null;\n /**\n * The renderer, or anything else with `setAnimationLoop`. Given one, the\n * adapter owns the loop: {@link WebXRRuntimeAdapter.start} binds it and each\n * callback becomes a frame. Omit it to drive frames yourself with\n * {@link WebXRRuntimeAdapter.emitFrame}.\n */\n readonly host?: AnimationLoopHostLike;\n /**\n * Given a scheduler, each owned frame also emits the `renderTick` channel\n * with `source: \"three\"`, exactly as `ThreeRenderLoopBridge` does, so an app\n * needs one loop owner rather than two.\n */\n readonly scheduler?: IScheduler;\n /**\n * Supplies the `XRSessionInit` for a mode - required and optional features.\n * Called once per request; the default sends no init at all.\n */\n readonly sessionInit?: (mode: SessionMode) => unknown;\n}\n\ntype SessionStateListener = (state: SessionState) => void;\ntype SessionVisibilityListener = (visibility: SessionVisibility) => void;\n\nconst SESSION_VISIBILITY_VALUES: ReadonlySet<string> = new Set([\n \"visible\",\n \"visible-blurred\",\n \"hidden\",\n]);\n\n/**\n * Map `XRSession.visibilityState` onto the facet's vocabulary. An unrecognised\n * value is reported as `\"hidden\"`, because treating an unknown state as visible\n * would keep game logic running when it should not.\n */\nfunction toSessionVisibility(value: unknown): SessionVisibility {\n if (typeof value === \"string\" && SESSION_VISIBILITY_VALUES.has(value)) {\n return value as SessionVisibility;\n }\n\n return \"hidden\";\n}\n\n/**\n * WebXR reports a blocked request through the error name: `NotAllowedError`\n * when the user or the permission prompt refused, `SecurityError` when the\n * permissions policy did. Both are a refusal rather than a fault.\n */\nfunction toFailureReason(error: unknown): SessionFailureReason {\n const name = (error as { readonly name?: unknown } | null | undefined)?.name;\n\n if (name === \"NotAllowedError\" || name === \"SecurityError\") {\n return \"denied\";\n }\n\n return \"error\";\n}\n\n/** `navigator.xr`, read defensively: there is no navigator in a Node test. */\nfunction defaultXRSystem(): WebXRSystemLike | null {\n const globalNavigator = (globalThis as { navigator?: { xr?: WebXRSystemLike } }).navigator;\n\n return globalNavigator?.xr ?? null;\n}\n\nexport class WebXRRuntimeAdapter implements RuntimeAdapter {\n private readonly frameListeners = new Set<FrameListener>();\n private readonly capabilitiesListeners = new Set<CapabilitiesListener>();\n private readonly stateListeners = new Set<SessionStateListener>();\n private readonly visibilityListeners = new Set<SessionVisibilityListener>();\n private readonly endWaiters = new Set<() => void>();\n\n private readonly xr: WebXRManagerLike;\n private readonly xrSystem: WebXRSystemLike | null;\n private readonly host: AnimationLoopHostLike | undefined;\n private readonly scheduler: IScheduler | undefined;\n private readonly sessionInit: ((mode: SessionMode) => unknown) | undefined;\n\n private derived: AdapterCapabilities = DEFAULT_CAPABILITIES;\n private overrides: Partial<AdapterCapabilities> = {};\n private capabilities: AdapterCapabilities = DEFAULT_CAPABILITIES;\n private sessionState: SessionState = \"none\";\n private boundSession: WebXRSessionLike | null = null;\n private sessionListeners: {\n readonly type: WebXRSessionEventType;\n readonly listener: WebXREventListener;\n }[] = [];\n private animationLoopBound = false;\n private frame = 0;\n private lastTimestamp = 0;\n\n private readonly onManagerSessionStart: WebXREventListener = () => {\n this.handleSessionStart();\n };\n\n private readonly onManagerSessionEnd: WebXREventListener = () => {\n this.handleSessionEnd();\n };\n\n /** Session lifecycle over `navigator.xr` and the renderer's XR manager. */\n public readonly session: SessionFacet = {\n getState: () => this.sessionState,\n request: (mode, options) => this.requestSession(mode, options),\n end: () => this.endSession(),\n onStateChange: (listener) => {\n this.stateListeners.add(listener);\n return () => {\n this.stateListeners.delete(listener);\n };\n },\n onVisibilityChange: (listener) => {\n this.visibilityListeners.add(listener);\n return () => {\n this.visibilityListeners.delete(listener);\n };\n },\n };\n\n public constructor(options: WebXRRuntimeAdapterOptions) {\n this.xr = options.xr;\n this.xrSystem = options.xrSystem === undefined ? defaultXRSystem() : options.xrSystem;\n this.host = options.host;\n this.scheduler = options.scheduler;\n this.sessionInit = options.sessionInit;\n\n this.xr.addEventListener(\"sessionstart\", this.onManagerSessionStart);\n this.xr.addEventListener(\"sessionend\", this.onManagerSessionEnd);\n\n const session = this.xr.getSession();\n\n if (session) {\n this.attachSession(session);\n this.sessionState = \"active\";\n }\n\n this.derived = deriveCapabilities(session);\n this.capabilities = this.derived;\n }\n\n public onFrame(listener: FrameListener): Unsubscribe {\n this.frameListeners.add(listener);\n return () => {\n this.frameListeners.delete(listener);\n };\n }\n\n public getCapabilities(): AdapterCapabilities {\n return this.capabilities;\n }\n\n public onCapabilitiesChange(listener: CapabilitiesListener): Unsubscribe {\n this.capabilitiesListeners.add(listener);\n return () => {\n this.capabilitiesListeners.delete(listener);\n };\n }\n\n /** The session the renderer is presenting, or null in 2D. */\n public getSession(): WebXRSessionLike | null {\n return this.boundSession;\n }\n\n /**\n * Bind the animation loop, if this adapter was given a host. Each callback\n * becomes one {@link FrameInfo} and, where a scheduler was supplied, one\n * `renderTick`. With no host this does nothing: the app owns the loop and\n * calls {@link WebXRRuntimeAdapter.emitFrame} itself.\n *\n * three.js routes `setAnimationLoop` through the session's own\n * `requestAnimationFrame` while presenting, so one call covers both the 2D\n * page and the headset.\n */\n public start(): void {\n if (this.animationLoopBound) {\n return;\n }\n\n this.animationLoopBound = true;\n this.host?.setAnimationLoop((timestamp) => this.handleAnimationFrame(timestamp));\n }\n\n /** Release the animation loop. Safe to call when it was never bound. */\n public stop(): void {\n if (!this.animationLoopBound) {\n return;\n }\n\n this.animationLoopBound = false;\n this.host?.setAnimationLoop(null);\n }\n\n /** Push one frame to every subscriber. Call this when you own the loop. */\n public emitFrame(timestamp: number, delta: number): void {\n const frame: FrameInfo = { timestamp, delta };\n this.frameListeners.forEach((listener) => listener(frame));\n }\n\n /**\n * Re-read the renderer's session and publish any capability change. The\n * adapter does this itself on every session and input-source event; call it\n * directly after the host enables a feature mid-session, or after a host that\n * raises no events changes what it presents.\n */\n public refreshCapabilities(): void {\n const session = this.xr.getSession();\n\n if (session) {\n this.attachSession(session);\n this.updateDerived(session);\n return;\n }\n\n if (this.boundSession) {\n this.handleSessionEnd();\n return;\n }\n\n this.updateDerived(null);\n }\n\n /**\n * Force capability flags regardless of what the session reports. Overrides\n * are a layer on top of the derived values: they win for as long as they are\n * set, survive every later derivation, and are dropped only by\n * {@link WebXRRuntimeAdapter.clearCapabilityOverrides} or\n * {@link WebXRRuntimeAdapter.dispose}.\n */\n public setCapabilities(capabilities: Partial<AdapterCapabilities>): void {\n this.overrides = { ...this.overrides, ...capabilities };\n this.publishCapabilities();\n }\n\n /** Drop every manual override and fall back to the derived capabilities. */\n public clearCapabilityOverrides(): void {\n this.overrides = {};\n this.publishCapabilities();\n }\n\n /**\n * Release everything the adapter holds: the animation loop, both sets of host\n * event listeners, any in-flight `end()`, the manual overrides and every\n * listener. It does not end the session - the app owns that decision.\n */\n public dispose(): void {\n this.stop();\n this.xr.removeEventListener(\"sessionstart\", this.onManagerSessionStart);\n this.xr.removeEventListener(\"sessionend\", this.onManagerSessionEnd);\n this.detachSession();\n this.settleEndWaiters();\n\n this.overrides = {};\n this.frameListeners.clear();\n this.capabilitiesListeners.clear();\n this.stateListeners.clear();\n this.visibilityListeners.clear();\n }\n\n private handleAnimationFrame(timestamp: number): void {\n const deltaMs =\n this.lastTimestamp === 0 ? FIRST_FRAME_DELTA_MS : timestamp - this.lastTimestamp;\n\n this.lastTimestamp = timestamp;\n this.frame += 1;\n\n // `FrameInfo.delta` is seconds; the scheduler's `LifecycleContext` is in\n // milliseconds, which is the unit `ThreeRenderLoopBridge` already emits.\n this.emitFrame(timestamp, deltaMs / 1000);\n\n const context: LifecycleContext = {\n timestamp,\n deltaTime: deltaMs,\n frame: this.frame,\n source: \"three\",\n };\n\n this.scheduler?.emit(\"renderTick\", context);\n }\n\n /**\n * Subscribe to one session's own events. The listeners close over the session\n * they belong to, so nothing here has to re-check which session is live.\n */\n private attachSession(session: WebXRSessionLike): void {\n if (session === this.boundSession) {\n return;\n }\n\n this.detachSession();\n this.boundSession = session;\n this.sessionListeners = [\n { type: \"end\", listener: () => this.handleSessionEnd() },\n {\n type: \"visibilitychange\",\n listener: () => this.notifyVisibility(toSessionVisibility(session.visibilityState)),\n },\n { type: \"inputsourceschange\", listener: () => this.updateDerived(session) },\n ];\n\n for (const entry of this.sessionListeners) {\n session.addEventListener(entry.type, entry.listener);\n }\n }\n\n private detachSession(): void {\n const session = this.boundSession;\n\n if (!session) {\n return;\n }\n\n for (const entry of this.sessionListeners) {\n session.removeEventListener(entry.type, entry.listener);\n }\n\n this.sessionListeners = [];\n this.boundSession = null;\n }\n\n private handleSessionStart(): void {\n const session = this.xr.getSession();\n\n if (!session) {\n return;\n }\n\n this.attachSession(session);\n this.setSessionState(\"active\");\n this.updateDerived(session);\n this.notifyVisibility(toSessionVisibility(session.visibilityState));\n }\n\n /**\n * One session ending raises both the session's `end` event and the manager's\n * `sessionend`, so this runs twice per session and is written to be\n * idempotent.\n */\n private handleSessionEnd(): void {\n if (!this.boundSession && this.sessionState === \"none\") {\n return;\n }\n\n this.detachSession();\n this.setSessionState(\"none\");\n this.updateDerived(null);\n this.notifyVisibility(\"non-immersive\");\n this.settleEndWaiters();\n }\n\n private updateDerived(session: CapabilitySessionLike | null): void {\n this.derived = deriveCapabilities(session);\n this.publishCapabilities();\n }\n\n private publishCapabilities(): void {\n const next: AdapterCapabilities = { ...this.derived, ...this.overrides };\n const current = this.capabilities;\n\n if (\n next.immersive === current.immersive &&\n next.handTracking === current.handTracking &&\n next.planeDetection === current.planeDetection &&\n next.passthrough === current.passthrough &&\n next.environmentBlendMode === current.environmentBlendMode\n ) {\n return;\n }\n\n this.capabilities = next;\n this.capabilitiesListeners.forEach((listener) => listener(next));\n }\n\n private async requestSession(\n mode: SessionMode,\n options?: SessionRequestOptions,\n ): Promise<SessionResult> {\n if (this.sessionState === \"active\") {\n return { ok: true };\n }\n\n const system = this.xrSystem;\n\n if (!system) {\n return { ok: false, reason: \"unsupported\" };\n }\n\n const timeoutMs = options?.timeoutMs ?? DEFAULT_SESSION_TIMEOUT_MS;\n this.setSessionState(\"requesting\");\n\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<SessionResult>((resolve) => {\n timer = setTimeout(() => resolve({ ok: false, reason: \"timeout\" }), timeoutMs);\n });\n\n const result = await Promise.race([this.openSession(system, mode, options), timeout]);\n clearTimeout(timer);\n\n if (!result.ok) {\n this.setSessionState(\"none\");\n return result;\n }\n\n this.setSessionState(\"active\");\n return result;\n }\n\n /**\n * Negotiate the session and hand it to the renderer. Nothing here throws at\n * the caller: a headset that is absent, refused or broken is a normal runtime\n * condition, so every outcome comes back as a {@link SessionResult}.\n *\n * The request's own features are merged over the `sessionInit` hook's result\n * by the core's `mergeSessionInit`, so they add to the app's defaults rather\n * than replacing them. A request that names none passes the hook's result\n * through untouched.\n */\n private async openSession(\n system: WebXRSystemLike,\n mode: SessionMode,\n options?: SessionRequestOptions,\n ): Promise<SessionResult> {\n try {\n const supported = await system.isSessionSupported(mode);\n\n if (!supported) {\n return { ok: false, reason: \"unsupported\" };\n }\n\n const init = mergeSessionInit(this.sessionInit?.(mode), options);\n const session = await system.requestSession(mode, init);\n await this.xr.setSession(session);\n\n // A manager that raises `sessionstart` has already attached this session;\n // one that does not is attached here, so both host styles behave alike.\n this.attachSession(session);\n this.updateDerived(session);\n\n return { ok: true };\n } catch (error) {\n return { ok: false, reason: toFailureReason(error), error };\n }\n }\n\n private async endSession(): Promise<void> {\n const session = this.boundSession;\n\n if (!session) {\n return;\n }\n\n this.setSessionState(\"ending\");\n\n const ended = new Promise<void>((resolve) => {\n this.endWaiters.add(resolve);\n });\n\n try {\n await session.end();\n } catch {\n // Ending is best effort. The `end` event, not this promise, is what the\n // adapter acts on, and a host that refuses still has to be reported.\n }\n\n // The event has normally arrived by now and this is a no-op. A host that\n // raises none would otherwise leave the caller waiting forever, so the\n // settled `end()` call is the fallback signal.\n this.handleSessionEnd();\n\n await ended;\n }\n\n private settleEndWaiters(): void {\n const waiters = Array.from(this.endWaiters);\n this.endWaiters.clear();\n waiters.forEach((resolve) => resolve());\n }\n\n private notifyVisibility(visibility: SessionVisibility): void {\n this.visibilityListeners.forEach((listener) => listener(visibility));\n }\n\n private setSessionState(state: SessionState): void {\n if (this.sessionState === state) {\n return;\n }\n\n this.sessionState = state;\n this.stateListeners.forEach((listener) => listener(state));\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@realitycollective/service-framework-three",
3
- "version": "1.0.1-preview.2",
3
+ "version": "1.0.1-preview.4",
4
4
  "description": "three.js render-loop bindings for the Reality Collective TypeScript Service Framework.",
5
5
  "author": "Reality Collective",
6
6
  "license": "MIT",
@@ -35,7 +35,7 @@
35
35
  "url": "https://github.com/realitycollective/com.realitycollective.service-framework.ts/issues"
36
36
  },
37
37
  "dependencies": {
38
- "@realitycollective/service-framework": "^1.0.1-preview.2"
38
+ "@realitycollective/service-framework": "^1.0.1-preview.4"
39
39
  },
40
40
  "scripts": {
41
41
  "build": "tsc -p tsconfig.build.json",