@realitycollective/iwsdk-environment 0.1.0-preview.0 → 0.1.0-preview.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Changelog
2
2
 
3
- Change log for the Reality Collective WebXR Environment Extensions packages. All three packages are versioned and released together; the version below is the one carried by the `v<version>` release tag.
3
+ Change log for the Reality Collective WebXR Environment Extensions packages. All four packages are versioned and released together; the version below is the one carried by the `v<version>` release tag.
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
 
@@ -12,11 +12,36 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
12
12
  - `@realitycollective/webxr-environment` - `AudioDirector`: a cue registry, a bus and master mix with mute kept separate from gain, per-cue retrigger policy (`overlap` / `restart` / `ignore`), a per-cue minimum retrigger interval, and voice tracking. The `AudioPort` is handed an absolute resolved gain and has no mixing left to do. A silent one-shot is never started; a silent loop is, so that unmuting mid-flight finds the ambience bed already running.
13
13
  - `@realitycollective/threejs-environment` - the three.js adapter. Gradient skies become an equirectangular `DataTexture` generated by a pure function and regenerated in place across a transition; `Fog` / `FogExp2` are mutated rather than rebuilt while the kind holds; the key light is positioned from the direction light travels. Audio runs over `Audio` / `PositionalAudio`, and a play that arrives before its buffer has decoded is held rather than dropped, so the first press of a session is not silent.
14
14
  - `@realitycollective/iwsdk-environment` - the Meta IWSDK adapter, driving IWSDK's own `DomeGradient`, light components, transform entities and `AudioSource` machinery rather than reaching past them to three.js. One-call `registerEnvironment(world)` setup that also registers the ECS system ticking both directors, converting IWSDK's seconds to the directors' milliseconds once. Fog is the one exception and is set on `world.scene`, since IWSDK has no fog component. Playback end is inferred by polling, because IWSDK reports only whether a source is playing; a voice that never starts is reaped with a warning rather than tracked forever.
15
+ - `@realitycollective/webxr-environment` - an **image-based lighting slot**. `IblSpec` is a gradient, an authored image or the host's own room probe, with intensity and a Y rotation, interpolated like every other slot. Without it a physically-based material had nothing to reflect: every host keeps the sky and the environment map as two separate facilities.
16
+ - `@realitycollective/webxr-environment` - **authored skies** (`SkySpec` kind `"texture"`, with intensity, rotation and blur) and an optional **horizon stop** on a gradient sky. The horizon colour exists because IWSDK's dome has always been a three-stop ramp and the adapter was deriving the middle colour; an app that puts a bright band at eye level can now say so and have both engines agree.
17
+ - `@realitycollective/webxr-environment` - **the sensing seam**. `EnvironmentPort.observe(host)` is the one inbound path: a port reports `unsupported` / `unavailable` / `pending` / `active` per feature with a sentence saying why, and reports measured lighting. `EnvironmentDirector.getSensing()` and `onSensing()` read it. A report never changes what the app asked for; it changes what the app can be told - which matters because a sensor that is doing nothing looks exactly like a sensor that is working.
18
+ - `@realitycollective/webxr-environment` - **depth occlusion**. `setOcclusion(spec)` reaches the port only while passthrough is on, and is remembered across passthrough changes. `OcclusionSpec` carries the union of what the hosts actually expose: three shader modes, a scope, softness, the WebXR session preferences (usage, data format, depth type, `matchDepthView`) and an update-rate cap. While occlusion is ACTIVE the sky is suppressed as well, because three.js stops drawing `scene.background` itself when its depth sensing runs.
19
+ - `@realitycollective/webxr-environment` - **light estimation**. `setLightEstimation(true | spec)` lets the host's measurement of the real room take over the ambient, key and ibl slots while it is measuring, and hands them straight back when it stops. The estimate arrives as the same spec types the app writes by hand, so it can be read, blended or ignored.
20
+ - `@realitycollective/webxr-environment` - **blend-mode-aware passthrough**. `setPassthrough` now takes `"opaque" | "alpha-blend" | "additive"` as well as a boolean, and `passthroughSuppresses` can differ per mode. On an additive display the rendered image is ADDED to the world, so a dark fog is invisible rather than dimming; an app can now do the opposite thing on each rather than choosing one compromise.
21
+ - `@realitycollective/webxr-environment` - **per-cue spatial attenuation** (`AudioCue.spatial`: reference distance, rolloff, maximum distance, curve) and a `ui` bus in the default set. All three hosts expose those four per source; the contract previously flattened them to one number chosen by the adapter.
22
+ - `@realitycollective/webxr-environment` - `EnvironmentPort.update(deltaMs)`, optional, called from the director's own `update`. Only sensor-backed ports need it: three.js reports depth through a renderer flag that flips some frames after the session starts, and a light estimate has to be read out of the current `XRFrame` or not at all.
23
+ - `@realitycollective/xrblocks-environment` - **a Google XR Blocks adapter**, EXPERIMENTAL, built on the three.js adapter the way `xrblocks-interactions` is built on `threejs-interactions`. Occlusion registers as a client of XR Blocks' `Depth` manager and picks its blur; light estimation reads the `Lighting` manager, which already owns the WebXR half. XR Blocks is never imported - its managers are described structurally, as in the other XR Blocks adapters in this estate - and anything configured before this adapter could ask is named in the report rather than dropped, including the warning that XR Blocks may be lighting the scene itself.
24
+ - `@realitycollective/webxr-environment` - **world sensing**, as a separate component on the same seam. `WorldSensingDirector` asks for planes, meshes and anchors, keeps one copy of each registry, and emits `added` / `updated` / `removed` by id so an app can spawn content on a new surface and drop it when the surface goes. Standing hit-test requests, anchor creation, and the host's own semantic labels passed through rather than mapped to a closed set. It reports detected geometry and creates none: a mesh's vertices are the runtime's buffers by reference, and what to draw on a table is the app's.
25
+ - `@realitycollective/threejs-environment`, `@realitycollective/iwsdk-environment`, `@realitycollective/xrblocks-environment` - world-sensing ports for all three hosts. three.js reads the `XRFrame` directly and measures a mesh once per `lastChangedTime`; IWSDK queries the entities its `SceneUnderstandingSystem` already made and casts through `EnvironmentRaycastTarget`; XR Blocks reads its `PlaneDetector` and `MeshDetector`, and reports anchors and hit testing as unsupported because it places objects for you rather than answering where a ray lands.
26
+ - `@realitycollective/webxr-environment` - `IblSpec` gained `{ kind: "estimated" }`: the reflections the host measured from the real room. It is a MARKER rather than data, because a reflection cube map is a live texture on a GPU and cannot be a `src` string without ceasing to be what it is. The adapter that measured it applies it; the others report that they cannot. Arrives on its own through `setLightEstimation({ ibl: true })`.
27
+ - `@realitycollective/webxr-environment` - **directional sound**. `AudioSpatial.cone` says how narrowly a cue points (inner and outer widths in RADIANS, like every other angle here, plus the gain outside), and `PlayOptions.facing` says which way this particular one is turned - the direction the sound travels, the same convention `KeyLightSpec.direction` uses. Both hosts want degrees and both point a source along +Z, because both are wrapping the same Web Audio panner, and each adapter converts. Without it a television, a PA horn and a person talking all played as glowing orbs of sound on every engine.
28
+ - `@realitycollective/webxr-environment` - `WorldSensingDirector.meshesLabelled`, the counterpart of `planesLabelled`. A wall arrives as a detected mesh as often as a plane, and having the helper on one side only also hid that the matching is case-insensitive.
15
29
  - `demos/playground` - a standalone three.js/WebXR playground: preset transitions, a passthrough toggle that shows suppression restoring the environment exactly, and live bus/master sliders moving voices that are already sounding. It builds its own floor and objects, and binds a DOM event to `audio.play`, because both are the app's job.
16
30
 
