@realitycollective/iwsdk-environment 0.1.0-preview.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,22 @@
1
+ # Changelog
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.
4
+
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
+
7
+ ## [0.1.0]
8
+
9
+ ### Added
10
+
11
+ - `@realitycollective/webxr-environment` - the engine-free core. `EnvironmentDirector` holds a sky (gradient or solid), fog (linear or exponential), an ambient light and a key light as plain data; interpolates between named presets over a host-supplied `update(deltaMs)`; pushes only the slots that actually changed to an `EnvironmentPort`; and applies passthrough as a suppression over what the app asked for, so nothing has to remember what to restore. A partial `EnvironmentSpec` inherits the slots it omits and turns off the ones it sets to `null`, which is what makes presets composable. Six example presets (`void`, `dawn`, `noon`, `dusk`, `night`, `overcast`) to copy, and `clearedFog`, the helper that makes fog easeable in.
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
+ - `@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
+ - `@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
+ - `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
+
17
+ ### Notes
18
+
19
+ - 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.
20
+ - 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
+
22
+ [0.1.0]: https://github.com/realitycollective/WebXR-Environment/commits/main
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Reality Collective
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,67 @@
1
+ # @realitycollective/iwsdk-environment
2
+
3
+ The **Meta IWSDK adapter** for the Reality Collective WebXR Environment Extensions. Re-exports the engine-free [`@realitycollective/webxr-environment`](https://www.npmjs.com/package/@realitycollective/webxr-environment) core, so this is the only package you install.
4
+
5
+ ```bash
6
+ npm install @realitycollective/iwsdk-environment
7
+ ```
8
+
9
+ ## Use
10
+
11
+ Setup is one call, and it registers the system that ticks the directors:
12
+
13
+ ```ts
14
+ import {
15
+ registerEnvironment,
16
+ STOCK_PRESETS,
17
+ VOID,
18
+ } from "@realitycollective/iwsdk-environment";
19
+
20
+ const env = registerEnvironment(world, {
21
+ presets: STOCK_PRESETS,
22
+ initial: VOID,
23
+ audio: {
24
+ cues: [{ id: "hum", src: "/audio/hum.mp3", bus: "ambience", loop: true }],
25
+ },
26
+ });
27
+
28
+ env.environment.transition("dusk", { durationMs: 8000 });
29
+ ```
30
+
31
+ Because the tick is an IWSDK system, the environment stops advancing when the session loses focus, exactly like the rest of the app. IWSDK hands systems a delta in **seconds** and the directors take **milliseconds**; that conversion happens once, inside the system.
32
+
33
+ ## What the adapter does
34
+
35
+ It drives IWSDK's own machinery rather than reaching past it to three.js:
36
+
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
+ - **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.
40
+
41
+ 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
+
43
+ **Fog is the exception.** IWSDK has no fog component, so it is set on `world.scene` directly, with `Fog` / `FogExp2` imported from `@iwsdk/core` (one three.js instance, reached through the framework's re-export).
44
+
45
+ ## Knowing when a sound finished
46
+
47
+ IWSDK reports whether a source *is* playing, never that it has just stopped. So the port polls in the tick it is already given, and reports the end once a voice has been seen playing and then is not. A voice that never starts at all - a missing file, a failed decode - is reaped after `startTimeoutMs` (10 s by default) with a warning, rather than being tracked forever.
48
+
49
+ ## Passthrough
50
+
51
+ Push it in; do not expect the package to find it:
52
+
53
+ ```ts
54
+ // `adapter` is the service framework's RuntimeAdapter; the flag it reports is
55
+ // derived from the LIVE session, not from what was requested.
56
+ adapter.onCapabilitiesChange((c) => env.environment.setPassthrough(c.passthrough));
57
+ ```
58
+
59
+ A session belongs to the platform layer, not to the environment. `setPassthrough(true)` suppresses the sky and fog on top of whatever the app asked for, and turning it off restores exactly what was there - nothing has to remember what to put back.
60
+
61
+ ## Peer dependency
62
+
63
+ `@iwsdk/core >= 0.5.0 < 0.6.0`, developed and tested against 0.5.3.
64
+
65
+ ## Licence
66
+
67
+ MIT.
@@ -0,0 +1,44 @@
1
+ /**
2
+ * `AudioPort` for Meta IWSDK, over `AudioSource` + `AudioUtils`.
3
+ *
4
+ * ---------------------------------------------------------------------------
5
+ * ONE ENTITY PER VOICE
6
+ * ---------------------------------------------------------------------------
7
+ * IWSDK's audio is a component on an entity, with its own pooling and its own
8
+ * retrigger policy per source. Reusing one entity per CUE would hand those
9
+ * decisions back to IWSDK and make `restart` and `ignore` mean something
10
+ * different here than on three.js, which is the divergence this stack exists
11
+ * to remove. So a voice is an entity: the director's policy wins, every voice
12
+ * is individually stoppable, and IWSDK's own `playbackMode` is pinned to
13
+ * `Overlap` so it never second-guesses the core.
14
+ *
15
+ * ---------------------------------------------------------------------------
16
+ * KNOWING WHEN A SOUND FINISHED
17
+ * ---------------------------------------------------------------------------
18
+ * IWSDK reports whether a source IS playing, never that it has just stopped.
19
+ * So this port polls in `update()` - which the director already calls - and
20
+ * reports the end once a voice has been observed playing and then is not.
21
+ * A voice that never starts at all (a missing file, a decode failure) would
22
+ * otherwise be tracked forever, so it is reaped after `startTimeoutMs`.
23
+ */
24
+ import { type Entity, type World } from "@iwsdk/core";
25
+ import type { AudioPort, AudioVoiceRequest } from "@realitycollective/webxr-environment";
26
+ export interface IWSDKAudioPortOptions {
27
+ /** Parent for the voice entities. Defaults to the world's default parent. */
28
+ readonly parent?: Entity;
29
+ /**
30
+ * How long to wait for a voice to start before giving up on it,
31
+ * milliseconds. Default 10 000 - generous, because a cold asset fetch on a
32
+ * headset over hotel wifi is slower than anyone's patience.
33
+ */
34
+ readonly startTimeoutMs?: number;
35
+ }
36
+ export declare class IWSDKAudioPort implements AudioPort {
37
+ #private;
38
+ constructor(world: World, options?: IWSDKAudioPortOptions);
39
+ start(request: AudioVoiceRequest): void;
40
+ stop(voiceId: number): void;
41
+ setGain(voiceId: number, gain: number): void;
42
+ update(deltaMs: number): void;
43
+ dispose(): void;
44
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * `AudioPort` for Meta IWSDK, over `AudioSource` + `AudioUtils`.
3
+ *
4
+ * ---------------------------------------------------------------------------
5
+ * ONE ENTITY PER VOICE
6
+ * ---------------------------------------------------------------------------
7
+ * IWSDK's audio is a component on an entity, with its own pooling and its own
8
+ * retrigger policy per source. Reusing one entity per CUE would hand those
9
+ * decisions back to IWSDK and make `restart` and `ignore` mean something
10
+ * different here than on three.js, which is the divergence this stack exists
11
+ * to remove. So a voice is an entity: the director's policy wins, every voice
12
+ * is individually stoppable, and IWSDK's own `playbackMode` is pinned to
13
+ * `Overlap` so it never second-guesses the core.
14
+ *
15
+ * ---------------------------------------------------------------------------
16
+ * KNOWING WHEN A SOUND FINISHED
17
+ * ---------------------------------------------------------------------------
18
+ * IWSDK reports whether a source IS playing, never that it has just stopped.
19
+ * So this port polls in `update()` - which the director already calls - and
20
+ * reports the end once a voice has been observed playing and then is not.
21
+ * A voice that never starts at all (a missing file, a decode failure) would
22
+ * otherwise be tracked forever, so it is reaped after `startTimeoutMs`.
23
+ */
24
+ import { AudioSource, AudioUtils, PlaybackMode, Transform } from "@iwsdk/core";
25
+ export class IWSDKAudioPort {
26
+ #world;
27
+ #parent;
28
+ #startTimeoutMs;
29
+ #voices = new Map();
30
+ #disposed = false;
31
+ constructor(world, options = {}) {
32
+ this.#world = world;
33
+ this.#parent = options.parent;
34
+ this.#startTimeoutMs = options.startTimeoutMs ?? 10_000;
35
+ }
36
+ start(request) {
37
+ if (this.#disposed) {
38
+ request.ended();
39
+ return;
40
+ }
41
+ const entity = this.#world.createTransformEntity(undefined, this.#parent === undefined ? undefined : { parent: this.#parent });
42
+ const positional = request.at !== null || (request.cue.positional ?? false);
43
+ if (request.at !== null) {
44
+ const view = entity.getVectorView(Transform, "position");
45
+ view[0] = request.at[0];
46
+ view[1] = request.at[1];
47
+ view[2] = request.at[2];
48
+ }
49
+ entity.addComponent(AudioSource, {
50
+ src: request.cue.src,
51
+ volume: request.gain,
52
+ loop: request.loop,
53
+ positional,
54
+ autoplay: false,
55
+ // The core already applied the cue's policy before we got here.
56
+ playbackMode: PlaybackMode.Overlap,
57
+ });
58
+ this.#voices.set(request.voiceId, { entity, request, started: false, waitedMs: 0 });
59
+ AudioUtils.play(entity);
60
+ }
61
+ stop(voiceId) {
62
+ const voice = this.#voices.get(voiceId);
63
+ if (voice === undefined)
64
+ return;
65
+ this.#voices.delete(voiceId);
66
+ this.#retire(voice);
67
+ }
68
+ setGain(voiceId, gain) {
69
+ const voice = this.#voices.get(voiceId);
70
+ if (voice === undefined)
71
+ return;
72
+ AudioUtils.setVolume(voice.entity, gain);
73
+ }
74
+ update(deltaMs) {
75
+ if (this.#disposed || this.#voices.size === 0)
76
+ return;
77
+ for (const [id, voice] of [...this.#voices]) {
78
+ // A looping voice ends when someone stops it, never on its own.
79
+ if (voice.request.loop)
80
+ continue;
81
+ const playing = AudioUtils.isPlaying(voice.entity);
82
+ if (playing) {
83
+ voice.started = true;
84
+ continue;
85
+ }
86
+ if (!voice.started) {
87
+ voice.waitedMs += deltaMs;
88
+ if (voice.waitedMs < this.#startTimeoutMs)
89
+ continue;
90
+ console.warn(`[iwsdk-environment] cue "${voice.request.cue.id}" never started (${voice.request.cue.src})`);
91
+ }
92
+ this.#voices.delete(id);
93
+ this.#retire(voice);
94
+ voice.request.ended();
95
+ }
96
+ }
97
+ dispose() {
98
+ if (this.#disposed)
99
+ return;
100
+ this.#disposed = true;
101
+ for (const [id, voice] of [...this.#voices]) {
102
+ this.#voices.delete(id);
103
+ this.#retire(voice);
104
+ }
105
+ }
106
+ #retire(voice) {
107
+ AudioUtils.stop(voice.entity);
108
+ voice.entity.destroy();
109
+ }
110
+ }
111
+ //# sourceMappingURL=audio-port.js.map
@@ -0,0 +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"]}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * `EnvironmentPort` for Meta IWSDK.
3
+ *
4
+ * ---------------------------------------------------------------------------
5
+ * IT USES IWSDK'S OWN MACHINERY, NOT THREE.JS BEHIND ITS BACK
6
+ * ---------------------------------------------------------------------------
7
+ * The sky is a `DomeGradient` on the level root and the lights are
8
+ * `AmbientLightComponent` and `DirectionalLightComponent` on entities.
9
+ * Reaching past IWSDK to write `scene.background` or to `scene.add()` a light
10
+ * would work right up until IWSDK's own `EnvironmentSystem` or `LightSystem`
11
+ * disagreed - and one of the things IWSDK's environment system already does
12
+ * correctly is hide authored backgrounds in an AR session, which is behaviour
13
+ * worth inheriting rather than fighting.
14
+ *
15
+ * No geometry is created here. Every component this port touches is an IWSDK
16
+ * platform facility with no app-side equivalent; a floor would be a mesh, and
17
+ * meshes are the app's.
18
+ *
19
+ * Fog is the exception. IWSDK has no fog component, so it is set on
20
+ * `world.scene` directly, with three.js's own `Fog` / `FogExp2` imported from
21
+ * `@iwsdk/core` (the framework's rule - one three.js instance, reached through
22
+ * the framework's re-export).
23
+ *
24
+ * ---------------------------------------------------------------------------
25
+ * WRITING COMPONENT COLOURS
26
+ * ---------------------------------------------------------------------------
27
+ * A `Types.Color` or `Types.Vec3` field must be written through
28
+ * `getVectorView`; `setValue` on one throws. Every colour write here goes
29
+ * through `writeColour` for that reason - it is not a style choice.
30
+ */
31
+ import { type Entity, type World } from "@iwsdk/core";
32
+ import type { AmbientLightSpec, EnvironmentPort, FogSpec, KeyLightSpec, SkySpec } from "@realitycollective/webxr-environment";
33
+ export interface IWSDKEnvironmentPortOptions {
34
+ /**
35
+ * Where the light entities are parented. Defaults to the active level root,
36
+ * which is also where the dome lives, so the whole environment is torn down
37
+ * with the level.
38
+ */
39
+ readonly parent?: Entity;
40
+ /** Dome brightness multiplier. Default 1. */
41
+ readonly skyIntensity?: number;
42
+ }
43
+ export declare class IWSDKEnvironmentPort implements EnvironmentPort {
44
+ #private;
45
+ constructor(world: World, options?: IWSDKEnvironmentPortOptions);
46
+ applySky(sky: SkySpec | null): void;
47
+ applyFog(fog: FogSpec | null): void;
48
+ applyAmbient(light: AmbientLightSpec | null): void;
49
+ applyKeyLight(light: KeyLightSpec | null): void;
50
+ dispose(): void;
51
+ }
@@ -0,0 +1,207 @@
1
+ /**
2
+ * `EnvironmentPort` for Meta IWSDK.
3
+ *
4
+ * ---------------------------------------------------------------------------
5
+ * IT USES IWSDK'S OWN MACHINERY, NOT THREE.JS BEHIND ITS BACK
6
+ * ---------------------------------------------------------------------------
7
+ * The sky is a `DomeGradient` on the level root and the lights are
8
+ * `AmbientLightComponent` and `DirectionalLightComponent` on entities.
9
+ * Reaching past IWSDK to write `scene.background` or to `scene.add()` a light
10
+ * would work right up until IWSDK's own `EnvironmentSystem` or `LightSystem`
11
+ * disagreed - and one of the things IWSDK's environment system already does
12
+ * correctly is hide authored backgrounds in an AR session, which is behaviour
13
+ * worth inheriting rather than fighting.
14
+ *
15
+ * No geometry is created here. Every component this port touches is an IWSDK
16
+ * platform facility with no app-side equivalent; a floor would be a mesh, and
17
+ * meshes are the app's.
18
+ *
19
+ * Fog is the exception. IWSDK has no fog component, so it is set on
20
+ * `world.scene` directly, with three.js's own `Fog` / `FogExp2` imported from
21
+ * `@iwsdk/core` (the framework's rule - one three.js instance, reached through
22
+ * the framework's re-export).
23
+ *
24
+ * ---------------------------------------------------------------------------
25
+ * WRITING COMPONENT COLOURS
26
+ * ---------------------------------------------------------------------------
27
+ * A `Types.Color` or `Types.Vec3` field must be written through
28
+ * `getVectorView`; `setValue` on one throws. Every colour write here goes
29
+ * through `writeColour` for that reason - it is not a style choice.
30
+ */
31
+ import { AmbientLightComponent, DirectionalLightComponent, DomeGradient, Fog, FogExp2, Quaternion, Transform, Vector3, } from "@iwsdk/core";
32
+ import { clamp01, lerpRgb } from "@realitycollective/webxr-environment";
33
+ /** Local -Z is where an IWSDK directional light points. */
34
+ const LIGHT_FORWARD = new Vector3(0, 0, -1);
35
+ const TEMP_DIRECTION = new Vector3();
36
+ const TEMP_QUATERNION = new Quaternion();
37
+ export class IWSDKEnvironmentPort {
38
+ #world;
39
+ #parent;
40
+ #skyIntensity;
41
+ #domeEntity = null;
42
+ #ambientEntity = null;
43
+ #keyEntity = null;
44
+ #fogKind = null;
45
+ constructor(world, options = {}) {
46
+ this.#world = world;
47
+ this.#parent = options.parent;
48
+ this.#skyIntensity = options.skyIntensity ?? 1;
49
+ }
50
+ applySky(sky) {
51
+ const root = this.#levelRoot();
52
+ if (root === null)
53
+ return;
54
+ if (sky === null) {
55
+ if (this.#domeEntity !== null && this.#domeEntity.hasComponent(DomeGradient)) {
56
+ this.#domeEntity.removeComponent(DomeGradient);
57
+ }
58
+ this.#domeEntity = null;
59
+ return;
60
+ }
61
+ if (this.#domeEntity !== root || !root.hasComponent(DomeGradient)) {
62
+ if (!root.hasComponent(DomeGradient))
63
+ root.addComponent(DomeGradient);
64
+ this.#domeEntity = root;
65
+ }
66
+ // 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
69
+ // rather than only at the poles.
70
+ if (sky.kind === "solid") {
71
+ writeRgb(root.getVectorView(DomeGradient, "sky"), sky.colour);
72
+ writeRgb(root.getVectorView(DomeGradient, "equator"), sky.colour);
73
+ writeRgb(root.getVectorView(DomeGradient, "ground"), sky.colour);
74
+ }
75
+ else {
76
+ const equator = lerpRgb(sky.bottom, sky.top, Math.pow(0.5, sky.exponent ?? 1));
77
+ writeRgb(root.getVectorView(DomeGradient, "sky"), sky.top);
78
+ writeRgb(root.getVectorView(DomeGradient, "equator"), equator);
79
+ writeRgb(root.getVectorView(DomeGradient, "ground"), sky.bottom);
80
+ }
81
+ root.setValue(DomeGradient, "intensity", this.#skyIntensity);
82
+ root.setValue(DomeGradient, "_needsUpdate", true);
83
+ }
84
+ applyFog(fog) {
85
+ const scene = this.#world.scene;
86
+ if (fog === null) {
87
+ scene.fog = null;
88
+ this.#fogKind = null;
89
+ return;
90
+ }
91
+ if (this.#fogKind !== fog.kind || scene.fog === null) {
92
+ scene.fog =
93
+ fog.kind === "linear"
94
+ ? new Fog(rgbToHexNumber(fog.colour), fog.near, fog.far)
95
+ : new FogExp2(rgbToHexNumber(fog.colour), fog.density);
96
+ this.#fogKind = fog.kind;
97
+ return;
98
+ }
99
+ const existing = scene.fog;
100
+ existing.color.setHex(rgbToHexNumber(fog.colour));
101
+ if (fog.kind === "linear" && existing instanceof Fog) {
102
+ existing.near = fog.near;
103
+ existing.far = fog.far;
104
+ }
105
+ else if (fog.kind === "exponential" && existing instanceof FogExp2) {
106
+ existing.density = fog.density;
107
+ }
108
+ }
109
+ applyAmbient(light) {
110
+ if (light === null) {
111
+ this.#destroy("ambient");
112
+ return;
113
+ }
114
+ const entity = this.#ensure("ambient");
115
+ if (!entity.hasComponent(AmbientLightComponent)) {
116
+ entity.addComponent(AmbientLightComponent);
117
+ }
118
+ writeRgb(entity.getVectorView(AmbientLightComponent, "color"), light.colour);
119
+ entity.setValue(AmbientLightComponent, "intensity", light.intensity);
120
+ }
121
+ applyKeyLight(light) {
122
+ if (light === null) {
123
+ this.#destroy("key");
124
+ return;
125
+ }
126
+ const entity = this.#ensure("key");
127
+ if (!entity.hasComponent(DirectionalLightComponent)) {
128
+ entity.addComponent(DirectionalLightComponent);
129
+ }
130
+ writeRgb(entity.getVectorView(DirectionalLightComponent, "color"), light.colour);
131
+ entity.setValue(DirectionalLightComponent, "intensity", light.intensity);
132
+ entity.setValue(DirectionalLightComponent, "castShadow", light.castShadow ?? false);
133
+ // The spec names the direction light TRAVELS; the component emits along
134
+ // the entity's local -Z, so the entity is rotated to face that way. A
135
+ // zero-length direction would produce a NaN quaternion, so it falls back
136
+ // to straight down.
137
+ const [x, y, z] = light.direction;
138
+ if (Math.hypot(x, y, z) === 0)
139
+ TEMP_DIRECTION.set(0, -1, 0);
140
+ else
141
+ TEMP_DIRECTION.set(x, y, z).normalize();
142
+ TEMP_QUATERNION.setFromUnitVectors(LIGHT_FORWARD, TEMP_DIRECTION);
143
+ writeQuaternion(entity.getVectorView(Transform, "orientation"), TEMP_QUATERNION);
144
+ }
145
+ dispose() {
146
+ this.applySky(null);
147
+ this.applyFog(null);
148
+ this.applyAmbient(null);
149
+ this.applyKeyLight(null);
150
+ }
151
+ #levelRoot() {
152
+ return this.#parent ?? this.#world.activeLevel?.value ?? null;
153
+ }
154
+ /** The light entities exist independently of the level, so this cannot fail. */
155
+ #ensure(which) {
156
+ const existing = which === "ambient" ? this.#ambientEntity : this.#keyEntity;
157
+ if (existing !== null)
158
+ return existing;
159
+ const parent = this.#levelRoot();
160
+ const entity = this.#world.createTransformEntity(undefined, parent === null ? undefined : { parent });
161
+ if (which === "ambient")
162
+ this.#ambientEntity = entity;
163
+ else
164
+ this.#keyEntity = entity;
165
+ return entity;
166
+ }
167
+ #destroy(which) {
168
+ const entity = which === "ambient" ? this.#ambientEntity : this.#keyEntity;
169
+ entity?.destroy();
170
+ if (which === "ambient")
171
+ this.#ambientEntity = null;
172
+ else
173
+ this.#keyEntity = null;
174
+ }
175
+ }
176
+ /** `[r, g, b]` in 0..1 -> a packed 0xRRGGBB, for the three.js-side setters. */
177
+ function rgbToHexNumber(rgb) {
178
+ const channel = (value) => Math.round(clamp01(value) * 255);
179
+ return (channel(rgb[0]) << 16) | (channel(rgb[1]) << 8) | channel(rgb[2]);
180
+ }
181
+ /**
182
+ * Write an RGBA colour into a component's vector view.
183
+ *
184
+ * The VIEW is passed in rather than the component and field name, so every
185
+ * call site keeps elics's own `getVectorView` typing - which checks that the
186
+ * field exists AND that it is an array type. A helper that took the component
187
+ * would have to erase those generics, and the first misspelled field name
188
+ * would then reach a headset instead of the compiler.
189
+ *
190
+ * Alpha is always 1: the specs describe opaque colours. Writing index 3 of a
191
+ * three-component view is a no-op in JavaScript rather than an error, so no
192
+ * length check is needed for a component whose colour is a `Types.Vec3`.
193
+ */
194
+ function writeRgb(view, rgb) {
195
+ view[0] = rgb[0];
196
+ view[1] = rgb[1];
197
+ view[2] = rgb[2];
198
+ view[3] = 1;
199
+ }
200
+ /** As above, for a `Types.Vec4` orientation. */
201
+ function writeQuaternion(view, quaternion) {
202
+ view[0] = quaternion.x;
203
+ view[1] = quaternion.y;
204
+ view[2] = quaternion.z;
205
+ view[3] = quaternion.w;
206
+ }
207
+ //# sourceMappingURL=environment-port.js.map
@@ -0,0 +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"]}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * `@realitycollective/iwsdk-environment` - the Meta IWSDK adapter.
3
+ *
4
+ * Re-exports the whole engine-free core, so an app installs this package and
5
+ * nothing else.
6
+ */
7
+ import { type Entity, type World } from "@iwsdk/core";
8
+ import type { AudioDirectorOptions, EnvironmentDirectorOptions } from "@realitycollective/webxr-environment";
9
+ import { AudioDirector, EnvironmentDirector } from "@realitycollective/webxr-environment";
10
+ import type { IWSDKEnvironmentPortOptions } from "./environment-port.js";
11
+ import { IWSDKEnvironmentPort } from "./environment-port.js";
12
+ import type { IWSDKAudioPortOptions } from "./audio-port.js";
13
+ import { IWSDKAudioPort } from "./audio-port.js";
14
+ export type { IWSDKEnvironmentPortOptions } from "./environment-port.js";
15
+ export { IWSDKEnvironmentPort } from "./environment-port.js";
16
+ export type { IWSDKAudioPortOptions } from "./audio-port.js";
17
+ export { IWSDKAudioPort } from "./audio-port.js";
18
+ export * from "@realitycollective/webxr-environment";
19
+ export interface IWSDKEnvironmentOptions extends EnvironmentDirectorOptions, IWSDKEnvironmentPortOptions {
20
+ /** Audio director + port options. Omit to set up the environment only. */
21
+ readonly audio?: AudioDirectorOptions & IWSDKAudioPortOptions;
22
+ }
23
+ export interface IWSDKEnvironmentSetup {
24
+ readonly environment: EnvironmentDirector;
25
+ readonly environmentPort: IWSDKEnvironmentPort;
26
+ readonly audio: AudioDirector | null;
27
+ readonly audioPort: IWSDKAudioPort | null;
28
+ /** Tear both directors down and remove everything they created. */
29
+ dispose(): void;
30
+ }
31
+ /** The setup registered for a world, if `registerEnvironment` has run. */
32
+ export declare function environmentFor(world: World): IWSDKEnvironmentSetup | undefined;
33
+ declare const EnvironmentSystem_base: import("elics").SystemConstructor<import("elics").SystemSchema, {}, World, import("@iwsdk/core").System<import("elics").SystemSchema, {}>>;
34
+ /**
35
+ * The per-frame tick.
36
+ *
37
+ * Neither director owns a loop, so something has to drive them, and on IWSDK
38
+ * that something is a system - which means the environment stops advancing
39
+ * when the session loses focus, exactly like the rest of the app.
40
+ * `registerEnvironment` registers this for you; it is exported so an app that
41
+ * builds its own system schedule can place it deliberately.
42
+ *
43
+ * IWSDK hands a system its delta in SECONDS and the directors take
44
+ * milliseconds. That conversion happens here, once.
45
+ */
46
+ export declare class EnvironmentSystem extends EnvironmentSystem_base {
47
+ update(delta: number): void;
48
+ }
49
+ /**
50
+ * One call to wire the environment (and optionally the audio) into an IWSDK
51
+ * world, and register the system that ticks them.
52
+ *
53
+ * ```ts
54
+ * const env = registerEnvironment(world, {
55
+ * presets: STOCK_PRESETS,
56
+ * initial: VOID,
57
+ * audio: { cues: [{ id: "hum", src: "/audio/hum.mp3", bus: "ambience", loop: true }] },
58
+ * });
59
+ * env.environment.transition("dusk", { durationMs: 8000 });
60
+ * ```
61
+ *
62
+ * Passthrough is NOT read from the session here - push it in from whatever
63
+ * already tracks capabilities (`env.environment.setPassthrough(...)`). The
64
+ * session belongs to the platform layer, not to the environment.
65
+ */
66
+ export declare function registerEnvironment(world: World, options?: IWSDKEnvironmentOptions): IWSDKEnvironmentSetup;
67
+ export type { Entity as IWSDKEntity };
package/dist/index.js ADDED
@@ -0,0 +1,85 @@
1
+ /**
2
+ * `@realitycollective/iwsdk-environment` - the Meta IWSDK adapter.
3
+ *
4
+ * Re-exports the whole engine-free core, so an app installs this package and
5
+ * nothing else.
6
+ */
7
+ import { createSystem } from "@iwsdk/core";
8
+ import { AudioDirector, EnvironmentDirector } from "@realitycollective/webxr-environment";
9
+ import { IWSDKEnvironmentPort } from "./environment-port.js";
10
+ import { IWSDKAudioPort } from "./audio-port.js";
11
+ export { IWSDKEnvironmentPort } from "./environment-port.js";
12
+ export { IWSDKAudioPort } from "./audio-port.js";
13
+ export * from "@realitycollective/webxr-environment";
14
+ const hosts = new WeakMap();
15
+ /** The setup registered for a world, if `registerEnvironment` has run. */
16
+ export function environmentFor(world) {
17
+ return hosts.get(world);
18
+ }
19
+ /**
20
+ * The per-frame tick.
21
+ *
22
+ * Neither director owns a loop, so something has to drive them, and on IWSDK
23
+ * that something is a system - which means the environment stops advancing
24
+ * when the session loses focus, exactly like the rest of the app.
25
+ * `registerEnvironment` registers this for you; it is exported so an app that
26
+ * builds its own system schedule can place it deliberately.
27
+ *
28
+ * IWSDK hands a system its delta in SECONDS and the directors take
29
+ * milliseconds. That conversion happens here, once.
30
+ */
31
+ export class EnvironmentSystem extends createSystem({}) {
32
+ update(delta) {
33
+ const setup = hosts.get(this.world);
34
+ if (setup === undefined)
35
+ return;
36
+ const deltaMs = delta * 1000;
37
+ setup.environment.update(deltaMs);
38
+ setup.audio?.update(deltaMs);
39
+ }
40
+ }
41
+ /**
42
+ * One call to wire the environment (and optionally the audio) into an IWSDK
43
+ * world, and register the system that ticks them.
44
+ *
45
+ * ```ts
46
+ * const env = registerEnvironment(world, {
47
+ * presets: STOCK_PRESETS,
48
+ * initial: VOID,
49
+ * audio: { cues: [{ id: "hum", src: "/audio/hum.mp3", bus: "ambience", loop: true }] },
50
+ * });
51
+ * env.environment.transition("dusk", { durationMs: 8000 });
52
+ * ```
53
+ *
54
+ * Passthrough is NOT read from the session here - push it in from whatever
55
+ * already tracks capabilities (`env.environment.setPassthrough(...)`). The
56
+ * session belongs to the platform layer, not to the environment.
57
+ */
58
+ export function registerEnvironment(world, options = {}) {
59
+ const existing = hosts.get(world);
60
+ if (existing !== undefined)
61
+ return existing;
62
+ const environmentPort = new IWSDKEnvironmentPort(world, options);
63
+ const environment = new EnvironmentDirector(environmentPort, options);
64
+ let audio = null;
65
+ let audioPort = null;
66
+ if (options.audio !== undefined) {
67
+ audioPort = new IWSDKAudioPort(world, options.audio);
68
+ audio = new AudioDirector(audioPort, options.audio);
69
+ }
70
+ const setup = {
71
+ environment,
72
+ environmentPort,
73
+ audio,
74
+ audioPort,
75
+ dispose() {
76
+ audio?.dispose();
77
+ environment.dispose();
78
+ hosts.delete(world);
79
+ },
80
+ };
81
+ hosts.set(world, setup);
82
+ world.registerSystem(EnvironmentSystem);
83
+ return setup;
84
+ }
85
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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"]}
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@realitycollective/iwsdk-environment",
3
+ "version": "0.1.0-preview.0",
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
+ "keywords": [
6
+ "realitycollective",
7
+ "iwsdk",
8
+ "webxr",
9
+ "xr",
10
+ "environment",
11
+ "skybox",
12
+ "fog",
13
+ "lighting",
14
+ "audio",
15
+ "typescript"
16
+ ],
17
+ "license": "MIT",
18
+ "author": "Reality Collective",
19
+ "type": "module",
20
+ "main": "./dist/index.js",
21
+ "types": "./dist/index.d.ts",
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/index.d.ts",
25
+ "default": "./dist/index.js"
26
+ }
27
+ },
28
+ "sideEffects": false,
29
+ "files": [
30
+ "dist",
31
+ "CHANGELOG.md"
32
+ ],
33
+ "scripts": {
34
+ "build": "tsc -p tsconfig.build.json",
35
+ "typecheck": "tsc -p tsconfig.json --noEmit"
36
+ },
37
+ "dependencies": {
38
+ "@realitycollective/webxr-environment": "^0.1.0-preview.0"
39
+ },
40
+ "peerDependencies": {
41
+ "@iwsdk/core": ">=0.5.0 <0.6.0"
42
+ },
43
+ "devDependencies": {
44
+ "@iwsdk/core": "0.5.3",
45
+ "three": "npm:super-three@0.181.0",
46
+ "@types/three": "*"
47
+ },
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "git+https://github.com/realitycollective/WebXR-Environment.git",
51
+ "directory": "packages/iwsdk-environment"
52
+ },
53
+ "homepage": "https://github.com/realitycollective/WebXR-Environment#readme",
54
+ "bugs": {
55
+ "url": "https://github.com/realitycollective/WebXR-Environment/issues"
56
+ },
57
+ "publishConfig": {
58
+ "access": "public"
59
+ },
60
+ "engines": {
61
+ "node": ">=20.19.0 <21.0.0-0 || >=22.12.0 <23.0.0-0 || >=24.0.0"
62
+ }
63
+ }