31
+ ### Fixed
32
+
33
+ - **A hand ray is now cast from the hand.** The three.js world-sensing port used to bind every hit test to the viewer's reference space and mention in a detail string that hands were not really supported, which meant the most common AR gesture - point at the floor, place something - silently aimed from the head. It now looks the input source up by handedness, binds to its `targetRaySpace`, waits visibly while there is no such source, and unbinds when the hand goes away.
34
+ - **`HitTestRequest.offsetRay` is no longer dropped.** It becomes an `XRRay` (injectable, defaulting to the browser's) and is passed to the runtime. When no `XRRay` is available the request still works and the report says the offset was ignored, rather than quietly aiming somewhere else.
35
+ - **`WorldHit.distance` is filled where a host can fill it.** The three.js port poses the ray's own space once per frame and measures from there. IWSDK keeps its ray to itself, so it reports `null`, and the contract now says exactly what `null` means and what it does not.
36
+ - **Creating an anchor turns anchor detection on.** `createAnchor` used to hand back an id that nothing would ever update unless the app had separately asked to detect anchors - content pinned to it would never move again. The director now enables it and reports that it did.
37
+
17
38
  ### Notes
18
39
 
19
40
  - The core has **no runtime dependencies at all**, and its architecture test asserts it: no engine, no service framework, no input contracts. No sibling package is imported, named in a type, or asserted against in a test either. Passthrough arrives through `setPassthrough(boolean)` rather than by reading a session, and a sound is played because the APP subscribed something to `play(cueId)` - if a source is present the app binds it, and if it is not, nothing here notices. Those two boundaries are what keep this package a leaf.
41
+ - The three.js adapter uses three.js's **own** depth-sensing occlusion, which has been in the renderer since r158 and draws its occlusion mesh before everything else. There is no hand-rolled depth-priming pass here: writing one would duplicate the renderer in the one place this estate cannot afford drift, GL state that three.js caches. What the adapter does instead is decide whether the request can be served - a session, the `depth-sensing` feature, gpu-optimized depth, a scope three.js can honour - and report the reason when it cannot.
42
+ - The IWSDK adapter takes an `occludables` option for `scope: "all"`, because IWSDK opts entities into occlusion one at a time and the entities are content, which is the app's. It never removes a `DepthOccludable` the app added itself. IWSDK 0.5.3 has no light estimation at all, and that is reported rather than left silent; both gaps are filed in [`docs/UPSTREAM_ENHANCEMENTS.md`](docs/UPSTREAM_ENHANCEMENTS.md) as items 6.1 and 6.2.
43
+ - World sensing is a **second director**, not more slots. Sky, fog and lighting are things the app describes and the host draws; planes and meshes are things the host measures and the app reads, and their consumers are placement, locomotion, physics and interaction rather than the eye. An app that wants only a sky pays for none of it.
44
+ - **A body solver was considered and declined.** No host exposes a body, so it fails the boundary test this repository is built on; see [`docs/BOUNDARY.md`](docs/BOUNDARY.md). Planes, meshes, anchors and hit test are real platform sensing but feed placement and interaction rather than the environment, and are recorded there as a separate component rather than as slots of `EnvironmentSpec`.
20
45
  - Every slot the environment describes is a PLATFORM facility each host exposes differently - `scene.background` and `Fog` on three.js, `DomeGradient` and `AmbientLightComponent` on IWSDK. CONTENT is the app's: no geometry is created by either adapter, and the stock presets are examples to copy rather than art direction. See [`docs/BOUNDARY.md`](docs/BOUNDARY.md) for the boundary in full, and [`docs/UPSTREAM_ENHANCEMENTS.md`](docs/UPSTREAM_ENHANCEMENTS.md) for the gaps that belong to the other packages rather than this one.
21
46
 
22
47
  [0.1.0]: https://github.com/realitycollective/WebXR-Environment/commits/main
package/README.md CHANGED
@@ -36,7 +36,12 @@ It drives IWSDK's own machinery rather than reaching past it to three.js:
36
36
 
37
37
  - **Sky** - a `DomeGradient` on the level root. IWSDK's `EnvironmentSystem` already hides authored backgrounds in an AR session, which is behaviour worth inheriting rather than fighting. A gradient's equator is derived from the same ramp the three.js adapter uses, so the two engines agree at the horizon and not only at the poles.
38
38
  - **Lights** - `AmbientLightComponent` and `DirectionalLightComponent` on transform entities. The key entity is rotated so its local `-Z` runs along the direction the light travels.
39
- - **Audio** - one entity per voice, carrying `AudioSource` with `playbackMode` pinned to `Overlap`. The core has already applied the cue's retrigger policy, and letting IWSDK apply its own on top would make `restart` mean two different things on two engines.
39
+ - **Image-based lighting** - `IBLGradient` and `IBLTexture`. `kind: "room"` is native here: IWSDK's own component takes the string `"room"` for its built-in probe, so the one kind three.js has to approximate is the one this host has for free.
40
+ - **Authored skies** - `DomeTexture`, with blurriness and rotation. The gradient dome is removed rather than left underneath, so the two never fight over the background.
41
+ - **Depth occlusion** - `DepthSensingSystem` for the system-level settings and `DepthOccludable` per entity. IWSDK opts entities in one at a time by patching their materials, so `scope: "all"` needs an `occludables` option naming which entities the real world may hide - the entities are content, and content is yours. Whatever the port adds it remembers, so turning occlusion off never strips a component you added yourself. Without that option it reports `unavailable` and says so.
42
+ - **Light estimation** - none. IWSDK 0.5.3 has no WebXR light estimation at all, and the port reports `unsupported` with that sentence rather than going quiet. Filed upstream as item 6.1. An `ibl: { kind: "estimated" }` asked for here is refused the same way, and the environment map you set yourself is left where it was.
43
+ - **The room** - hits carry no distance here, because IWSDK moves a target entity to the hit and keeps the ray to itself; guessing from the head would be a different number wearing the same name. `IWSDKWorldSensingPort` queries the entities IWSDK's `SceneUnderstandingSystem` already created rather than reaching past it to the session, so planes, meshes and anchors come with IWSDK's own measurements. A hit test becomes an entity carrying `EnvironmentRaycastTarget`, which is how IWSDK casts; creating an anchor means creating an entity with `XRAnchor`, which is how IWSDK anchors.
44
+ - **Audio** - one entity per voice, carrying `AudioSource` with `playbackMode` pinned to `Overlap`, plus the cue's own reference distance, rolloff, maximum distance, `DistanceModel` and cone. IWSDK hands the entity transform to the same three.js positional audio the other adapter uses, so a `facing` turns the entity and the cone angles are written in degrees. The core has already applied the cue's retrigger policy, and letting IWSDK apply its own on top would make `restart` mean two different things on two engines.
40
45
 
41
46
  Every one of those is an IWSDK platform component with no app-side equivalent. The adapter creates **no geometry**: a floor would be a mesh, and meshes are the app's.
42
47
 
@@ -21,7 +21,7 @@
21
21
  * A voice that never starts at all (a missing file, a decode failure) would
22
22
  * otherwise be tracked forever, so it is reaped after `startTimeoutMs`.
23
23
  */
24
- import { AudioSource, AudioUtils, PlaybackMode, Transform } from "@iwsdk/core";
24
+ import { AudioSource, AudioUtils, DistanceModel, PlaybackMode, Quaternion, Transform, Vector3, } from "@iwsdk/core";
25
25
  export class IWSDKAudioPort {
26
26
  #world;
27
27
  #parent;
@@ -46,6 +46,22 @@ export class IWSDKAudioPort {
46
46
  view[1] = request.at[1];
47
47
  view[2] = request.at[2];
48
48
  }
49
+ // IWSDK's audio system hands the entity's transform to the same three.js
50
+ // `PositionalAudio` the other adapter uses, so forward is +Z here too and
51
+ // the entity is turned to point that axis the way the sound travels.
52
+ const towards = request.facing ?? null;
53
+ if (towards !== null) {
54
+ const facing = TEMP_FACING.set(towards[0], towards[1], towards[2]);
55
+ if (facing.lengthSq() > 0) {
56
+ TEMP_ROTATION.setFromUnitVectors(AUDIO_FORWARD, facing.normalize());
57
+ const view = entity.getVectorView(Transform, "orientation");
58
+ view[0] = TEMP_ROTATION.x;
59
+ view[1] = TEMP_ROTATION.y;
60
+ view[2] = TEMP_ROTATION.z;
61
+ view[3] = TEMP_ROTATION.w;
62
+ }
63
+ }
64
+ const attenuation = request.spatial;
49
65
  entity.addComponent(AudioSource, {
50
66
  src: request.cue.src,
51
67
  volume: request.gain,
@@ -54,6 +70,25 @@ export class IWSDKAudioPort {
54
70
  autoplay: false,
55
71
  // The core already applied the cue's policy before we got here.
56
72
  playbackMode: PlaybackMode.Overlap,
73
+ // Anything the cue did not describe is left to IWSDK's own defaults,
74
+ // which is why these are spread in rather than defaulted here: this port
75
+ // has no opinion about how far a sound carries, and inventing one would
76
+ // make the same cue sound different on each engine.
77
+ ...(attenuation?.refDistance === undefined ? {} : { refDistance: attenuation.refDistance }),
78
+ ...(attenuation?.rolloffFactor === undefined
79
+ ? {}
80
+ : { rolloffFactor: attenuation.rolloffFactor }),
81
+ ...(attenuation?.maxDistance === undefined ? {} : { maxDistance: attenuation.maxDistance }),
82
+ ...(attenuation?.model === undefined ? {} : { distanceModel: distanceModel(attenuation.model) }),
83
+ // Radians in the contract, degrees on the component - IWSDK passes these
84
+ // straight to the same Web Audio panner three.js uses.
85
+ ...(attenuation?.cone === undefined
86
+ ? {}
87
+ : {
88
+ coneInnerAngle: toDegrees(attenuation.cone.inner),
89
+ coneOuterAngle: toDegrees(attenuation.cone.outer),
90
+ coneOuterGain: attenuation.cone.outsideGain,
91
+ }),
57
92
  });
58
93
  this.#voices.set(request.voiceId, { entity, request, started: false, waitedMs: 0 });
59
94
  AudioUtils.play(entity);
@@ -108,4 +143,20 @@ export class IWSDKAudioPort {
108
143
  voice.entity.destroy();
109
144
  }
110
145
  }
146
+ /** Our distance model names onto IWSDK's enum, which holds the same three. */
147
+ function distanceModel(model) {
148
+ if (model === "linear")
149
+ return DistanceModel.Linear;
150
+ if (model === "exponential")
151
+ return DistanceModel.Exponential;
152
+ return DistanceModel.Inverse;
153
+ }
154
+ /** Web Audio points a source along +Z, and IWSDK's audio system follows it. */
155
+ const AUDIO_FORWARD = new Vector3(0, 0, 1);
156
+ const TEMP_FACING = new Vector3();
157
+ const TEMP_ROTATION = new Quaternion();
158
+ /** Radians in the contract; degrees on the component. */
159
+ function toDegrees(radians) {
160
+ return (radians * 180) / Math.PI;
161
+ }
111
162
  //# sourceMappingURL=audio-port.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"audio-port.js","sourceRoot":"","sources":["../src/audio-port.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `AudioPort` for Meta IWSDK, over `AudioSource` + `AudioUtils`.\n *\n * ---------------------------------------------------------------------------\n * ONE ENTITY PER VOICE\n * ---------------------------------------------------------------------------\n * IWSDK's audio is a component on an entity, with its own pooling and its own\n * retrigger policy per source. Reusing one entity per CUE would hand those\n * decisions back to IWSDK and make `restart` and `ignore` mean something\n * different here than on three.js, which is the divergence this stack exists\n * to remove. So a voice is an entity: the director's policy wins, every voice\n * is individually stoppable, and IWSDK's own `playbackMode` is pinned to\n * `Overlap` so it never second-guesses the core.\n *\n * ---------------------------------------------------------------------------\n * KNOWING WHEN A SOUND FINISHED\n * ---------------------------------------------------------------------------\n * IWSDK reports whether a source IS playing, never that it has just stopped.\n * So this port polls in `update()` - which the director already calls - and\n * reports the end once a voice has been observed playing and then is not.\n * A voice that never starts at all (a missing file, a decode failure) would\n * otherwise be tracked forever, so it is reaped after `startTimeoutMs`.\n */\nimport { AudioSource, AudioUtils, PlaybackMode, Transform, type Entity, type World } from \"@iwsdk/core\";\nimport type { AudioPort, AudioVoiceRequest } from \"@realitycollective/webxr-environment\";\n\nexport interface IWSDKAudioPortOptions {\n /** Parent for the voice entities. Defaults to the world's default parent. */\n readonly parent?: Entity;\n /**\n * How long to wait for a voice to start before giving up on it,\n * milliseconds. Default 10 000 - generous, because a cold asset fetch on a\n * headset over hotel wifi is slower than anyone's patience.\n */\n readonly startTimeoutMs?: number;\n}\n\ninterface Voice {\n readonly entity: Entity;\n readonly request: AudioVoiceRequest;\n started: boolean;\n waitedMs: number;\n}\n\nexport class IWSDKAudioPort implements AudioPort {\n readonly #world: World;\n readonly #parent: Entity | undefined;\n readonly #startTimeoutMs: number;\n readonly #voices = new Map<number, Voice>();\n\n #disposed = false;\n\n constructor(world: World, options: IWSDKAudioPortOptions = {}) {\n this.#world = world;\n this.#parent = options.parent;\n this.#startTimeoutMs = options.startTimeoutMs ?? 10_000;\n }\n\n start(request: AudioVoiceRequest): void {\n if (this.#disposed) {\n request.ended();\n return;\n }\n const entity = this.#world.createTransformEntity(\n undefined,\n this.#parent === undefined ? undefined : { parent: this.#parent },\n );\n const positional = request.at !== null || (request.cue.positional ?? false);\n if (request.at !== null) {\n const view = entity.getVectorView(Transform, \"position\");\n view[0] = request.at[0];\n view[1] = request.at[1];\n view[2] = request.at[2];\n }\n entity.addComponent(AudioSource, {\n src: request.cue.src,\n volume: request.gain,\n loop: request.loop,\n positional,\n autoplay: false,\n // The core already applied the cue's policy before we got here.\n playbackMode: PlaybackMode.Overlap,\n });\n this.#voices.set(request.voiceId, { entity, request, started: false, waitedMs: 0 });\n AudioUtils.play(entity);\n }\n\n stop(voiceId: number): void {\n const voice = this.#voices.get(voiceId);\n if (voice === undefined) return;\n this.#voices.delete(voiceId);\n this.#retire(voice);\n }\n\n setGain(voiceId: number, gain: number): void {\n const voice = this.#voices.get(voiceId);\n if (voice === undefined) return;\n AudioUtils.setVolume(voice.entity, gain);\n }\n\n update(deltaMs: number): void {\n if (this.#disposed || this.#voices.size === 0) return;\n for (const [id, voice] of [...this.#voices]) {\n // A looping voice ends when someone stops it, never on its own.\n if (voice.request.loop) continue;\n const playing = AudioUtils.isPlaying(voice.entity);\n if (playing) {\n voice.started = true;\n continue;\n }\n if (!voice.started) {\n voice.waitedMs += deltaMs;\n if (voice.waitedMs < this.#startTimeoutMs) continue;\n console.warn(\n `[iwsdk-environment] cue \"${voice.request.cue.id}\" never started (${voice.request.cue.src})`,\n );\n }\n this.#voices.delete(id);\n this.#retire(voice);\n voice.request.ended();\n }\n }\n\n dispose(): void {\n if (this.#disposed) return;\n this.#disposed = true;\n for (const [id, voice] of [...this.#voices]) {\n this.#voices.delete(id);\n this.#retire(voice);\n }\n }\n\n #retire(voice: Voice): void {\n AudioUtils.stop(voice.entity);\n voice.entity.destroy();\n }\n}\n"]}
1
+ {"version":3,"file":"audio-port.js","sourceRoot":"","sources":["../src/audio-port.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,EACL,WAAW,EACX,UAAU,EACV,aAAa,EACb,YAAY,EACZ,UAAU,EACV,SAAS,EACT,OAAO,GAGR,MAAM,aAAa,CAAC;AAqBrB,MAAM,OAAO,cAAc;IAChB,MAAM,CAAQ;IACd,OAAO,CAAqB;IAC5B,eAAe,CAAS;IACxB,OAAO,GAAG,IAAI,GAAG,EAAiB,CAAC;IAE5C,SAAS,GAAG,KAAK,CAAC;IAElB,YAAY,KAAY,EAAE,UAAiC,EAAE;QAC3D,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;QACpB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC;QAC9B,IAAI,CAAC,eAAe,GAAG,OAAO,CAAC,cAAc,IAAI,MAAM,CAAC;IAC1D,CAAC;IAED,KAAK,CAAC,OAA0B;QAC9B,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACnB,OAAO,CAAC,KAAK,EAAE,CAAC;YAChB,OAAO;QACT,CAAC;QACD,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,qBAAqB,CAC9C,SAAS,EACT,IAAI,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,OAAO,EAAE,CAClE,CAAC;QACF,MAAM,UAAU,GAAG,OAAO,CAAC,EAAE,KAAK,IAAI,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,KAAK,CAAC,CAAC;QAC5E,IAAI,OAAO,CAAC,EAAE,KAAK,IAAI,EAAE,CAAC;YACxB,MAAM,IAAI,GAAG,MAAM,CAAC,aAAa,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;YACzD,IAAI,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;YACxB,IAAI,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;YACxB,IAAI,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAC1B,CAAC;QACD,yEAAyE;QACzE,0EAA0E;QAC1E,qEAAqE;QACrE,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,IAAI,IAAI,CAAC;QACvC,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;YACrB,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;YACnE,IAAI,MAAM,CAAC,QAAQ,EAAE,GAAG,CAAC,EAAE,CAAC;gBAC1B,aAAa,CAAC,kBAAkB,CAAC,aAAa,EAAE,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC;gBACpE,MAAM,IAAI,GAAG,MAAM,CAAC,aAAa,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC;gBAC5D,IAAI,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC;gBAC1B,IAAI,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC;gBAC1B,IAAI,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC;gBAC1B,IAAI,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC;YAC5B,CAAC;QACH,CAAC;QACD,MAAM,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC;QACpC,MAAM,CAAC,YAAY,CAAC,WAAW,EAAE;YAC/B,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,GAAG;YACpB,MAAM,EAAE,OAAO,CAAC,IAAI;YACpB,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,UAAU;YACV,QAAQ,EAAE,KAAK;YACf,gEAAgE;YAChE,YAAY,EAAE,YAAY,CAAC,OAAO;YAClC,qEAAqE;YACrE,yEAAyE;YACzE,wEAAwE;YACxE,oDAAoD;YACpD,GAAG,CAAC,WAAW,EAAE,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,WAAW,CAAC,WAAW,EAAE,CAAC;YAC3F,GAAG,CAAC,WAAW,EAAE,aAAa,KAAK,SAAS;gBAC1C,CAAC,CAAC,EAAE;gBACJ,CAAC,CAAC,EAAE,aAAa,EAAE,WAAW,CAAC,aAAa,EAAE,CAAC;YACjD,GAAG,CAAC,WAAW,EAAE,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,WAAW,CAAC,WAAW,EAAE,CAAC;YAC3F,GAAG,CAAC,WAAW,EAAE,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,aAAa,CAAC,WAAW,CAAC,KAAK,CAAC,EAAE,CAAC;YAChG,yEAAyE;YACzE,uDAAuD;YACvD,GAAG,CAAC,WAAW,EAAE,IAAI,KAAK,SAAS;gBACjC,CAAC,CAAC,EAAE;gBACJ,CAAC,CAAC;oBACE,cAAc,EAAE,SAAS,CAAC,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC;oBACjD,cAAc,EAAE,SAAS,CAAC,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC;oBACjD,aAAa,EAAE,WAAW,CAAC,IAAI,CAAC,WAAW;iBAC5C,CAAC;SACP,CAAC,CAAC;QACH,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC,CAAC;QACpF,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC1B,CAAC;IAED,IAAI,CAAC,OAAe;QAClB,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACxC,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO;QAChC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAC7B,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IACtB,CAAC;IAED,OAAO,CAAC,OAAe,EAAE,IAAY;QACnC,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACxC,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO;QAChC,UAAU,CAAC,SAAS,CAAC,KAAK,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAC3C,CAAC;IAED,MAAM,CAAC,OAAe;QACpB,IAAI,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC;YAAE,OAAO;QACtD,KAAK,MAAM,CAAC,EAAE,EAAE,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;YAC5C,gEAAgE;YAChE,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI;gBAAE,SAAS;YACjC,MAAM,OAAO,GAAG,UAAU,CAAC,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;YACnD,IAAI,OAAO,EAAE,CAAC;gBACZ,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC;gBACrB,SAAS;YACX,CAAC;YACD,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC;gBACnB,KAAK,CAAC,QAAQ,IAAI,OAAO,CAAC;gBAC1B,IAAI,KAAK,CAAC,QAAQ,GAAG,IAAI,CAAC,eAAe;oBAAE,SAAS;gBACpD,OAAO,CAAC,IAAI,CACV,4BAA4B,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,oBAAoB,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG,CAC7F,CAAC;YACJ,CAAC;YACD,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YACxB,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;YACpB,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QACxB,CAAC;IACH,CAAC;IAED,OAAO;QACL,IAAI,IAAI,CAAC,SAAS;YAAE,OAAO;QAC3B,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;QACtB,KAAK,MAAM,CAAC,EAAE,EAAE,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;YAC5C,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YACxB,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QACtB,CAAC;IACH,CAAC;IAED,OAAO,CAAC,KAAY;QAClB,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QAC9B,KAAK,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;IACzB,CAAC;CACF;AAED,8EAA8E;AAC9E,SAAS,aAAa,CAAC,KAA2C;IAChE,IAAI,KAAK,KAAK,QAAQ;QAAE,OAAO,aAAa,CAAC,MAAM,CAAC;IACpD,IAAI,KAAK,KAAK,aAAa;QAAE,OAAO,aAAa,CAAC,WAAW,CAAC;IAC9D,OAAO,aAAa,CAAC,OAAO,CAAC;AAC/B,CAAC;AAED,+EAA+E;AAC/E,MAAM,aAAa,GAAG,IAAI,OAAO,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;AAC3C,MAAM,WAAW,GAAG,IAAI,OAAO,EAAE,CAAC;AAClC,MAAM,aAAa,GAAG,IAAI,UAAU,EAAE,CAAC;AAEvC,yDAAyD;AACzD,SAAS,SAAS,CAAC,OAAe;IAChC,OAAO,CAAC,OAAO,GAAG,GAAG,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC;AACnC,CAAC","sourcesContent":["/**\n * `AudioPort` for Meta IWSDK, over `AudioSource` + `AudioUtils`.\n *\n * ---------------------------------------------------------------------------\n * ONE ENTITY PER VOICE\n * ---------------------------------------------------------------------------\n * IWSDK's audio is a component on an entity, with its own pooling and its own\n * retrigger policy per source. Reusing one entity per CUE would hand those\n * decisions back to IWSDK and make `restart` and `ignore` mean something\n * different here than on three.js, which is the divergence this stack exists\n * to remove. So a voice is an entity: the director's policy wins, every voice\n * is individually stoppable, and IWSDK's own `playbackMode` is pinned to\n * `Overlap` so it never second-guesses the core.\n *\n * ---------------------------------------------------------------------------\n * KNOWING WHEN A SOUND FINISHED\n * ---------------------------------------------------------------------------\n * IWSDK reports whether a source IS playing, never that it has just stopped.\n * So this port polls in `update()` - which the director already calls - and\n * reports the end once a voice has been observed playing and then is not.\n * A voice that never starts at all (a missing file, a decode failure) would\n * otherwise be tracked forever, so it is reaped after `startTimeoutMs`.\n */\nimport {\n AudioSource,\n AudioUtils,\n DistanceModel,\n PlaybackMode,\n Quaternion,\n Transform,\n Vector3,\n type Entity,\n type World,\n} from \"@iwsdk/core\";\nimport type { AudioPort, AudioVoiceRequest } from \"@realitycollective/webxr-environment\";\n\nexport interface IWSDKAudioPortOptions {\n /** Parent for the voice entities. Defaults to the world's default parent. */\n readonly parent?: Entity;\n /**\n * How long to wait for a voice to start before giving up on it,\n * milliseconds. Default 10 000 - generous, because a cold asset fetch on a\n * headset over hotel wifi is slower than anyone's patience.\n */\n readonly startTimeoutMs?: number;\n}\n\ninterface Voice {\n readonly entity: Entity;\n readonly request: AudioVoiceRequest;\n started: boolean;\n waitedMs: number;\n}\n\nexport class IWSDKAudioPort implements AudioPort {\n readonly #world: World;\n readonly #parent: Entity | undefined;\n readonly #startTimeoutMs: number;\n readonly #voices = new Map<number, Voice>();\n\n #disposed = false;\n\n constructor(world: World, options: IWSDKAudioPortOptions = {}) {\n this.#world = world;\n this.#parent = options.parent;\n this.#startTimeoutMs = options.startTimeoutMs ?? 10_000;\n }\n\n start(request: AudioVoiceRequest): void {\n if (this.#disposed) {\n request.ended();\n return;\n }\n const entity = this.#world.createTransformEntity(\n undefined,\n this.#parent === undefined ? undefined : { parent: this.#parent },\n );\n const positional = request.at !== null || (request.cue.positional ?? false);\n if (request.at !== null) {\n const view = entity.getVectorView(Transform, \"position\");\n view[0] = request.at[0];\n view[1] = request.at[1];\n view[2] = request.at[2];\n }\n // IWSDK's audio system hands the entity's transform to the same three.js\n // `PositionalAudio` the other adapter uses, so forward is +Z here too and\n // the entity is turned to point that axis the way the sound travels.\n const towards = request.facing ?? null;\n if (towards !== null) {\n const facing = TEMP_FACING.set(towards[0], towards[1], towards[2]);\n if (facing.lengthSq() > 0) {\n TEMP_ROTATION.setFromUnitVectors(AUDIO_FORWARD, facing.normalize());\n const view = entity.getVectorView(Transform, \"orientation\");\n view[0] = TEMP_ROTATION.x;\n view[1] = TEMP_ROTATION.y;\n view[2] = TEMP_ROTATION.z;\n view[3] = TEMP_ROTATION.w;\n }\n }\n const attenuation = request.spatial;\n entity.addComponent(AudioSource, {\n src: request.cue.src,\n volume: request.gain,\n loop: request.loop,\n positional,\n autoplay: false,\n // The core already applied the cue's policy before we got here.\n playbackMode: PlaybackMode.Overlap,\n // Anything the cue did not describe is left to IWSDK's own defaults,\n // which is why these are spread in rather than defaulted here: this port\n // has no opinion about how far a sound carries, and inventing one would\n // make the same cue sound different on each engine.\n ...(attenuation?.refDistance === undefined ? {} : { refDistance: attenuation.refDistance }),\n ...(attenuation?.rolloffFactor === undefined\n ? {}\n : { rolloffFactor: attenuation.rolloffFactor }),\n ...(attenuation?.maxDistance === undefined ? {} : { maxDistance: attenuation.maxDistance }),\n ...(attenuation?.model === undefined ? {} : { distanceModel: distanceModel(attenuation.model) }),\n // Radians in the contract, degrees on the component - IWSDK passes these\n // straight to the same Web Audio panner three.js uses.\n ...(attenuation?.cone === undefined\n ? {}\n : {\n coneInnerAngle: toDegrees(attenuation.cone.inner),\n coneOuterAngle: toDegrees(attenuation.cone.outer),\n coneOuterGain: attenuation.cone.outsideGain,\n }),\n });\n this.#voices.set(request.voiceId, { entity, request, started: false, waitedMs: 0 });\n AudioUtils.play(entity);\n }\n\n stop(voiceId: number): void {\n const voice = this.#voices.get(voiceId);\n if (voice === undefined) return;\n this.#voices.delete(voiceId);\n this.#retire(voice);\n }\n\n setGain(voiceId: number, gain: number): void {\n const voice = this.#voices.get(voiceId);\n if (voice === undefined) return;\n AudioUtils.setVolume(voice.entity, gain);\n }\n\n update(deltaMs: number): void {\n if (this.#disposed || this.#voices.size === 0) return;\n for (const [id, voice] of [...this.#voices]) {\n // A looping voice ends when someone stops it, never on its own.\n if (voice.request.loop) continue;\n const playing = AudioUtils.isPlaying(voice.entity);\n if (playing) {\n voice.started = true;\n continue;\n }\n if (!voice.started) {\n voice.waitedMs += deltaMs;\n if (voice.waitedMs < this.#startTimeoutMs) continue;\n console.warn(\n `[iwsdk-environment] cue \"${voice.request.cue.id}\" never started (${voice.request.cue.src})`,\n );\n }\n this.#voices.delete(id);\n this.#retire(voice);\n voice.request.ended();\n }\n }\n\n dispose(): void {\n if (this.#disposed) return;\n this.#disposed = true;\n for (const [id, voice] of [...this.#voices]) {\n this.#voices.delete(id);\n this.#retire(voice);\n }\n }\n\n #retire(voice: Voice): void {\n AudioUtils.stop(voice.entity);\n voice.entity.destroy();\n }\n}\n\n/** Our distance model names onto IWSDK's enum, which holds the same three. */\nfunction distanceModel(model: \"linear\" | \"inverse\" | \"exponential\"): string {\n if (model === \"linear\") return DistanceModel.Linear;\n if (model === \"exponential\") return DistanceModel.Exponential;\n return DistanceModel.Inverse;\n}\n\n/** Web Audio points a source along +Z, and IWSDK's audio system follows it. */\nconst AUDIO_FORWARD = new Vector3(0, 0, 1);\nconst TEMP_FACING = new Vector3();\nconst TEMP_ROTATION = new Quaternion();\n\n/** Radians in the contract; degrees on the component. */\nfunction toDegrees(radians: number): number {\n return (radians * 180) / Math.PI;\n}\n"]}
@@ -29,7 +29,7 @@
29
29
  * through `writeColour` for that reason - it is not a style choice.
30
30
  */
31
31
  import { type Entity, type World } from "@iwsdk/core";
32
- import type { AmbientLightSpec, EnvironmentPort, FogSpec, KeyLightSpec, SkySpec } from "@realitycollective/webxr-environment";
32
+ import type { AmbientLightSpec, EnvironmentPort, EnvironmentPortHost, FogSpec, IblSpec, KeyLightSpec, OcclusionSpec, ResolvedLightEstimation, SkySpec } from "@realitycollective/webxr-environment";
33
33
  export interface IWSDKEnvironmentPortOptions {
34
34
  /**
35
35
  * Where the light entities are parented. Defaults to the active level root,
@@ -37,13 +37,54 @@ export interface IWSDKEnvironmentPortOptions {
37
37
  * with the level.
38
38
  */
39
39
  readonly parent?: Entity;
40
- /** Dome brightness multiplier. Default 1. */
40
+ /** Dome brightness multiplier, used when a sky does not name its own. Default 1. */
41
41
  readonly skyIntensity?: number;
42
+ /**
43
+ * Which entities the real world is allowed to hide, for `scope: "all"`.
44
+ *
45
+ * IWSDK opts entities INTO occlusion one at a time, by patching the
46
+ * materials under each one, so somebody has to say which entities those
47
+ * are. It cannot be this port: the entities are content, content is the
48
+ * app's, and walking a scene graph looking for meshes would be this package
49
+ * guessing at the app's structure.
50
+ *
51
+ * With no list, `scope: "all"` reports `unavailable` and says why. An app
52
+ * that would rather tag its own entities uses `scope: "tagged"` and adds
53
+ * `DepthOccludable` itself; then this port only configures the system.
54
+ */
55
+ readonly occludables?: () => Iterable<Entity>;
42
56
  }
43
57
  export declare class IWSDKEnvironmentPort implements EnvironmentPort {
44
58
  #private;
45
59
  constructor(world: World, options?: IWSDKEnvironmentPortOptions);
60
+ observe(host: EnvironmentPortHost): () => void;
46
61
  applySky(sky: SkySpec | null): void;
62
+ /**
63
+ * The environment map, as IWSDK's own image-based lighting components.
64
+ *
65
+ * `room` is not an approximation here: IWSDK's `IBLTexture` takes the string
66
+ * `"room"` for its built-in probe, so the one kind three.js has to fake is
67
+ * the one kind this host has natively.
68
+ */
69
+ applyIbl(ibl: IblSpec | null): void;
70
+ /**
71
+ * Depth occlusion, through IWSDK's `DepthSensingSystem` and the per-entity
72
+ * `DepthOccludable` component.
73
+ *
74
+ * IWSDK patches the materials under each opted-in entity, which is why the
75
+ * scope matters more here than on three.js: there is no global mode to turn
76
+ * on, and an entity nobody opted in is an entity the real world will not
77
+ * hide. Whatever this port adds, it remembers, so turning occlusion off
78
+ * never strips a component the app put there itself.
79
+ */
80
+ applyOcclusion(spec: OcclusionSpec | null): void;
81
+ /**
82
+ * IWSDK 0.5.3 has no light estimation at all - no probe, no session hook,
83
+ * nothing in its typings. This says so instead of leaving the app to wonder
84
+ * why the room never lights anything, and the request is recorded in
85
+ * docs/UPSTREAM_ENHANCEMENTS.md.
86
+ */
87
+ applyLightEstimation(estimation: ResolvedLightEstimation | null): void;
47
88
  applyFog(fog: FogSpec | null): void;
48
89
  applyAmbient(light: AmbientLightSpec | null): void;
49
90
  applyKeyLight(light: KeyLightSpec | null): void;
@@ -28,8 +28,10 @@
28
28
  * `getVectorView`; `setValue` on one throws. Every colour write here goes
29
29
  * through `writeColour` for that reason - it is not a style choice.
30
30
  */
31
- import { AmbientLightComponent, DirectionalLightComponent, DomeGradient, Fog, FogExp2, Quaternion, Transform, Vector3, } from "@iwsdk/core";
31
+ import { AmbientLightComponent, DepthOccludable, DepthSensingSystem, DirectionalLightComponent, DomeGradient, DomeTexture, Fog, FogExp2, IBLGradient, IBLTexture, OcclusionShadersMode, Quaternion, Transform, Vector3, } from "@iwsdk/core";
32
32
  import { clamp01, lerpRgb } from "@realitycollective/webxr-environment";
33
+ /** 0..1 softness onto IWSDK's blur radius in pixels, whose default is 20. */
34
+ const MAX_BLUR_RADIUS = 40;
33
35
  /** Local -Z is where an IWSDK directional light points. */
34
36
  const LIGHT_FORWARD = new Vector3(0, 0, -1);
35
37
  const TEMP_DIRECTION = new Vector3();
@@ -38,14 +40,26 @@ export class IWSDKEnvironmentPort {
38
40
  #world;
39
41
  #parent;
40
42
  #skyIntensity;
43
+ #occludables;
41
44
  #domeEntity = null;
45
+ #iblEntity = null;
42
46
  #ambientEntity = null;
43
47
  #keyEntity = null;
44
48
  #fogKind = null;
49
+ #host = null;
50
+ /** Only what THIS port added, so it never removes the app's own component. */
51
+ #occluding = [];
45
52
  constructor(world, options = {}) {
46
53
  this.#world = world;
47
54
  this.#parent = options.parent;
48
55
  this.#skyIntensity = options.skyIntensity ?? 1;
56
+ this.#occludables = options.occludables;
57
+ }
58
+ observe(host) {
59
+ this.#host = host;
60
+ return () => {
61
+ this.#host = null;
62
+ };
49
63
  }
50
64
  applySky(sky) {
51
65
  const root = this.#levelRoot();
@@ -58,29 +72,191 @@ export class IWSDKEnvironmentPort {
58
72
  this.#domeEntity = null;
59
73
  return;
60
74
  }
75
+ if (sky.kind === "texture") {
76
+ // An authored sky is a different IWSDK component, so the dome one goes
77
+ // rather than sitting underneath and fighting over the same background.
78
+ if (root.hasComponent(DomeGradient))
79
+ root.removeComponent(DomeGradient);
80
+ if (!root.hasComponent(DomeTexture))
81
+ root.addComponent(DomeTexture);
82
+ this.#domeEntity = root;
83
+ root.setValue(DomeTexture, "src", sky.src);
84
+ root.setValue(DomeTexture, "intensity", sky.intensity ?? this.#skyIntensity);
85
+ root.setValue(DomeTexture, "blurriness", sky.blur ?? 0);
86
+ writeVec3(root.getVectorView(DomeTexture, "rotation"), 0, sky.rotationY ?? 0, 0);
87
+ root.setValue(DomeTexture, "_needsUpdate", true);
88
+ return;
89
+ }
90
+ if (root.hasComponent(DomeTexture))
91
+ root.removeComponent(DomeTexture);
61
92
  if (this.#domeEntity !== root || !root.hasComponent(DomeGradient)) {
62
93
  if (!root.hasComponent(DomeGradient))
63
94
  root.addComponent(DomeGradient);
64
95
  this.#domeEntity = root;
65
96
  }
66
97
  // IWSDK's dome is a Unity-style sky / equator / ground triple. A solid sky
67
- // is all three the same; a gradient reads its equator from the same ramp
68
- // the three.js adapter uses, so the two engines agree at the horizon
98
+ // is all three the same. A gradient uses the horizon colour the app gave,
99
+ // and derives one from the ramp when it did not - which is the same colour
100
+ // the three.js adapter puts at the horizon, so the two engines agree there
69
101
  // rather than only at the poles.
70
102
  if (sky.kind === "solid") {
71
103
  writeRgb(root.getVectorView(DomeGradient, "sky"), sky.colour);
72
104
  writeRgb(root.getVectorView(DomeGradient, "equator"), sky.colour);
73
105
  writeRgb(root.getVectorView(DomeGradient, "ground"), sky.colour);
106
+ root.setValue(DomeGradient, "intensity", this.#skyIntensity);
74
107
  }
75
108
  else {
76
- const equator = lerpRgb(sky.bottom, sky.top, Math.pow(0.5, sky.exponent ?? 1));
77
109
  writeRgb(root.getVectorView(DomeGradient, "sky"), sky.top);
78
- writeRgb(root.getVectorView(DomeGradient, "equator"), equator);
110
+ writeRgb(root.getVectorView(DomeGradient, "equator"), equatorOf(sky));
79
111
  writeRgb(root.getVectorView(DomeGradient, "ground"), sky.bottom);
112
+ root.setValue(DomeGradient, "intensity", sky.intensity ?? this.#skyIntensity);
80
113
  }
81
- root.setValue(DomeGradient, "intensity", this.#skyIntensity);
82
114
  root.setValue(DomeGradient, "_needsUpdate", true);
83
115
  }
116
+ /**
117
+ * The environment map, as IWSDK's own image-based lighting components.
118
+ *
119
+ * `room` is not an approximation here: IWSDK's `IBLTexture` takes the string
120
+ * `"room"` for its built-in probe, so the one kind three.js has to fake is
121
+ * the one kind this host has natively.
122
+ */
123
+ applyIbl(ibl) {
124
+ const root = this.#levelRoot();
125
+ if (root === null)
126
+ return;
127
+ if (ibl === null) {
128
+ if (this.#iblEntity !== null) {
129
+ if (this.#iblEntity.hasComponent(IBLGradient))
130
+ this.#iblEntity.removeComponent(IBLGradient);
131
+ if (this.#iblEntity.hasComponent(IBLTexture))
132
+ this.#iblEntity.removeComponent(IBLTexture);
133
+ }
134
+ this.#iblEntity = null;
135
+ return;
136
+ }
137
+ this.#iblEntity = root;
138
+ if (ibl.kind === "estimated") {
139
+ // IWSDK measures no reflections (UPSTREAM_ENHANCEMENTS 6.1), so the slot
140
+ // keeps whatever it had rather than being cleared: an app that asked for
141
+ // measured reflections and got none should not also lose its own map.
142
+ this.#report({
143
+ feature: "lightEstimation",
144
+ state: "unsupported",
145
+ detail: "IWSDK measures no reflections, so an estimated environment map cannot be applied",
146
+ });
147
+ return;
148
+ }
149
+ if (ibl.kind === "gradient") {
150
+ if (root.hasComponent(IBLTexture))
151
+ root.removeComponent(IBLTexture);
152
+ if (!root.hasComponent(IBLGradient))
153
+ root.addComponent(IBLGradient);
154
+ writeRgb(root.getVectorView(IBLGradient, "sky"), ibl.top);
155
+ writeRgb(root.getVectorView(IBLGradient, "equator"), equatorOf(ibl));
156
+ writeRgb(root.getVectorView(IBLGradient, "ground"), ibl.bottom);
157
+ root.setValue(IBLGradient, "intensity", ibl.intensity ?? 1);
158
+ root.setValue(IBLGradient, "_needsUpdate", true);
159
+ return;
160
+ }
161
+ if (root.hasComponent(IBLGradient))
162
+ root.removeComponent(IBLGradient);
163
+ if (!root.hasComponent(IBLTexture))
164
+ root.addComponent(IBLTexture);
165
+ root.setValue(IBLTexture, "src", ibl.kind === "room" ? "room" : ibl.src);
166
+ root.setValue(IBLTexture, "intensity", ibl.intensity ?? 1);
167
+ writeVec3(root.getVectorView(IBLTexture, "rotation"), 0, ibl.rotationY ?? 0, 0);
168
+ root.setValue(IBLTexture, "_needsUpdate", true);
169
+ }
170
+ /**
171
+ * Depth occlusion, through IWSDK's `DepthSensingSystem` and the per-entity
172
+ * `DepthOccludable` component.
173
+ *
174
+ * IWSDK patches the materials under each opted-in entity, which is why the
175
+ * scope matters more here than on three.js: there is no global mode to turn
176
+ * on, and an entity nobody opted in is an entity the real world will not
177
+ * hide. Whatever this port adds, it remembers, so turning occlusion off
178
+ * never strips a component the app put there itself.
179
+ */
180
+ applyOcclusion(spec) {
181
+ if (spec === null) {
182
+ this.#releaseOccludables();
183
+ const running = this.#system();
184
+ if (running !== undefined)
185
+ running.config.enableOcclusion.value = false;
186
+ this.#report({ feature: "occlusion", state: "unavailable", detail: "occlusion is off" });
187
+ return;
188
+ }
189
+ const blurRadius = spec.softness === undefined ? undefined : clamp01(spec.softness) * MAX_BLUR_RADIUS;
190
+ const system = this.#system();
191
+ if (system === undefined) {
192
+ this.#world.registerSystem(DepthSensingSystem, {
193
+ configData: {
194
+ enableOcclusion: true,
195
+ enableDepthTexture: true,
196
+ useFloat32: spec.source?.format !== "luminance-alpha",
197
+ ...(blurRadius === undefined ? {} : { blurRadius }),
198
+ },
199
+ });
200
+ }
201
+ else {
202
+ system.config.enableOcclusion.value = true;
203
+ system.config.useFloat32.value = spec.source?.format !== "luminance-alpha";
204
+ if (blurRadius !== undefined)
205
+ system.config.blurRadius.value = blurRadius;
206
+ }
207
+ const mode = occlusionMode(spec.mode);
208
+ this.#releaseOccludables();
209
+ if (spec.scope === "tagged") {
210
+ this.#report({
211
+ feature: "occlusion",
212
+ state: "active",
213
+ detail: "occluding the entities the app marked with DepthOccludable",
214
+ });
215
+ return;
216
+ }
217
+ if (this.#occludables === undefined) {
218
+ this.#report({
219
+ feature: "occlusion",
220
+ state: "unavailable",
221
+ detail: "IWSDK occludes per entity; pass an occludables option, or use scope: tagged and add DepthOccludable yourself",
222
+ });
223
+ return;
224
+ }
225
+ let count = 0;
226
+ for (const entity of this.#occludables()) {
227
+ if (entity.hasComponent(DepthOccludable))
228
+ continue;
229
+ entity.addComponent(DepthOccludable, { mode });
230
+ this.#occluding.push(entity);
231
+ count += 1;
232
+ }
233
+ this.#report({
234
+ feature: "occlusion",
235
+ state: "active",
236
+ detail: "occluding " + String(count) + " entities",
237
+ });
238
+ }
239
+ /**
240
+ * IWSDK 0.5.3 has no light estimation at all - no probe, no session hook,
241
+ * nothing in its typings. This says so instead of leaving the app to wonder
242
+ * why the room never lights anything, and the request is recorded in
243
+ * docs/UPSTREAM_ENHANCEMENTS.md.
244
+ */
245
+ applyLightEstimation(estimation) {
246
+ if (estimation === null) {
247
+ this.#report({
248
+ feature: "lightEstimation",
249
+ state: "unavailable",
250
+ detail: "light estimation is off",
251
+ });
252
+ return;
253
+ }
254
+ this.#report({
255
+ feature: "lightEstimation",
256
+ state: "unsupported",
257
+ detail: "IWSDK 0.5.3 does not expose WebXR light estimation",
258
+ });
259
+ }
84
260
  applyFog(fog) {
85
261
  const scene = this.#world.scene;
86
262
  if (fog === null) {
@@ -147,6 +323,22 @@ export class IWSDKEnvironmentPort {
147
323
  this.applyFog(null);
148
324
  this.applyAmbient(null);
149
325
  this.applyKeyLight(null);
326
+ this.applyIbl(null);
327
+ this.#releaseOccludables();
328
+ this.#host = null;
329
+ }
330
+ #system() {
331
+ return this.#world.getSystem(DepthSensingSystem);
332
+ }
333
+ #releaseOccludables() {
334
+ for (const entity of this.#occluding) {
335
+ if (entity.hasComponent(DepthOccludable))
336
+ entity.removeComponent(DepthOccludable);
337
+ }
338
+ this.#occluding = [];
339
+ }
340
+ #report(report) {
341
+ this.#host?.report(report);
150
342
  }
151
343
  #levelRoot() {
152
344
  return this.#parent ?? this.#world.activeLevel?.value ?? null;
@@ -173,6 +365,34 @@ export class IWSDKEnvironmentPort {
173
365
  this.#keyEntity = null;
174
366
  }
175
367
  }
368
+ /**
369
+ * The colour at the horizon: the app's, or the one the ramp implies.
370
+ *
371
+ * IWSDK's dome and IBL components are both three-stop, so this port always has
372
+ * to supply a middle colour. Deriving it from the same ramp the three.js
373
+ * adapter draws is what stops the two engines disagreeing about a sky the app
374
+ * described once.
375
+ */
376
+ function equatorOf(gradient) {
377
+ if (gradient.equator !== undefined)
378
+ return gradient.equator;
379
+ const exponent = "exponent" in gradient ? (gradient.exponent ?? 1) : 1;
380
+ return lerpRgb(gradient.bottom, gradient.top, Math.pow(0.5, exponent));
381
+ }
382
+ /** Our mode names onto IWSDK's shader modes. */
383
+ function occlusionMode(mode) {
384
+ if (mode === "hard")
385
+ return OcclusionShadersMode.HardOcclusion;
386
+ if (mode === "minmax-soft")
387
+ return OcclusionShadersMode.MinMaxSoftOcclusion;
388
+ return OcclusionShadersMode.SoftOcclusion;
389
+ }
390
+ /** As `writeRgb`, for a `Types.Vec3` that is not a colour. */
391
+ function writeVec3(view, x, y, z) {
392
+ view[0] = x;
393
+ view[1] = y;
394
+ view[2] = z;
395
+ }
176
396
  /** `[r, g, b]` in 0..1 -> a packed 0xRRGGBB, for the three.js-side setters. */
177
397
  function rgbToHexNumber(rgb) {
178
398
  const channel = (value) => Math.round(clamp01(value) * 255);
@@ -1 +1 @@
1
- {"version":3,"file":"environment-port.js","sourceRoot":"","sources":["../src/environment-port.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `EnvironmentPort` for Meta IWSDK.\n *\n * ---------------------------------------------------------------------------\n * IT USES IWSDK'S OWN MACHINERY, NOT THREE.JS BEHIND ITS BACK\n * ---------------------------------------------------------------------------\n * The sky is a `DomeGradient` on the level root and the lights are\n * `AmbientLightComponent` and `DirectionalLightComponent` on entities.\n * Reaching past IWSDK to write `scene.background` or to `scene.add()` a light\n * would work right up until IWSDK's own `EnvironmentSystem` or `LightSystem`\n * disagreed - and one of the things IWSDK's environment system already does\n * correctly is hide authored backgrounds in an AR session, which is behaviour\n * worth inheriting rather than fighting.\n *\n * No geometry is created here. Every component this port touches is an IWSDK\n * platform facility with no app-side equivalent; a floor would be a mesh, and\n * meshes are the app's.\n *\n * Fog is the exception. IWSDK has no fog component, so it is set on\n * `world.scene` directly, with three.js's own `Fog` / `FogExp2` imported from\n * `@iwsdk/core` (the framework's rule - one three.js instance, reached through\n * the framework's re-export).\n *\n * ---------------------------------------------------------------------------\n * WRITING COMPONENT COLOURS\n * ---------------------------------------------------------------------------\n * A `Types.Color` or `Types.Vec3` field must be written through\n * `getVectorView`; `setValue` on one throws. Every colour write here goes\n * through `writeColour` for that reason - it is not a style choice.\n */\nimport {\n AmbientLightComponent,\n DirectionalLightComponent,\n DomeGradient,\n Fog,\n FogExp2,\n Quaternion,\n Transform,\n Vector3,\n type Entity,\n type World,\n} from \"@iwsdk/core\";\nimport type {\n AmbientLightSpec,\n EnvironmentPort,\n FogSpec,\n KeyLightSpec,\n Rgb,\n SkySpec,\n} from \"@realitycollective/webxr-environment\";\nimport { clamp01, lerpRgb } from \"@realitycollective/webxr-environment\";\n\nexport interface IWSDKEnvironmentPortOptions {\n /**\n * Where the light entities are parented. Defaults to the active level root,\n * which is also where the dome lives, so the whole environment is torn down\n * with the level.\n */\n readonly parent?: Entity;\n /** Dome brightness multiplier. Default 1. */\n readonly skyIntensity?: number;\n}\n\n/**\n * The narrowest thing a vector view is: elics types it as a union of every\n * typed array, and all these helpers need is indexed writes and a length.\n */\ntype NumericView = { [index: number]: number; readonly length: number };\n\n/** Local -Z is where an IWSDK directional light points. */\nconst LIGHT_FORWARD = new Vector3(0, 0, -1);\nconst TEMP_DIRECTION = new Vector3();\nconst TEMP_QUATERNION = new Quaternion();\n\nexport class IWSDKEnvironmentPort implements EnvironmentPort {\n readonly #world: World;\n readonly #parent: Entity | undefined;\n readonly #skyIntensity: number;\n\n #domeEntity: Entity | null = null;\n #ambientEntity: Entity | null = null;\n #keyEntity: Entity | null = null;\n #fogKind: FogSpec[\"kind\"] | null = null;\n\n constructor(world: World, options: IWSDKEnvironmentPortOptions = {}) {\n this.#world = world;\n this.#parent = options.parent;\n this.#skyIntensity = options.skyIntensity ?? 1;\n }\n\n applySky(sky: SkySpec | null): void {\n const root = this.#levelRoot();\n if (root === null) return;\n\n if (sky === null) {\n if (this.#domeEntity !== null && this.#domeEntity.hasComponent(DomeGradient)) {\n this.#domeEntity.removeComponent(DomeGradient);\n }\n this.#domeEntity = null;\n return;\n }\n\n if (this.#domeEntity !== root || !root.hasComponent(DomeGradient)) {\n if (!root.hasComponent(DomeGradient)) root.addComponent(DomeGradient);\n this.#domeEntity = root;\n }\n\n // IWSDK's dome is a Unity-style sky / equator / ground triple. A solid sky\n // is all three the same; a gradient reads its equator from the same ramp\n // the three.js adapter uses, so the two engines agree at the horizon\n // rather than only at the poles.\n if (sky.kind === \"solid\") {\n writeRgb(root.getVectorView(DomeGradient, \"sky\"), sky.colour);\n writeRgb(root.getVectorView(DomeGradient, \"equator\"), sky.colour);\n writeRgb(root.getVectorView(DomeGradient, \"ground\"), sky.colour);\n } else {\n const equator = lerpRgb(sky.bottom, sky.top, Math.pow(0.5, sky.exponent ?? 1));\n writeRgb(root.getVectorView(DomeGradient, \"sky\"), sky.top);\n writeRgb(root.getVectorView(DomeGradient, \"equator\"), equator);\n writeRgb(root.getVectorView(DomeGradient, \"ground\"), sky.bottom);\n }\n root.setValue(DomeGradient, \"intensity\", this.#skyIntensity);\n root.setValue(DomeGradient, \"_needsUpdate\", true);\n }\n\n applyFog(fog: FogSpec | null): void {\n const scene = this.#world.scene;\n if (fog === null) {\n scene.fog = null;\n this.#fogKind = null;\n return;\n }\n if (this.#fogKind !== fog.kind || scene.fog === null) {\n scene.fog =\n fog.kind === \"linear\"\n ? new Fog(rgbToHexNumber(fog.colour), fog.near, fog.far)\n : new FogExp2(rgbToHexNumber(fog.colour), fog.density);\n this.#fogKind = fog.kind;\n return;\n }\n const existing = scene.fog;\n existing.color.setHex(rgbToHexNumber(fog.colour));\n if (fog.kind === \"linear\" && existing instanceof Fog) {\n existing.near = fog.near;\n existing.far = fog.far;\n } else if (fog.kind === \"exponential\" && existing instanceof FogExp2) {\n existing.density = fog.density;\n }\n }\n\n applyAmbient(light: AmbientLightSpec | null): void {\n if (light === null) {\n this.#destroy(\"ambient\");\n return;\n }\n const entity = this.#ensure(\"ambient\");\n if (!entity.hasComponent(AmbientLightComponent)) {\n entity.addComponent(AmbientLightComponent);\n }\n writeRgb(entity.getVectorView(AmbientLightComponent, \"color\"), light.colour);\n entity.setValue(AmbientLightComponent, \"intensity\", light.intensity);\n }\n\n applyKeyLight(light: KeyLightSpec | null): void {\n if (light === null) {\n this.#destroy(\"key\");\n return;\n }\n const entity = this.#ensure(\"key\");\n if (!entity.hasComponent(DirectionalLightComponent)) {\n entity.addComponent(DirectionalLightComponent);\n }\n writeRgb(entity.getVectorView(DirectionalLightComponent, \"color\"), light.colour);\n entity.setValue(DirectionalLightComponent, \"intensity\", light.intensity);\n entity.setValue(DirectionalLightComponent, \"castShadow\", light.castShadow ?? false);\n\n // The spec names the direction light TRAVELS; the component emits along\n // the entity's local -Z, so the entity is rotated to face that way. A\n // zero-length direction would produce a NaN quaternion, so it falls back\n // to straight down.\n const [x, y, z] = light.direction;\n if (Math.hypot(x, y, z) === 0) TEMP_DIRECTION.set(0, -1, 0);\n else TEMP_DIRECTION.set(x, y, z).normalize();\n TEMP_QUATERNION.setFromUnitVectors(LIGHT_FORWARD, TEMP_DIRECTION);\n writeQuaternion(entity.getVectorView(Transform, \"orientation\"), TEMP_QUATERNION);\n }\n\n dispose(): void {\n this.applySky(null);\n this.applyFog(null);\n this.applyAmbient(null);\n this.applyKeyLight(null);\n }\n\n #levelRoot(): Entity | null {\n return this.#parent ?? this.#world.activeLevel?.value ?? null;\n }\n\n /** The light entities exist independently of the level, so this cannot fail. */\n #ensure(which: \"ambient\" | \"key\"): Entity {\n const existing = which === \"ambient\" ? this.#ambientEntity : this.#keyEntity;\n if (existing !== null) return existing;\n const parent = this.#levelRoot();\n const entity = this.#world.createTransformEntity(\n undefined,\n parent === null ? undefined : { parent },\n );\n if (which === \"ambient\") this.#ambientEntity = entity;\n else this.#keyEntity = entity;\n return entity;\n }\n\n #destroy(which: \"ambient\" | \"key\"): void {\n const entity = which === \"ambient\" ? this.#ambientEntity : this.#keyEntity;\n entity?.destroy();\n if (which === \"ambient\") this.#ambientEntity = null;\n else this.#keyEntity = null;\n }\n}\n\n/** `[r, g, b]` in 0..1 -> a packed 0xRRGGBB, for the three.js-side setters. */\nfunction rgbToHexNumber(rgb: Rgb): number {\n const channel = (value: number) => Math.round(clamp01(value) * 255);\n return (channel(rgb[0]) << 16) | (channel(rgb[1]) << 8) | channel(rgb[2]);\n}\n\n/**\n * Write an RGBA colour into a component's vector view.\n *\n * The VIEW is passed in rather than the component and field name, so every\n * call site keeps elics's own `getVectorView` typing - which checks that the\n * field exists AND that it is an array type. A helper that took the component\n * would have to erase those generics, and the first misspelled field name\n * would then reach a headset instead of the compiler.\n *\n * Alpha is always 1: the specs describe opaque colours. Writing index 3 of a\n * three-component view is a no-op in JavaScript rather than an error, so no\n * length check is needed for a component whose colour is a `Types.Vec3`.\n */\nfunction writeRgb(view: NumericView, rgb: Rgb): void {\n view[0] = rgb[0];\n view[1] = rgb[1];\n view[2] = rgb[2];\n view[3] = 1;\n}\n\n/** As above, for a `Types.Vec4` orientation. */\nfunction writeQuaternion(view: NumericView, quaternion: Quaternion): void {\n view[0] = quaternion.x;\n view[1] = quaternion.y;\n view[2] = quaternion.z;\n view[3] = quaternion.w;\n}\n"]}
1
+ {"version":3,"file":"environment-port.js","sourceRoot":"","sources":["../src/environment-port.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `EnvironmentPort` for Meta IWSDK.\n *\n * ---------------------------------------------------------------------------\n * IT USES IWSDK'S OWN MACHINERY, NOT THREE.JS BEHIND ITS BACK\n * ---------------------------------------------------------------------------\n * The sky is a `DomeGradient` on the level root and the lights are\n * `AmbientLightComponent` and `DirectionalLightComponent` on entities.\n * Reaching past IWSDK to write `scene.background` or to `scene.add()` a light\n * would work right up until IWSDK's own `EnvironmentSystem` or `LightSystem`\n * disagreed - and one of the things IWSDK's environment system already does\n * correctly is hide authored backgrounds in an AR session, which is behaviour\n * worth inheriting rather than fighting.\n *\n * No geometry is created here. Every component this port touches is an IWSDK\n * platform facility with no app-side equivalent; a floor would be a mesh, and\n * meshes are the app's.\n *\n * Fog is the exception. IWSDK has no fog component, so it is set on\n * `world.scene` directly, with three.js's own `Fog` / `FogExp2` imported from\n * `@iwsdk/core` (the framework's rule - one three.js instance, reached through\n * the framework's re-export).\n *\n * ---------------------------------------------------------------------------\n * WRITING COMPONENT COLOURS\n * ---------------------------------------------------------------------------\n * A `Types.Color` or `Types.Vec3` field must be written through\n * `getVectorView`; `setValue` on one throws. Every colour write here goes\n * through `writeColour` for that reason - it is not a style choice.\n */\nimport {\n AmbientLightComponent,\n DepthOccludable,\n DepthSensingSystem,\n DirectionalLightComponent,\n DomeGradient,\n DomeTexture,\n Fog,\n FogExp2,\n IBLGradient,\n IBLTexture,\n OcclusionShadersMode,\n Quaternion,\n Transform,\n Vector3,\n type Entity,\n type World,\n} from \"@iwsdk/core\";\nimport type {\n AmbientLightSpec,\n EnvironmentPort,\n EnvironmentPortHost,\n FogSpec,\n IblSpec,\n KeyLightSpec,\n OcclusionSpec,\n ResolvedLightEstimation,\n Rgb,\n SensingReport,\n SkyGradient,\n SkySpec,\n} from \"@realitycollective/webxr-environment\";\nimport { clamp01, lerpRgb } from \"@realitycollective/webxr-environment\";\n\nexport interface IWSDKEnvironmentPortOptions {\n /**\n * Where the light entities are parented. Defaults to the active level root,\n * which is also where the dome lives, so the whole environment is torn down\n * with the level.\n */\n readonly parent?: Entity;\n /** Dome brightness multiplier, used when a sky does not name its own. Default 1. */\n readonly skyIntensity?: number;\n /**\n * Which entities the real world is allowed to hide, for `scope: \"all\"`.\n *\n * IWSDK opts entities INTO occlusion one at a time, by patching the\n * materials under each one, so somebody has to say which entities those\n * are. It cannot be this port: the entities are content, content is the\n * app's, and walking a scene graph looking for meshes would be this package\n * guessing at the app's structure.\n *\n * With no list, `scope: \"all\"` reports `unavailable` and says why. An app\n * that would rather tag its own entities uses `scope: \"tagged\"` and adds\n * `DepthOccludable` itself; then this port only configures the system.\n */\n readonly occludables?: () => Iterable<Entity>;\n}\n\n/** 0..1 softness onto IWSDK's blur radius in pixels, whose default is 20. */\nconst MAX_BLUR_RADIUS = 40;\n\n/**\n * The narrowest thing a vector view is: elics types it as a union of every\n * typed array, and all these helpers need is indexed writes and a length.\n */\ntype NumericView = { [index: number]: number; readonly length: number };\n\n/** Local -Z is where an IWSDK directional light points. */\nconst LIGHT_FORWARD = new Vector3(0, 0, -1);\nconst TEMP_DIRECTION = new Vector3();\nconst TEMP_QUATERNION = new Quaternion();\n\nexport class IWSDKEnvironmentPort implements EnvironmentPort {\n readonly #world: World;\n readonly #parent: Entity | undefined;\n readonly #skyIntensity: number;\n\n readonly #occludables: (() => Iterable<Entity>) | undefined;\n\n #domeEntity: Entity | null = null;\n #iblEntity: Entity | null = null;\n #ambientEntity: Entity | null = null;\n #keyEntity: Entity | null = null;\n #fogKind: FogSpec[\"kind\"] | null = null;\n #host: EnvironmentPortHost | null = null;\n /** Only what THIS port added, so it never removes the app's own component. */\n #occluding: Entity[] = [];\n\n constructor(world: World, options: IWSDKEnvironmentPortOptions = {}) {\n this.#world = world;\n this.#parent = options.parent;\n this.#skyIntensity = options.skyIntensity ?? 1;\n this.#occludables = options.occludables;\n }\n\n observe(host: EnvironmentPortHost): () => void {\n this.#host = host;\n return () => {\n this.#host = null;\n };\n }\n\n applySky(sky: SkySpec | null): void {\n const root = this.#levelRoot();\n if (root === null) return;\n\n if (sky === null) {\n if (this.#domeEntity !== null && this.#domeEntity.hasComponent(DomeGradient)) {\n this.#domeEntity.removeComponent(DomeGradient);\n }\n this.#domeEntity = null;\n return;\n }\n\n if (sky.kind === \"texture\") {\n // An authored sky is a different IWSDK component, so the dome one goes\n // rather than sitting underneath and fighting over the same background.\n if (root.hasComponent(DomeGradient)) root.removeComponent(DomeGradient);\n if (!root.hasComponent(DomeTexture)) root.addComponent(DomeTexture);\n this.#domeEntity = root;\n root.setValue(DomeTexture, \"src\", sky.src);\n root.setValue(DomeTexture, \"intensity\", sky.intensity ?? this.#skyIntensity);\n root.setValue(DomeTexture, \"blurriness\", sky.blur ?? 0);\n writeVec3(root.getVectorView(DomeTexture, \"rotation\"), 0, sky.rotationY ?? 0, 0);\n root.setValue(DomeTexture, \"_needsUpdate\", true);\n return;\n }\n\n if (root.hasComponent(DomeTexture)) root.removeComponent(DomeTexture);\n if (this.#domeEntity !== root || !root.hasComponent(DomeGradient)) {\n if (!root.hasComponent(DomeGradient)) root.addComponent(DomeGradient);\n this.#domeEntity = root;\n }\n\n // IWSDK's dome is a Unity-style sky / equator / ground triple. A solid sky\n // is all three the same. A gradient uses the horizon colour the app gave,\n // and derives one from the ramp when it did not - which is the same colour\n // the three.js adapter puts at the horizon, so the two engines agree there\n // rather than only at the poles.\n if (sky.kind === \"solid\") {\n writeRgb(root.getVectorView(DomeGradient, \"sky\"), sky.colour);\n writeRgb(root.getVectorView(DomeGradient, \"equator\"), sky.colour);\n writeRgb(root.getVectorView(DomeGradient, \"ground\"), sky.colour);\n root.setValue(DomeGradient, \"intensity\", this.#skyIntensity);\n } else {\n writeRgb(root.getVectorView(DomeGradient, \"sky\"), sky.top);\n writeRgb(root.getVectorView(DomeGradient, \"equator\"), equatorOf(sky));\n writeRgb(root.getVectorView(DomeGradient, \"ground\"), sky.bottom);\n root.setValue(DomeGradient, \"intensity\", sky.intensity ?? this.#skyIntensity);\n }\n root.setValue(DomeGradient, \"_needsUpdate\", true);\n }\n\n /**\n * The environment map, as IWSDK's own image-based lighting components.\n *\n * `room` is not an approximation here: IWSDK's `IBLTexture` takes the string\n * `\"room\"` for its built-in probe, so the one kind three.js has to fake is\n * the one kind this host has natively.\n */\n applyIbl(ibl: IblSpec | null): void {\n const root = this.#levelRoot();\n if (root === null) return;\n\n if (ibl === null) {\n if (this.#iblEntity !== null) {\n if (this.#iblEntity.hasComponent(IBLGradient)) this.#iblEntity.removeComponent(IBLGradient);\n if (this.#iblEntity.hasComponent(IBLTexture)) this.#iblEntity.removeComponent(IBLTexture);\n }\n this.#iblEntity = null;\n return;\n }\n this.#iblEntity = root;\n\n if (ibl.kind === \"estimated\") {\n // IWSDK measures no reflections (UPSTREAM_ENHANCEMENTS 6.1), so the slot\n // keeps whatever it had rather than being cleared: an app that asked for\n // measured reflections and got none should not also lose its own map.\n this.#report({\n feature: \"lightEstimation\",\n state: \"unsupported\",\n detail: \"IWSDK measures no reflections, so an estimated environment map cannot be applied\",\n });\n return;\n }\n\n if (ibl.kind === \"gradient\") {\n if (root.hasComponent(IBLTexture)) root.removeComponent(IBLTexture);\n if (!root.hasComponent(IBLGradient)) root.addComponent(IBLGradient);\n writeRgb(root.getVectorView(IBLGradient, \"sky\"), ibl.top);\n writeRgb(root.getVectorView(IBLGradient, \"equator\"), equatorOf(ibl));\n writeRgb(root.getVectorView(IBLGradient, \"ground\"), ibl.bottom);\n root.setValue(IBLGradient, \"intensity\", ibl.intensity ?? 1);\n root.setValue(IBLGradient, \"_needsUpdate\", true);\n return;\n }\n\n if (root.hasComponent(IBLGradient)) root.removeComponent(IBLGradient);\n if (!root.hasComponent(IBLTexture)) root.addComponent(IBLTexture);\n root.setValue(IBLTexture, \"src\", ibl.kind === \"room\" ? \"room\" : ibl.src);\n root.setValue(IBLTexture, \"intensity\", ibl.intensity ?? 1);\n writeVec3(root.getVectorView(IBLTexture, \"rotation\"), 0, ibl.rotationY ?? 0, 0);\n root.setValue(IBLTexture, \"_needsUpdate\", true);\n }\n\n /**\n * Depth occlusion, through IWSDK's `DepthSensingSystem` and the per-entity\n * `DepthOccludable` component.\n *\n * IWSDK patches the materials under each opted-in entity, which is why the\n * scope matters more here than on three.js: there is no global mode to turn\n * on, and an entity nobody opted in is an entity the real world will not\n * hide. Whatever this port adds, it remembers, so turning occlusion off\n * never strips a component the app put there itself.\n */\n applyOcclusion(spec: OcclusionSpec | null): void {\n if (spec === null) {\n this.#releaseOccludables();\n const running = this.#system();\n if (running !== undefined) running.config.enableOcclusion.value = false;\n this.#report({ feature: \"occlusion\", state: \"unavailable\", detail: \"occlusion is off\" });\n return;\n }\n\n const blurRadius =\n spec.softness === undefined ? undefined : clamp01(spec.softness) * MAX_BLUR_RADIUS;\n const system = this.#system();\n if (system === undefined) {\n this.#world.registerSystem(DepthSensingSystem, {\n configData: {\n enableOcclusion: true,\n enableDepthTexture: true,\n useFloat32: spec.source?.format !== \"luminance-alpha\",\n ...(blurRadius === undefined ? {} : { blurRadius }),\n },\n });\n } else {\n system.config.enableOcclusion.value = true;\n system.config.useFloat32.value = spec.source?.format !== \"luminance-alpha\";\n if (blurRadius !== undefined) system.config.blurRadius.value = blurRadius;\n }\n\n const mode = occlusionMode(spec.mode);\n this.#releaseOccludables();\n if (spec.scope === \"tagged\") {\n this.#report({\n feature: \"occlusion\",\n state: \"active\",\n detail: \"occluding the entities the app marked with DepthOccludable\",\n });\n return;\n }\n if (this.#occludables === undefined) {\n this.#report({\n feature: \"occlusion\",\n state: \"unavailable\",\n detail:\n \"IWSDK occludes per entity; pass an occludables option, or use scope: tagged and add DepthOccludable yourself\",\n });\n return;\n }\n let count = 0;\n for (const entity of this.#occludables()) {\n if (entity.hasComponent(DepthOccludable)) continue;\n entity.addComponent(DepthOccludable, { mode });\n this.#occluding.push(entity);\n count += 1;\n }\n this.#report({\n feature: \"occlusion\",\n state: \"active\",\n detail: \"occluding \" + String(count) + \" entities\",\n });\n }\n\n /**\n * IWSDK 0.5.3 has no light estimation at all - no probe, no session hook,\n * nothing in its typings. This says so instead of leaving the app to wonder\n * why the room never lights anything, and the request is recorded in\n * docs/UPSTREAM_ENHANCEMENTS.md.\n */\n applyLightEstimation(estimation: ResolvedLightEstimation | null): void {\n if (estimation === null) {\n this.#report({\n feature: \"lightEstimation\",\n state: \"unavailable\",\n detail: \"light estimation is off\",\n });\n return;\n }\n this.#report({\n feature: \"lightEstimation\",\n state: \"unsupported\",\n detail: \"IWSDK 0.5.3 does not expose WebXR light estimation\",\n });\n }\n\n applyFog(fog: FogSpec | null): void {\n const scene = this.#world.scene;\n if (fog === null) {\n scene.fog = null;\n this.#fogKind = null;\n return;\n }\n if (this.#fogKind !== fog.kind || scene.fog === null) {\n scene.fog =\n fog.kind === \"linear\"\n ? new Fog(rgbToHexNumber(fog.colour), fog.near, fog.far)\n : new FogExp2(rgbToHexNumber(fog.colour), fog.density);\n this.#fogKind = fog.kind;\n return;\n }\n const existing = scene.fog;\n existing.color.setHex(rgbToHexNumber(fog.colour));\n if (fog.kind === \"linear\" && existing instanceof Fog) {\n existing.near = fog.near;\n existing.far = fog.far;\n } else if (fog.kind === \"exponential\" && existing instanceof FogExp2) {\n existing.density = fog.density;\n }\n }\n\n applyAmbient(light: AmbientLightSpec | null): void {\n if (light === null) {\n this.#destroy(\"ambient\");\n return;\n }\n const entity = this.#ensure(\"ambient\");\n if (!entity.hasComponent(AmbientLightComponent)) {\n entity.addComponent(AmbientLightComponent);\n }\n writeRgb(entity.getVectorView(AmbientLightComponent, \"color\"), light.colour);\n entity.setValue(AmbientLightComponent, \"intensity\", light.intensity);\n }\n\n applyKeyLight(light: KeyLightSpec | null): void {\n if (light === null) {\n this.#destroy(\"key\");\n return;\n }\n const entity = this.#ensure(\"key\");\n if (!entity.hasComponent(DirectionalLightComponent)) {\n entity.addComponent(DirectionalLightComponent);\n }\n writeRgb(entity.getVectorView(DirectionalLightComponent, \"color\"), light.colour);\n entity.setValue(DirectionalLightComponent, \"intensity\", light.intensity);\n entity.setValue(DirectionalLightComponent, \"castShadow\", light.castShadow ?? false);\n\n // The spec names the direction light TRAVELS; the component emits along\n // the entity's local -Z, so the entity is rotated to face that way. A\n // zero-length direction would produce a NaN quaternion, so it falls back\n // to straight down.\n const [x, y, z] = light.direction;\n if (Math.hypot(x, y, z) === 0) TEMP_DIRECTION.set(0, -1, 0);\n else TEMP_DIRECTION.set(x, y, z).normalize();\n TEMP_QUATERNION.setFromUnitVectors(LIGHT_FORWARD, TEMP_DIRECTION);\n writeQuaternion(entity.getVectorView(Transform, \"orientation\"), TEMP_QUATERNION);\n }\n\n dispose(): void {\n this.applySky(null);\n this.applyFog(null);\n this.applyAmbient(null);\n this.applyKeyLight(null);\n this.applyIbl(null);\n this.#releaseOccludables();\n this.#host = null;\n }\n\n #system() {\n return this.#world.getSystem(DepthSensingSystem);\n }\n\n #releaseOccludables(): void {\n for (const entity of this.#occluding) {\n if (entity.hasComponent(DepthOccludable)) entity.removeComponent(DepthOccludable);\n }\n this.#occluding = [];\n }\n\n #report(report: SensingReport): void {\n this.#host?.report(report);\n }\n\n #levelRoot(): Entity | null {\n return this.#parent ?? this.#world.activeLevel?.value ?? null;\n }\n\n /** The light entities exist independently of the level, so this cannot fail. */\n #ensure(which: \"ambient\" | \"key\"): Entity {\n const existing = which === \"ambient\" ? this.#ambientEntity : this.#keyEntity;\n if (existing !== null) return existing;\n const parent = this.#levelRoot();\n const entity = this.#world.createTransformEntity(\n undefined,\n parent === null ? undefined : { parent },\n );\n if (which === \"ambient\") this.#ambientEntity = entity;\n else this.#keyEntity = entity;\n return entity;\n }\n\n #destroy(which: \"ambient\" | \"key\"): void {\n const entity = which === \"ambient\" ? this.#ambientEntity : this.#keyEntity;\n entity?.destroy();\n if (which === \"ambient\") this.#ambientEntity = null;\n else this.#keyEntity = null;\n }\n}\n\n/**\n * The colour at the horizon: the app's, or the one the ramp implies.\n *\n * IWSDK's dome and IBL components are both three-stop, so this port always has\n * to supply a middle colour. Deriving it from the same ramp the three.js\n * adapter draws is what stops the two engines disagreeing about a sky the app\n * described once.\n */\nfunction equatorOf(gradient: SkyGradient | { top: Rgb; bottom: Rgb; equator?: Rgb }): Rgb {\n if (gradient.equator !== undefined) return gradient.equator;\n const exponent = \"exponent\" in gradient ? (gradient.exponent ?? 1) : 1;\n return lerpRgb(gradient.bottom, gradient.top, Math.pow(0.5, exponent));\n}\n\n/** Our mode names onto IWSDK's shader modes. */\nfunction occlusionMode(mode: OcclusionSpec[\"mode\"]): string {\n if (mode === \"hard\") return OcclusionShadersMode.HardOcclusion;\n if (mode === \"minmax-soft\") return OcclusionShadersMode.MinMaxSoftOcclusion;\n return OcclusionShadersMode.SoftOcclusion;\n}\n\n/** As `writeRgb`, for a `Types.Vec3` that is not a colour. */\nfunction writeVec3(view: NumericView, x: number, y: number, z: number): void {\n view[0] = x;\n view[1] = y;\n view[2] = z;\n}\n\n/** `[r, g, b]` in 0..1 -> a packed 0xRRGGBB, for the three.js-side setters. */\nfunction rgbToHexNumber(rgb: Rgb): number {\n const channel = (value: number) => Math.round(clamp01(value) * 255);\n return (channel(rgb[0]) << 16) | (channel(rgb[1]) << 8) | channel(rgb[2]);\n}\n\n/**\n * Write an RGBA colour into a component's vector view.\n *\n * The VIEW is passed in rather than the component and field name, so every\n * call site keeps elics's own `getVectorView` typing - which checks that the\n * field exists AND that it is an array type. A helper that took the component\n * would have to erase those generics, and the first misspelled field name\n * would then reach a headset instead of the compiler.\n *\n * Alpha is always 1: the specs describe opaque colours. Writing index 3 of a\n * three-component view is a no-op in JavaScript rather than an error, so no\n * length check is needed for a component whose colour is a `Types.Vec3`.\n */\nfunction writeRgb(view: NumericView, rgb: Rgb): void {\n view[0] = rgb[0];\n view[1] = rgb[1];\n view[2] = rgb[2];\n view[3] = 1;\n}\n\n/** As above, for a `Types.Vec4` orientation. */\nfunction writeQuaternion(view: NumericView, quaternion: Quaternion): void {\n view[0] = quaternion.x;\n view[1] = quaternion.y;\n view[2] = quaternion.z;\n view[3] = quaternion.w;\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -15,6 +15,8 @@ export type { IWSDKEnvironmentPortOptions } from "./environment-port.js";
15
15
  export { IWSDKEnvironmentPort } from "./environment-port.js";
16
16
  export type { IWSDKAudioPortOptions } from "./audio-port.js";
17
17
  export { IWSDKAudioPort } from "./audio-port.js";
18
+ export type { IWSDKWorldSensingPortOptions } from "./world-sensing-port.js";
19
+ export { IWSDKWorldSensingPort } from "./world-sensing-port.js";
18
20
  export * from "@realitycollective/webxr-environment";
19
21
  export interface IWSDKEnvironmentOptions extends EnvironmentDirectorOptions, IWSDKEnvironmentPortOptions {
20
22
  /** Audio director + port options. Omit to set up the environment only. */
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@ import { IWSDKEnvironmentPort } from "./environment-port.js";
10
10
  import { IWSDKAudioPort } from "./audio-port.js";
11
11
  export { IWSDKEnvironmentPort } from "./environment-port.js";
12
12
  export { IWSDKAudioPort } from "./audio-port.js";
13
+ export { IWSDKWorldSensingPort } from "./world-sensing-port.js";
13
14
  export * from "@realitycollective/webxr-environment";
14
15
  const hosts = new WeakMap();
15
16
  /** The setup registered for a world, if `registerEnvironment` has run. */
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,YAAY,EAA2B,MAAM,aAAa,CAAC;AAKpE,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,sCAAsC,CAAC;AAE1F,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAE7D,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGjD,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAE7D,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEjD,cAAc,sCAAsC,CAAC;AAkBrD,MAAM,KAAK,GAAG,IAAI,OAAO,EAAgC,CAAC;AAE1D,0EAA0E;AAC1E,MAAM,UAAU,cAAc,CAAC,KAAY;IACzC,OAAO,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;AAC1B,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,OAAO,iBAAkB,SAAQ,YAAY,CAAC,EAAE,CAAC;IAC5C,MAAM,CAAC,KAAa;QAC3B,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,KAAyB,CAAC,CAAC;QACxD,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO;QAChC,MAAM,OAAO,GAAG,KAAK,GAAG,IAAI,CAAC;QAC7B,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAClC,KAAK,CAAC,KAAK,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;IAC/B,CAAC;CACF;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,mBAAmB,CACjC,KAAY,EACZ,UAAmC,EAAE;IAErC,MAAM,QAAQ,GAAG,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAClC,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO,QAAQ,CAAC;IAE5C,MAAM,eAAe,GAAG,IAAI,oBAAoB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IACjE,MAAM,WAAW,GAAG,IAAI,mBAAmB,CAAC,eAAe,EAAE,OAAO,CAAC,CAAC;IAEtE,IAAI,KAAK,GAAyB,IAAI,CAAC;IACvC,IAAI,SAAS,GAA0B,IAAI,CAAC;IAC5C,IAAI,OAAO,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;QAChC,SAAS,GAAG,IAAI,cAAc,CAAC,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;QACrD,KAAK,GAAG,IAAI,aAAa,CAAC,SAAS,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IACtD,CAAC;IAED,MAAM,KAAK,GAA0B;QACnC,WAAW;QACX,eAAe;QACf,KAAK;QACL,SAAS;QACT,OAAO;YACL,KAAK,EAAE,OAAO,EAAE,CAAC;YACjB,WAAW,CAAC,OAAO,EAAE,CAAC;YACtB,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACtB,CAAC;KACF,CAAC;IACF,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IACxB,KAAK,CAAC,cAAc,CAAC,iBAAiB,CAAC,CAAC;IACxC,OAAO,KAAK,CAAC;AACf,CAAC","sourcesContent":["/**\n * `@realitycollective/iwsdk-environment` - the Meta IWSDK adapter.\n *\n * Re-exports the whole engine-free core, so an app installs this package and\n * nothing else.\n */\nimport { createSystem, type Entity, type World } from \"@iwsdk/core\";\nimport type {\n AudioDirectorOptions,\n EnvironmentDirectorOptions,\n} from \"@realitycollective/webxr-environment\";\nimport { AudioDirector, EnvironmentDirector } from \"@realitycollective/webxr-environment\";\nimport type { IWSDKEnvironmentPortOptions } from \"./environment-port.js\";\nimport { IWSDKEnvironmentPort } from \"./environment-port.js\";\nimport type { IWSDKAudioPortOptions } from \"./audio-port.js\";\nimport { IWSDKAudioPort } from \"./audio-port.js\";\n\nexport type { IWSDKEnvironmentPortOptions } from \"./environment-port.js\";\nexport { IWSDKEnvironmentPort } from \"./environment-port.js\";\nexport type { IWSDKAudioPortOptions } from \"./audio-port.js\";\nexport { IWSDKAudioPort } from \"./audio-port.js\";\n\nexport * from \"@realitycollective/webxr-environment\";\n\nexport interface IWSDKEnvironmentOptions\n extends EnvironmentDirectorOptions,\n IWSDKEnvironmentPortOptions {\n /** Audio director + port options. Omit to set up the environment only. */\n readonly audio?: AudioDirectorOptions & IWSDKAudioPortOptions;\n}\n\nexport interface IWSDKEnvironmentSetup {\n readonly environment: EnvironmentDirector;\n readonly environmentPort: IWSDKEnvironmentPort;\n readonly audio: AudioDirector | null;\n readonly audioPort: IWSDKAudioPort | null;\n /** Tear both directors down and remove everything they created. */\n dispose(): void;\n}\n\nconst hosts = new WeakMap<World, IWSDKEnvironmentSetup>();\n\n/** The setup registered for a world, if `registerEnvironment` has run. */\nexport function environmentFor(world: World): IWSDKEnvironmentSetup | undefined {\n return hosts.get(world);\n}\n\n/**\n * The per-frame tick.\n *\n * Neither director owns a loop, so something has to drive them, and on IWSDK\n * that something is a system - which means the environment stops advancing\n * when the session loses focus, exactly like the rest of the app.\n * `registerEnvironment` registers this for you; it is exported so an app that\n * builds its own system schedule can place it deliberately.\n *\n * IWSDK hands a system its delta in SECONDS and the directors take\n * milliseconds. That conversion happens here, once.\n */\nexport class EnvironmentSystem extends createSystem({}) {\n override update(delta: number): void {\n const setup = hosts.get(this.world as unknown as World);\n if (setup === undefined) return;\n const deltaMs = delta * 1000;\n setup.environment.update(deltaMs);\n setup.audio?.update(deltaMs);\n }\n}\n\n/**\n * One call to wire the environment (and optionally the audio) into an IWSDK\n * world, and register the system that ticks them.\n *\n * ```ts\n * const env = registerEnvironment(world, {\n * presets: STOCK_PRESETS,\n * initial: VOID,\n * audio: { cues: [{ id: \"hum\", src: \"/audio/hum.mp3\", bus: \"ambience\", loop: true }] },\n * });\n * env.environment.transition(\"dusk\", { durationMs: 8000 });\n * ```\n *\n * Passthrough is NOT read from the session here - push it in from whatever\n * already tracks capabilities (`env.environment.setPassthrough(...)`). The\n * session belongs to the platform layer, not to the environment.\n */\nexport function registerEnvironment(\n world: World,\n options: IWSDKEnvironmentOptions = {},\n): IWSDKEnvironmentSetup {\n const existing = hosts.get(world);\n if (existing !== undefined) return existing;\n\n const environmentPort = new IWSDKEnvironmentPort(world, options);\n const environment = new EnvironmentDirector(environmentPort, options);\n\n let audio: AudioDirector | null = null;\n let audioPort: IWSDKAudioPort | null = null;\n if (options.audio !== undefined) {\n audioPort = new IWSDKAudioPort(world, options.audio);\n audio = new AudioDirector(audioPort, options.audio);\n }\n\n const setup: IWSDKEnvironmentSetup = {\n environment,\n environmentPort,\n audio,\n audioPort,\n dispose() {\n audio?.dispose();\n environment.dispose();\n hosts.delete(world);\n },\n };\n hosts.set(world, setup);\n world.registerSystem(EnvironmentSystem);\n return setup;\n}\n\nexport type { Entity as IWSDKEntity };\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,YAAY,EAA2B,MAAM,aAAa,CAAC;AAKpE,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,sCAAsC,CAAC;AAE1F,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAE7D,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGjD,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAE7D,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEjD,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAEhE,cAAc,sCAAsC,CAAC;AAkBrD,MAAM,KAAK,GAAG,IAAI,OAAO,EAAgC,CAAC;AAE1D,0EAA0E;AAC1E,MAAM,UAAU,cAAc,CAAC,KAAY;IACzC,OAAO,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;AAC1B,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,OAAO,iBAAkB,SAAQ,YAAY,CAAC,EAAE,CAAC;IAC5C,MAAM,CAAC,KAAa;QAC3B,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,KAAyB,CAAC,CAAC;QACxD,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO;QAChC,MAAM,OAAO,GAAG,KAAK,GAAG,IAAI,CAAC;QAC7B,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAClC,KAAK,CAAC,KAAK,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;IAC/B,CAAC;CACF;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,mBAAmB,CACjC,KAAY,EACZ,UAAmC,EAAE;IAErC,MAAM,QAAQ,GAAG,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAClC,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO,QAAQ,CAAC;IAE5C,MAAM,eAAe,GAAG,IAAI,oBAAoB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IACjE,MAAM,WAAW,GAAG,IAAI,mBAAmB,CAAC,eAAe,EAAE,OAAO,CAAC,CAAC;IAEtE,IAAI,KAAK,GAAyB,IAAI,CAAC;IACvC,IAAI,SAAS,GAA0B,IAAI,CAAC;IAC5C,IAAI,OAAO,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;QAChC,SAAS,GAAG,IAAI,cAAc,CAAC,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;QACrD,KAAK,GAAG,IAAI,aAAa,CAAC,SAAS,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IACtD,CAAC;IAED,MAAM,KAAK,GAA0B;QACnC,WAAW;QACX,eAAe;QACf,KAAK;QACL,SAAS;QACT,OAAO;YACL,KAAK,EAAE,OAAO,EAAE,CAAC;YACjB,WAAW,CAAC,OAAO,EAAE,CAAC;YACtB,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACtB,CAAC;KACF,CAAC;IACF,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IACxB,KAAK,CAAC,cAAc,CAAC,iBAAiB,CAAC,CAAC;IACxC,OAAO,KAAK,CAAC;AACf,CAAC","sourcesContent":["/**\n * `@realitycollective/iwsdk-environment` - the Meta IWSDK adapter.\n *\n * Re-exports the whole engine-free core, so an app installs this package and\n * nothing else.\n */\nimport { createSystem, type Entity, type World } from \"@iwsdk/core\";\nimport type {\n AudioDirectorOptions,\n EnvironmentDirectorOptions,\n} from \"@realitycollective/webxr-environment\";\nimport { AudioDirector, EnvironmentDirector } from \"@realitycollective/webxr-environment\";\nimport type { IWSDKEnvironmentPortOptions } from \"./environment-port.js\";\nimport { IWSDKEnvironmentPort } from \"./environment-port.js\";\nimport type { IWSDKAudioPortOptions } from \"./audio-port.js\";\nimport { IWSDKAudioPort } from \"./audio-port.js\";\n\nexport type { IWSDKEnvironmentPortOptions } from \"./environment-port.js\";\nexport { IWSDKEnvironmentPort } from \"./environment-port.js\";\nexport type { IWSDKAudioPortOptions } from \"./audio-port.js\";\nexport { IWSDKAudioPort } from \"./audio-port.js\";\nexport type { IWSDKWorldSensingPortOptions } from \"./world-sensing-port.js\";\nexport { IWSDKWorldSensingPort } from \"./world-sensing-port.js\";\n\nexport * from \"@realitycollective/webxr-environment\";\n\nexport interface IWSDKEnvironmentOptions\n extends EnvironmentDirectorOptions,\n IWSDKEnvironmentPortOptions {\n /** Audio director + port options. Omit to set up the environment only. */\n readonly audio?: AudioDirectorOptions & IWSDKAudioPortOptions;\n}\n\nexport interface IWSDKEnvironmentSetup {\n readonly environment: EnvironmentDirector;\n readonly environmentPort: IWSDKEnvironmentPort;\n readonly audio: AudioDirector | null;\n readonly audioPort: IWSDKAudioPort | null;\n /** Tear both directors down and remove everything they created. */\n dispose(): void;\n}\n\nconst hosts = new WeakMap<World, IWSDKEnvironmentSetup>();\n\n/** The setup registered for a world, if `registerEnvironment` has run. */\nexport function environmentFor(world: World): IWSDKEnvironmentSetup | undefined {\n return hosts.get(world);\n}\n\n/**\n * The per-frame tick.\n *\n * Neither director owns a loop, so something has to drive them, and on IWSDK\n * that something is a system - which means the environment stops advancing\n * when the session loses focus, exactly like the rest of the app.\n * `registerEnvironment` registers this for you; it is exported so an app that\n * builds its own system schedule can place it deliberately.\n *\n * IWSDK hands a system its delta in SECONDS and the directors take\n * milliseconds. That conversion happens here, once.\n */\nexport class EnvironmentSystem extends createSystem({}) {\n override update(delta: number): void {\n const setup = hosts.get(this.world as unknown as World);\n if (setup === undefined) return;\n const deltaMs = delta * 1000;\n setup.environment.update(deltaMs);\n setup.audio?.update(deltaMs);\n }\n}\n\n/**\n * One call to wire the environment (and optionally the audio) into an IWSDK\n * world, and register the system that ticks them.\n *\n * ```ts\n * const env = registerEnvironment(world, {\n * presets: STOCK_PRESETS,\n * initial: VOID,\n * audio: { cues: [{ id: \"hum\", src: \"/audio/hum.mp3\", bus: \"ambience\", loop: true }] },\n * });\n * env.environment.transition(\"dusk\", { durationMs: 8000 });\n * ```\n *\n * Passthrough is NOT read from the session here - push it in from whatever\n * already tracks capabilities (`env.environment.setPassthrough(...)`). The\n * session belongs to the platform layer, not to the environment.\n */\nexport function registerEnvironment(\n world: World,\n options: IWSDKEnvironmentOptions = {},\n): IWSDKEnvironmentSetup {\n const existing = hosts.get(world);\n if (existing !== undefined) return existing;\n\n const environmentPort = new IWSDKEnvironmentPort(world, options);\n const environment = new EnvironmentDirector(environmentPort, options);\n\n let audio: AudioDirector | null = null;\n let audioPort: IWSDKAudioPort | null = null;\n if (options.audio !== undefined) {\n audioPort = new IWSDKAudioPort(world, options.audio);\n audio = new AudioDirector(audioPort, options.audio);\n }\n\n const setup: IWSDKEnvironmentSetup = {\n environment,\n environmentPort,\n audio,\n audioPort,\n dispose() {\n audio?.dispose();\n environment.dispose();\n hosts.delete(world);\n },\n };\n hosts.set(world, setup);\n world.registerSystem(EnvironmentSystem);\n return setup;\n}\n\nexport type { Entity as IWSDKEntity };\n"]}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * `WorldSensingPort` for Meta IWSDK.
3
+ *
4
+ * ---------------------------------------------------------------------------
5
+ * IWSDK HAS ALREADY DONE HALF OF THIS
6
+ * ---------------------------------------------------------------------------
7
+ * Where the three.js adapter reads an `XRFrame` directly, IWSDK's own
8
+ * `SceneUnderstandingSystem` has already turned planes, meshes and anchors
9
+ * into ENTITIES with components on them, positioned in the scene. So this port
10
+ * queries for those entities and translates them, rather than reaching past
11
+ * IWSDK to the session - which would fight the system that owns them.
12
+ *
13
+ * The same goes for hit testing: IWSDK's `EnvironmentRaycastSystem` casts from
14
+ * a hand or the viewer and moves a target entity to the hit, so a request here
15
+ * becomes an entity with an `EnvironmentRaycastTarget` on it and the answer is
16
+ * read off that entity's transform.
17
+ *
18
+ * ---------------------------------------------------------------------------
19
+ * WHAT IT CANNOT DO
20
+ * ---------------------------------------------------------------------------
21
+ * Ask for the features. `plane-detection`, `mesh-detection` and `anchors` are
22
+ * session features, IWSDK requests them from its own world configuration, and
23
+ * the session belongs to the platform layer either way. If the app did not ask
24
+ * for them, the queries stay empty forever - so this port says `pending` and
25
+ * keeps saying it, rather than pretending to have turned something on.
26
+ */
27
+ import { type Entity, type World } from "@iwsdk/core";
28
+ import type { HitTestRequest, ResolvedWorldDetection, WorldPose, WorldSensingPort, WorldSensingPortHost } from "@realitycollective/webxr-environment";
29
+ export interface IWSDKWorldSensingPortOptions {
30
+ /** Where hit-test target entities are parented. Default: no parent. */
31
+ readonly parent?: Entity;
32
+ }
33
+ export declare class IWSDKWorldSensingPort implements WorldSensingPort {
34
+ #private;
35
+ constructor(world: World, options?: IWSDKWorldSensingPortOptions);
36
+ observe(host: WorldSensingPortHost): () => void;
37
+ setDetection(detection: ResolvedWorldDetection | null): void;
38
+ startHitTest(request: HitTestRequest): void;
39
+ stopHitTest(id: string): void;
40
+ /**
41
+ * IWSDK anchors an ENTITY rather than handing back an anchor object, so this
42
+ * makes one at the requested pose and lets `SceneUnderstandingSystem` attach
43
+ * it. The id that comes back is the same id the anchor will report with.
44
+ */
45
+ createAnchor(pose: WorldPose): Promise<string | null>;
46
+ removeAnchor(id: string): void;
47
+ update(): void;
48
+ dispose(): void;
49
+ }
@@ -0,0 +1,287 @@
1
+ /**
2
+ * `WorldSensingPort` for Meta IWSDK.
3
+ *
4
+ * ---------------------------------------------------------------------------
5
+ * IWSDK HAS ALREADY DONE HALF OF THIS
6
+ * ---------------------------------------------------------------------------
7
+ * Where the three.js adapter reads an `XRFrame` directly, IWSDK's own
8
+ * `SceneUnderstandingSystem` has already turned planes, meshes and anchors
9
+ * into ENTITIES with components on them, positioned in the scene. So this port
10
+ * queries for those entities and translates them, rather than reaching past
11
+ * IWSDK to the session - which would fight the system that owns them.
12
+ *
13
+ * The same goes for hit testing: IWSDK's `EnvironmentRaycastSystem` casts from
14
+ * a hand or the viewer and moves a target entity to the hit, so a request here
15
+ * becomes an entity with an `EnvironmentRaycastTarget` on it and the answer is
16
+ * read off that entity's transform.
17
+ *
18
+ * ---------------------------------------------------------------------------
19
+ * WHAT IT CANNOT DO
20
+ * ---------------------------------------------------------------------------
21
+ * Ask for the features. `plane-detection`, `mesh-detection` and `anchors` are
22
+ * session features, IWSDK requests them from its own world configuration, and
23
+ * the session belongs to the platform layer either way. If the app did not ask
24
+ * for them, the queries stay empty forever - so this port says `pending` and
25
+ * keeps saying it, rather than pretending to have turned something on.
26
+ */
27
+ import { EnvironmentRaycastSystem, EnvironmentRaycastTarget, Quaternion, RaycastSpace, Vector3, XRAnchor, XRMesh, XRPlane, } from "@iwsdk/core";
28
+ const TEMP_POSITION = new Vector3();
29
+ const TEMP_QUATERNION = new Quaternion();
30
+ export class IWSDKWorldSensingPort {
31
+ #world;
32
+ #parent;
33
+ #ids = new WeakMap();
34
+ #hitEntities = new Map();
35
+ #anchorEntities = new Map();
36
+ #states = new Map();
37
+ /**
38
+ * The three queries, registered once at construction.
39
+ *
40
+ * elics keeps one query per component mask and updates it as entities gain
41
+ * and lose components, so registering up front costs one mask walk each and
42
+ * removes any question of whether a set exists yet. An app that never turns
43
+ * detection on simply never reads them.
44
+ */
45
+ #planeQuery;
46
+ #meshQuery;
47
+ #anchorQuery;
48
+ #host = null;
49
+ #detection = null;
50
+ #nextId = 1;
51
+ constructor(world, options = {}) {
52
+ this.#world = world;
53
+ this.#parent = options.parent;
54
+ const manager = world.queryManager;
55
+ this.#planeQuery = manager.registerQuery({ required: [XRPlane] });
56
+ this.#meshQuery = manager.registerQuery({ required: [XRMesh] });
57
+ this.#anchorQuery = manager.registerQuery({ required: [XRAnchor] });
58
+ }
59
+ observe(host) {
60
+ this.#host = host;
61
+ return () => {
62
+ this.#host = null;
63
+ };
64
+ }
65
+ setDetection(detection) {
66
+ this.#detection = detection;
67
+ if (detection === null) {
68
+ this.#host?.planes([]);
69
+ this.#host?.meshes([]);
70
+ this.#host?.anchors([]);
71
+ for (const feature of ["planes", "meshes", "anchors"]) {
72
+ this.#report({ feature, state: "unavailable", detail: "detection is off" });
73
+ }
74
+ return;
75
+ }
76
+ for (const feature of ["planes", "meshes", "anchors"]) {
77
+ this.#report(detection[feature]
78
+ ? {
79
+ feature,
80
+ state: "pending",
81
+ detail: "waiting for IWSDK's scene understanding to find something",
82
+ }
83
+ : { feature, state: "unavailable", detail: "detection is off" });
84
+ }
85
+ }
86
+ startHitTest(request) {
87
+ this.#world.registerSystem(EnvironmentRaycastSystem);
88
+ const entity = this.#world.createTransformEntity(undefined, this.#parent === undefined ? undefined : { parent: this.#parent });
89
+ entity.addComponent(EnvironmentRaycastTarget, { space: raycastSpace(request.space) });
90
+ this.#hitEntities.set(request.id, entity);
91
+ this.#report({
92
+ feature: "hitTest",
93
+ state: "pending",
94
+ detail: "waiting for a hit from the " + request.space + " ray",
95
+ });
96
+ }
97
+ stopHitTest(id) {
98
+ const entity = this.#hitEntities.get(id);
99
+ if (entity === undefined)
100
+ return;
101
+ this.#hitEntities.delete(id);
102
+ entity.destroy();
103
+ }
104
+ /**
105
+ * IWSDK anchors an ENTITY rather than handing back an anchor object, so this
106
+ * makes one at the requested pose and lets `SceneUnderstandingSystem` attach
107
+ * it. The id that comes back is the same id the anchor will report with.
108
+ */
109
+ async createAnchor(pose) {
110
+ const entity = this.#world.createTransformEntity(undefined, this.#parent === undefined ? undefined : { parent: this.#parent });
111
+ const object3D = entity.object3D;
112
+ if (object3D === undefined) {
113
+ entity.destroy();
114
+ this.#report({
115
+ feature: "anchors",
116
+ state: "unavailable",
117
+ detail: "the anchor entity has no transform to place",
118
+ });
119
+ return null;
120
+ }
121
+ object3D.position.set(pose.position[0], pose.position[1], pose.position[2]);
122
+ object3D.quaternion.set(pose.orientation[0], pose.orientation[1], pose.orientation[2], pose.orientation[3]);
123
+ object3D.updateMatrixWorld();
124
+ entity.addComponent(XRAnchor);
125
+ const id = this.#idFor(entity);
126
+ this.#anchorEntities.set(id, entity);
127
+ return id;
128
+ }
129
+ removeAnchor(id) {
130
+ const entity = this.#anchorEntities.get(id);
131
+ if (entity === undefined)
132
+ return;
133
+ this.#anchorEntities.delete(id);
134
+ entity.destroy();
135
+ }
136
+ update() {
137
+ const detection = this.#detection;
138
+ if (detection === null || this.#host === null)
139
+ return;
140
+ if (detection.planes)
141
+ this.#readPlanes();
142
+ if (detection.meshes)
143
+ this.#readMeshes();
144
+ if (detection.anchors)
145
+ this.#readAnchors();
146
+ this.#readHits();
147
+ }
148
+ dispose() {
149
+ for (const entity of this.#hitEntities.values())
150
+ entity.destroy();
151
+ this.#hitEntities.clear();
152
+ this.#anchorEntities.clear();
153
+ this.#host = null;
154
+ }
155
+ #readPlanes() {
156
+ const planes = [];
157
+ for (const entity of this.#planeQuery.entities) {
158
+ const pose = poseOf(entity);
159
+ if (pose === null)
160
+ continue;
161
+ const raw = entity.getValue(XRPlane, "_plane");
162
+ const polygon = (raw?.polygon === undefined ? [] : Array.from(raw.polygon)).map((point) => [point.x, point.z]);
163
+ planes.push({
164
+ id: this.#idFor(entity),
165
+ pose,
166
+ extents: extentsOf(polygon),
167
+ orientation: orientationOf(raw?.orientation),
168
+ label: raw?.semanticLabel?.toLowerCase() ?? null,
169
+ ...(polygon.length === 0 ? {} : { polygon }),
170
+ ...(raw?.lastChangedTime === undefined ? {} : { changedAt: raw.lastChangedTime }),
171
+ });
172
+ }
173
+ this.#host?.planes(planes);
174
+ this.#report({ feature: "planes", state: "active", detail: planes.length + " surfaces" });
175
+ }
176
+ #readMeshes() {
177
+ const meshes = [];
178
+ for (const entity of this.#meshQuery.entities) {
179
+ const pose = poseOf(entity);
180
+ if (pose === null)
181
+ continue;
182
+ // IWSDK measures the mesh for us and puts the size on the component, so
183
+ // unlike the raw WebXR path there are no vertices to walk here.
184
+ const dimensions = entity.getVectorView(XRMesh, "dimensions");
185
+ const label = entity.getValue(XRMesh, "semanticLabel");
186
+ meshes.push({
187
+ id: this.#idFor(entity),
188
+ pose,
189
+ // A `Types.Vec3` view always has three components; the assertions are
190
+ // there because `noUncheckedIndexedAccess` cannot know that, and a
191
+ // fallback would be a branch no input can reach.
192
+ extents: [dimensions[0], dimensions[1], dimensions[2]],
193
+ label: typeof label === "string" && label.length > 0 ? label.toLowerCase() : null,
194
+ });
195
+ }
196
+ this.#host?.meshes(meshes);
197
+ this.#report({ feature: "meshes", state: "active", detail: meshes.length + " meshes" });
198
+ }
199
+ #readAnchors() {
200
+ const anchors = [];
201
+ for (const entity of this.#anchorQuery.entities) {
202
+ const pose = poseOf(entity);
203
+ if (pose === null)
204
+ continue;
205
+ anchors.push({
206
+ id: this.#idFor(entity),
207
+ pose,
208
+ // IWSDK's own flag: false until the system has attached the entity to
209
+ // a real anchor, which is exactly what "not tracked yet" means here.
210
+ tracked: entity.getValue(XRAnchor, "attached") === true,
211
+ });
212
+ }
213
+ this.#host?.anchors(anchors);
214
+ this.#report({ feature: "anchors", state: "active", detail: anchors.length + " anchors" });
215
+ }
216
+ #readHits() {
217
+ for (const [id, entity] of this.#hitEntities) {
218
+ const result = entity.getValue(EnvironmentRaycastTarget, "xrHitTestResult");
219
+ const pose = poseOf(entity);
220
+ const hits = result === undefined || result === null || pose === null
221
+ ? []
222
+ : [{ sourceId: id, pose, distance: null }];
223
+ this.#host?.hits(id, hits);
224
+ if (hits.length > 0) {
225
+ this.#report({ feature: "hitTest", state: "active", detail: "hitting the room" });
226
+ }
227
+ }
228
+ }
229
+ #idFor(object) {
230
+ const existing = this.#ids.get(object);
231
+ if (existing !== undefined)
232
+ return existing;
233
+ const id = "world-" + String(this.#nextId);
234
+ this.#nextId += 1;
235
+ this.#ids.set(object, id);
236
+ return id;
237
+ }
238
+ #report(report) {
239
+ const stamp = report.state + "|" + report.detail;
240
+ if (this.#states.get(report.feature) === stamp)
241
+ return;
242
+ this.#states.set(report.feature, stamp);
243
+ this.#host?.report(report);
244
+ }
245
+ }
246
+ /** The world transform of the entity IWSDK positioned for us. */
247
+ function poseOf(entity) {
248
+ const object3D = entity.object3D;
249
+ if (object3D === undefined)
250
+ return null;
251
+ object3D.getWorldPosition(TEMP_POSITION);
252
+ object3D.getWorldQuaternion(TEMP_QUATERNION);
253
+ return {
254
+ position: [TEMP_POSITION.x, TEMP_POSITION.y, TEMP_POSITION.z],
255
+ orientation: [TEMP_QUATERNION.x, TEMP_QUATERNION.y, TEMP_QUATERNION.z, TEMP_QUATERNION.w],
256
+ };
257
+ }
258
+ function orientationOf(value) {
259
+ const lowered = value?.toLowerCase();
260
+ if (lowered === "horizontal" || lowered === "vertical")
261
+ return lowered;
262
+ return "unknown";
263
+ }
264
+ function extentsOf(polygon) {
265
+ if (polygon.length === 0)
266
+ return [0, 0];
267
+ let minX = Infinity;
268
+ let maxX = -Infinity;
269
+ let minZ = Infinity;
270
+ let maxZ = -Infinity;
271
+ for (const [x, z] of polygon) {
272
+ minX = Math.min(minX, x);
273
+ maxX = Math.max(maxX, x);
274
+ minZ = Math.min(minZ, z);
275
+ maxZ = Math.max(maxZ, z);
276
+ }
277
+ return [maxX - minX, maxZ - minZ];
278
+ }
279
+ /** Our three ray sources onto IWSDK's four. */
280
+ function raycastSpace(space) {
281
+ if (space === "left")
282
+ return RaycastSpace.Left;
283
+ if (space === "right")
284
+ return RaycastSpace.Right;
285
+ return RaycastSpace.Viewer;
286
+ }
287
+ //# sourceMappingURL=world-sensing-port.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"world-sensing-port.js","sourceRoot":"","sources":["../src/world-sensing-port.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `WorldSensingPort` for Meta IWSDK.\n *\n * ---------------------------------------------------------------------------\n * IWSDK HAS ALREADY DONE HALF OF THIS\n * ---------------------------------------------------------------------------\n * Where the three.js adapter reads an `XRFrame` directly, IWSDK's own\n * `SceneUnderstandingSystem` has already turned planes, meshes and anchors\n * into ENTITIES with components on them, positioned in the scene. So this port\n * queries for those entities and translates them, rather than reaching past\n * IWSDK to the session - which would fight the system that owns them.\n *\n * The same goes for hit testing: IWSDK's `EnvironmentRaycastSystem` casts from\n * a hand or the viewer and moves a target entity to the hit, so a request here\n * becomes an entity with an `EnvironmentRaycastTarget` on it and the answer is\n * read off that entity's transform.\n *\n * ---------------------------------------------------------------------------\n * WHAT IT CANNOT DO\n * ---------------------------------------------------------------------------\n * Ask for the features. `plane-detection`, `mesh-detection` and `anchors` are\n * session features, IWSDK requests them from its own world configuration, and\n * the session belongs to the platform layer either way. If the app did not ask\n * for them, the queries stay empty forever - so this port says `pending` and\n * keeps saying it, rather than pretending to have turned something on.\n */\nimport {\n EnvironmentRaycastSystem,\n EnvironmentRaycastTarget,\n Quaternion,\n RaycastSpace,\n Vector3,\n XRAnchor,\n XRMesh,\n XRPlane,\n type Entity,\n type World,\n} from \"@iwsdk/core\";\nimport type {\n HitTestRequest,\n ResolvedWorldDetection,\n SensingFeature,\n SensingReport,\n WorldAnchor,\n WorldHit,\n WorldMesh,\n WorldPlane,\n WorldPlaneOrientation,\n WorldPose,\n WorldSensingPort,\n WorldSensingPortHost,\n} from \"@realitycollective/webxr-environment\";\n\n/** The raw `XRPlane` IWSDK keeps on the component, as far as we read it. */\ninterface RawPlane {\n readonly polygon?: ArrayLike<{ readonly x: number; readonly z: number }>;\n readonly orientation?: string;\n readonly semanticLabel?: string;\n readonly lastChangedTime?: number;\n}\n\n/** One elics query, of which this port uses only the entity set. */\ninterface QueryLike {\n readonly entities: Iterable<Entity>;\n}\n\ninterface QueryManagerLike {\n registerQuery(config: { required: unknown[] }): QueryLike;\n}\n\nexport interface IWSDKWorldSensingPortOptions {\n /** Where hit-test target entities are parented. Default: no parent. */\n readonly parent?: Entity;\n}\n\nconst TEMP_POSITION = new Vector3();\nconst TEMP_QUATERNION = new Quaternion();\n\nexport class IWSDKWorldSensingPort implements WorldSensingPort {\n readonly #world: World;\n readonly #parent: Entity | undefined;\n readonly #ids = new WeakMap<object, string>();\n readonly #hitEntities = new Map<string, Entity>();\n readonly #anchorEntities = new Map<string, Entity>();\n readonly #states = new Map<SensingFeature, string>();\n\n /**\n * The three queries, registered once at construction.\n *\n * elics keeps one query per component mask and updates it as entities gain\n * and lose components, so registering up front costs one mask walk each and\n * removes any question of whether a set exists yet. An app that never turns\n * detection on simply never reads them.\n */\n readonly #planeQuery: QueryLike;\n readonly #meshQuery: QueryLike;\n readonly #anchorQuery: QueryLike;\n\n #host: WorldSensingPortHost | null = null;\n #detection: ResolvedWorldDetection | null = null;\n #nextId = 1;\n\n constructor(world: World, options: IWSDKWorldSensingPortOptions = {}) {\n this.#world = world;\n this.#parent = options.parent;\n const manager = (world as unknown as { queryManager: QueryManagerLike }).queryManager;\n this.#planeQuery = manager.registerQuery({ required: [XRPlane] });\n this.#meshQuery = manager.registerQuery({ required: [XRMesh] });\n this.#anchorQuery = manager.registerQuery({ required: [XRAnchor] });\n }\n\n observe(host: WorldSensingPortHost): () => void {\n this.#host = host;\n return () => {\n this.#host = null;\n };\n }\n\n setDetection(detection: ResolvedWorldDetection | null): void {\n this.#detection = detection;\n if (detection === null) {\n this.#host?.planes([]);\n this.#host?.meshes([]);\n this.#host?.anchors([]);\n for (const feature of [\"planes\", \"meshes\", \"anchors\"] as const) {\n this.#report({ feature, state: \"unavailable\", detail: \"detection is off\" });\n }\n return;\n }\n for (const feature of [\"planes\", \"meshes\", \"anchors\"] as const) {\n this.#report(\n detection[feature]\n ? {\n feature,\n state: \"pending\",\n detail: \"waiting for IWSDK's scene understanding to find something\",\n }\n : { feature, state: \"unavailable\", detail: \"detection is off\" },\n );\n }\n }\n\n startHitTest(request: HitTestRequest): void {\n this.#world.registerSystem(EnvironmentRaycastSystem);\n const entity = this.#world.createTransformEntity(\n undefined,\n this.#parent === undefined ? undefined : { parent: this.#parent },\n );\n entity.addComponent(EnvironmentRaycastTarget, { space: raycastSpace(request.space) });\n this.#hitEntities.set(request.id, entity);\n this.#report({\n feature: \"hitTest\",\n state: \"pending\",\n detail: \"waiting for a hit from the \" + request.space + \" ray\",\n });\n }\n\n stopHitTest(id: string): void {\n const entity = this.#hitEntities.get(id);\n if (entity === undefined) return;\n this.#hitEntities.delete(id);\n entity.destroy();\n }\n\n /**\n * IWSDK anchors an ENTITY rather than handing back an anchor object, so this\n * makes one at the requested pose and lets `SceneUnderstandingSystem` attach\n * it. The id that comes back is the same id the anchor will report with.\n */\n async createAnchor(pose: WorldPose): Promise<string | null> {\n const entity = this.#world.createTransformEntity(\n undefined,\n this.#parent === undefined ? undefined : { parent: this.#parent },\n );\n const object3D = entity.object3D;\n if (object3D === undefined) {\n entity.destroy();\n this.#report({\n feature: \"anchors\",\n state: \"unavailable\",\n detail: \"the anchor entity has no transform to place\",\n });\n return null;\n }\n object3D.position.set(pose.position[0], pose.position[1], pose.position[2]);\n object3D.quaternion.set(\n pose.orientation[0],\n pose.orientation[1],\n pose.orientation[2],\n pose.orientation[3],\n );\n object3D.updateMatrixWorld();\n entity.addComponent(XRAnchor);\n const id = this.#idFor(entity as unknown as object);\n this.#anchorEntities.set(id, entity);\n return id;\n }\n\n removeAnchor(id: string): void {\n const entity = this.#anchorEntities.get(id);\n if (entity === undefined) return;\n this.#anchorEntities.delete(id);\n entity.destroy();\n }\n\n update(): void {\n const detection = this.#detection;\n if (detection === null || this.#host === null) return;\n if (detection.planes) this.#readPlanes();\n if (detection.meshes) this.#readMeshes();\n if (detection.anchors) this.#readAnchors();\n this.#readHits();\n }\n\n dispose(): void {\n for (const entity of this.#hitEntities.values()) entity.destroy();\n this.#hitEntities.clear();\n this.#anchorEntities.clear();\n this.#host = null;\n }\n\n #readPlanes(): void {\n const planes: WorldPlane[] = [];\n for (const entity of this.#planeQuery.entities) {\n const pose = poseOf(entity);\n if (pose === null) continue;\n const raw = entity.getValue(XRPlane, \"_plane\") as RawPlane | undefined;\n const polygon = (raw?.polygon === undefined ? [] : Array.from(raw.polygon)).map(\n (point) => [point.x, point.z] as const,\n );\n planes.push({\n id: this.#idFor(entity as unknown as object),\n pose,\n extents: extentsOf(polygon),\n orientation: orientationOf(raw?.orientation),\n label: raw?.semanticLabel?.toLowerCase() ?? null,\n ...(polygon.length === 0 ? {} : { polygon }),\n ...(raw?.lastChangedTime === undefined ? {} : { changedAt: raw.lastChangedTime }),\n });\n }\n this.#host?.planes(planes);\n this.#report({ feature: \"planes\", state: \"active\", detail: planes.length + \" surfaces\" });\n }\n\n #readMeshes(): void {\n const meshes: WorldMesh[] = [];\n for (const entity of this.#meshQuery.entities) {\n const pose = poseOf(entity);\n if (pose === null) continue;\n // IWSDK measures the mesh for us and puts the size on the component, so\n // unlike the raw WebXR path there are no vertices to walk here.\n const dimensions = entity.getVectorView(XRMesh, \"dimensions\");\n const label = entity.getValue(XRMesh, \"semanticLabel\");\n meshes.push({\n id: this.#idFor(entity as unknown as object),\n pose,\n // A `Types.Vec3` view always has three components; the assertions are\n // there because `noUncheckedIndexedAccess` cannot know that, and a\n // fallback would be a branch no input can reach.\n extents: [dimensions[0] as number, dimensions[1] as number, dimensions[2] as number],\n label: typeof label === \"string\" && label.length > 0 ? label.toLowerCase() : null,\n });\n }\n this.#host?.meshes(meshes);\n this.#report({ feature: \"meshes\", state: \"active\", detail: meshes.length + \" meshes\" });\n }\n\n #readAnchors(): void {\n const anchors: WorldAnchor[] = [];\n for (const entity of this.#anchorQuery.entities) {\n const pose = poseOf(entity);\n if (pose === null) continue;\n anchors.push({\n id: this.#idFor(entity as unknown as object),\n pose,\n // IWSDK's own flag: false until the system has attached the entity to\n // a real anchor, which is exactly what \"not tracked yet\" means here.\n tracked: entity.getValue(XRAnchor, \"attached\") === true,\n });\n }\n this.#host?.anchors(anchors);\n this.#report({ feature: \"anchors\", state: \"active\", detail: anchors.length + \" anchors\" });\n }\n\n #readHits(): void {\n for (const [id, entity] of this.#hitEntities) {\n const result = entity.getValue(EnvironmentRaycastTarget, \"xrHitTestResult\");\n const pose = poseOf(entity);\n const hits: WorldHit[] =\n result === undefined || result === null || pose === null\n ? []\n : [{ sourceId: id, pose, distance: null }];\n this.#host?.hits(id, hits);\n if (hits.length > 0) {\n this.#report({ feature: \"hitTest\", state: \"active\", detail: \"hitting the room\" });\n }\n }\n }\n\n #idFor(object: object): string {\n const existing = this.#ids.get(object);\n if (existing !== undefined) return existing;\n const id = \"world-\" + String(this.#nextId);\n this.#nextId += 1;\n this.#ids.set(object, id);\n return id;\n }\n\n #report(report: SensingReport): void {\n const stamp = report.state + \"|\" + report.detail;\n if (this.#states.get(report.feature) === stamp) return;\n this.#states.set(report.feature, stamp);\n this.#host?.report(report);\n }\n}\n\n/** The world transform of the entity IWSDK positioned for us. */\nfunction poseOf(entity: Entity): WorldPose | null {\n const object3D = entity.object3D;\n if (object3D === undefined) return null;\n object3D.getWorldPosition(TEMP_POSITION);\n object3D.getWorldQuaternion(TEMP_QUATERNION);\n return {\n position: [TEMP_POSITION.x, TEMP_POSITION.y, TEMP_POSITION.z],\n orientation: [TEMP_QUATERNION.x, TEMP_QUATERNION.y, TEMP_QUATERNION.z, TEMP_QUATERNION.w],\n };\n}\n\nfunction orientationOf(value: string | undefined): WorldPlaneOrientation {\n const lowered = value?.toLowerCase();\n if (lowered === \"horizontal\" || lowered === \"vertical\") return lowered;\n return \"unknown\";\n}\n\nfunction extentsOf(polygon: readonly (readonly [number, number])[]): readonly [number, number] {\n if (polygon.length === 0) return [0, 0];\n let minX = Infinity;\n let maxX = -Infinity;\n let minZ = Infinity;\n let maxZ = -Infinity;\n for (const [x, z] of polygon) {\n minX = Math.min(minX, x);\n maxX = Math.max(maxX, x);\n minZ = Math.min(minZ, z);\n maxZ = Math.max(maxZ, z);\n }\n return [maxX - minX, maxZ - minZ];\n}\n\n/** Our three ray sources onto IWSDK's four. */\nfunction raycastSpace(space: HitTestRequest[\"space\"]): string {\n if (space === \"left\") return RaycastSpace.Left;\n if (space === \"right\") return RaycastSpace.Right;\n return RaycastSpace.Viewer;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@realitycollective/iwsdk-environment",
3
- "version": "0.1.0-preview.0",
3
+ "version": "0.1.0-preview.1",
4
4
  "description": "Meta IWSDK adapter for the Reality Collective WebXR Environment Extensions - drives IWSDK's own DomeGradient, light components, transform entities and AudioSource machinery from the engine-free @realitycollective/webxr-environment core, with a one-call registerEnvironment(world) setup and an ECS system that ticks the directors. Re-exports the core.",
5
5
  "keywords": [
6
6
  "realitycollective",
@@ -35,14 +35,14 @@
35
35
  "typecheck": "tsc -p tsconfig.json --noEmit"
36
36
  },
37
37
  "dependencies": {
38
- "@realitycollective/webxr-environment": "^0.1.0-preview.0"
38
+ "@realitycollective/webxr-environment": "^0.1.0-preview.1"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "@iwsdk/core": ">=0.5.0 <0.6.0"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@iwsdk/core": "0.5.3",
45
- "three": "npm:super-three@0.181.0",
45
+ "three": "npm:super-three@0.185.0",
46
46
  "@types/three": "*"
47
47
  },
48
48
  "repository": {