@realitycollective/iwsdk-environment 0.1.0 → 0.1.1-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,9 +1,28 @@
1
1
  # Changelog
2
2
 
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.
3
+ Change log for the Reality Collective WebXR Environment Extensions packages. All five 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
 
7
+ ## [0.1.1]
8
+
9
+ ### Added
10
+
11
+ - `@realitycollective/webxr-environment` - **scene management**. `SceneManager` holds the rules and a new `ScenePort` builds and destroys content on each host. It covers a scene list (`register`, `list`), single and additive loads, `unload` of any scene in the stack with the rest keeping their order, preloading (`activate: false`, then `activate`) for an instant switch, an active scene (`getActiveScene`, `setActiveScene`) that receives new instances and whose `environment` drives the director with the configured transition, `instantiate` and `destroy` of named assets, `setNodeActive`, `makePersistent` (addressed afterwards under `PERSISTENT_SCENE_ID`), `bindNode` to tie something such as an interaction target to a node while its scene is shown, and `onSceneLoaded` / `onSceneUnloaded` / `onActiveSceneChanged`. Loads and unloads run in call order; a repeat load returns the same promise; an id not in the list rejects; a scene that repeats a node id fails to load. `dispose` leaves nothing on the host and restores the environment a scene first overrode. The owner placed it here on 2026-09-25, and `docs/BOUNDARY.md` records what moved in and what stays out.
12
+ - `@realitycollective/webxr-environment` - `sceneManagerContractCases()`, 18 cases covering every rule above, with `SCENE_CONTRACT_FIXTURES`. Unlike the port suites they run the real manager over a host's real port, with an inspector that answers from the host itself, including whether its own hit testing would find a node. Each case fails against a port or a manager built to break it.
13
+ - `@realitycollective/threejs-environment` - `ThreeScenePort` and `createThreeScenes(root, options)`. Each scene is a group under the root, loaded with `GLTFLoader` by default or an app's own `load`; node ids come from `userData.nodeId` or the object's name. A hidden scene or inactive node stays in the graph, invisible, with an empty layer mask so no `Raycaster` finds it and `pointerEvents = "none"` for pmndrs pointer events; each object's own values come back when it is shown.
14
+ - `@realitycollective/iwsdk-environment` - `IWSDKScenePort` and `createIWSDKScenes(world, options)`. Scenes are built by IWSDK's own `SceneJSONImporter`, each under its own persistent root beside `world.loadLevel` rather than replacing it. The `LevelTag` the importer adds is removed, so an unrelated `loadLevel` cannot destroy a managed scene. Every entity a scene owns is tracked, because destroying an IWSDK entity does not destroy the entities under it. Instances default to a clone of the preloaded glTF in IWSDK's `AssetManager`.
15
+ - `@realitycollective/xrblocks-environment` - `createXRBlocksScenes(root, options)`, which is `ThreeScenePort` outright, as `createXRBlocksAudio` is `ThreeAudioPort`.
16
+ - `@realitycollective/native-environment` - the `scenes` slice on `__rcHost`, `NativeScenePort` and `createNativeScenes`. The native app builds each scene from its `src` with its own loaders and physics from the physics components in it; scenes and nodes cross as the app's own string keys, and a node's key is its target id in the `interactions` slice. `test/helpers.ts` ships `createFakeScenesHost`, an in-memory app that conforms. All four platforms pass all 18 scene cases.
17
+ - `@realitycollective/webxr-environment` - `environmentPortContractCases()`, `audioPortContractCases()` and `worldSensingPortContractCases()`, the shared `EnvironmentPort` / `AudioPort` / `WorldSensingPort` conformance suites, shipped as data rather than as tests, in the style of `webxr-uiextensions`'s `contract-cases.ts`. Each case is a `name` plus a `run(subject)` that throws a plain `Error` on failure, so an adapter runs it in its own test runner. They check what the core relies on and nothing more: every sky, fog, ambient, key and IBL variant applies without throwing; a port that senses answers a request through `observe` with a report naming a known feature and state; a one-shot voice's `ended` runs exactly once and a stopped voice is never driven to end afterwards; a world-sensing port's `createAnchor` never rejects and its `stopHitTest` / `removeAnchor` tolerate an unknown id. All four platforms - `threejs-`, `iwsdk-`, `xrblocks-environment` and `native-environment` - now run all three suites, and all four already passed every case. `contract-cases.ts` is inside the core's 100% coverage gate.
18
+ - `@realitycollective/native-environment` - the native host adapter, for an app that embeds a JavaScript engine such as Hermes over OpenXR or CompositorServices, owns rendering and audio itself, and installs `globalThis.__rcHost`. `NativeEnvironmentPort`, `NativeAudioPort` and `NativeWorldSensingPort` forward every slot, request and callback to the host's `environment`, `audio` and `sensing` slices as plain data, deciding nothing: a `src` string is resolved and decoded entirely by the native app, and depth occlusion, light estimation and world sensing are grown on the port only when the host implements them, so the director's own "the port does not have this" reporting is what an app sees. Audio keeps every voice's `ended` callback in JavaScript, keyed by `voiceId`, because a callback the core hands to a port cannot itself cross this boundary. `environment` and `audio` are required by the port that reads them and throw, naming the slice, when neither an injected host nor `globalThis.__rcHost` has one; `sensing` is genuinely optional and is instead reported `unsupported`.
19
+
20
+ ### Fixed
21
+
22
+ - `@realitycollective/threejs-environment` - a gradient sky never changed after its first frame. Choosing `night` in the playground moved the lights and the fog while the sky stayed at noon; only a solid sky, which is a plain `Color`, followed a transition. `applySky` refilled the pixels of the one `DataTexture` and set `needsUpdate`, but three.js converts an equirectangular `scene.background` into a cube map once per texture object and keeps that cube map until the texture is disposed, so the 2D texture was re-uploaded and the sky on screen never re-derived. The port now builds a fresh texture on every gradient change, swaps it in and disposes the previous one, which is what drops the cached cube map; the texture is 2 by 64 texels, so the extra upload during a transition is negligible and the cube conversion costs the same either way. The test that pinned the old in-place refill now pins the swap and the disposal instead. `@realitycollective/xrblocks-environment` inherits the fix. Found while photographing the live playground for the Reality Collective site, on a real GPU as well as in software rendering.
23
+ - `@realitycollective/threejs-environment` - `XrInputSourceLike` is exported. It was the element type of `XrSessionLike.inputSources` but not exported itself, so a consumer typing a fake session for a test had nothing to name, and TypeDoc flagged it as referenced but undocumented.
24
+ - CI - the staging deploy published under `--branch=pr-<number>` while the `-test` Pages project's production branch is `staging`, so only `pr-<n>.webxr-environment-test.pages.dev` aliases were ever created and the project's root URL was a 404. It now deploys as `staging` on every pull request and on every push to `development`, so `webxr-environment-test.pages.dev` serves the newest preview build, the same arrangement WebXR-UIExtensions already had.
25
+
7
26
  ## [0.1.0] - 2026-09-17
8
27
 
9
28
  ### Added
@@ -49,4 +68,5 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
49
68
  - **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`.
50
69
  - 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.
51
70
 
71
+ [0.1.1]: https://github.com/realitycollective/WebXR-Environment/compare/v0.1.0...development
52
72
  [0.1.0]: https://github.com/realitycollective/WebXR-Environment/releases/tag/v0.1.0
package/dist/index.d.ts CHANGED
@@ -5,16 +5,20 @@
5
5
  * nothing else.
6
6
  */
7
7
  import type { Entity, World } from "@iwsdk/core";
8
- import type { AudioDirectorOptions, EnvironmentDirectorOptions } from "@realitycollective/webxr-environment";
9
- import { AudioDirector, EnvironmentDirector } from "@realitycollective/webxr-environment";
8
+ import type { AudioDirectorOptions, EnvironmentDirectorOptions, SceneManagerOptions } from "@realitycollective/webxr-environment";
9
+ import { AudioDirector, EnvironmentDirector, SceneManager } from "@realitycollective/webxr-environment";
10
10
  import type { IWSDKEnvironmentPortOptions } from "./environment-port.js";
11
11
  import { IWSDKEnvironmentPort } from "./environment-port.js";
12
12
  import type { IWSDKAudioPortOptions } from "./audio-port.js";
13
13
  import { IWSDKAudioPort } from "./audio-port.js";
14
+ import type { IWSDKScenePortOptions } from "./scene-port.js";
15
+ import { IWSDKScenePort } from "./scene-port.js";
14
16
  export type { IWSDKEnvironmentPortOptions } from "./environment-port.js";
15
17
  export { IWSDKEnvironmentPort } from "./environment-port.js";
16
18
  export type { IWSDKAudioPortOptions } from "./audio-port.js";
17
19
  export { IWSDKAudioPort } from "./audio-port.js";
20
+ export type { IWSDKAssetFactory, IWSDKSceneContent, IWSDKSceneLoader, IWSDKScenePortOptions, } from "./scene-port.js";
21
+ export { IWSDKScenePort } from "./scene-port.js";
18
22
  export type { IWSDKWorldSensingPortOptions } from "./world-sensing-port.js";
19
23
  export { IWSDKWorldSensingPort } from "./world-sensing-port.js";
20
24
  export * from "@realitycollective/webxr-environment";
@@ -67,3 +71,15 @@ export declare class EnvironmentSystem extends EnvironmentSystem_base {
67
71
  */
68
72
  export declare function registerEnvironment(world: World, options?: IWSDKEnvironmentOptions): IWSDKEnvironmentSetup;
69
73
  export type { Entity as IWSDKEntity };
74
+ export interface IWSDKScenesSetup {
75
+ readonly manager: SceneManager<Entity, Entity>;
76
+ readonly port: IWSDKScenePort;
77
+ }
78
+ /**
79
+ * Wire a scene manager to an IWSDK world. Scenes are `iwsdk.scene.v1`
80
+ * documents built by IWSDK's own importer, each under a persistent root of
81
+ * its own, so they live beside `world.loadLevel` rather than replacing it.
82
+ * Pass `environment: setup.environment` from `registerEnvironment` to let the
83
+ * active scene drive the sky, fog and light.
84
+ */
85
+ export declare function createIWSDKScenes(world: World, options?: SceneManagerOptions & IWSDKScenePortOptions): IWSDKScenesSetup;
package/dist/index.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import { createSystem } from "./create-system.js";
2
- import { AudioDirector, EnvironmentDirector } from "@realitycollective/webxr-environment";
2
+ import { AudioDirector, EnvironmentDirector, SceneManager } from "@realitycollective/webxr-environment";
3
3
  import { IWSDKEnvironmentPort } from "./environment-port.js";
4
4
  import { IWSDKAudioPort } from "./audio-port.js";
5
+ import { IWSDKScenePort } from "./scene-port.js";
5
6
  export { IWSDKEnvironmentPort } from "./environment-port.js";
6
7
  export { IWSDKAudioPort } from "./audio-port.js";
8
+ export { IWSDKScenePort } from "./scene-port.js";
7
9
  export { IWSDKWorldSensingPort } from "./world-sensing-port.js";
8
10
  export * from "@realitycollective/webxr-environment";
9
11
  const hosts = new WeakMap();
@@ -77,4 +79,15 @@ export function registerEnvironment(world, options = {}) {
77
79
  world.registerSystem(EnvironmentSystem);
78
80
  return setup;
79
81
  }
82
+ /**
83
+ * Wire a scene manager to an IWSDK world. Scenes are `iwsdk.scene.v1`
84
+ * documents built by IWSDK's own importer, each under a persistent root of
85
+ * its own, so they live beside `world.loadLevel` rather than replacing it.
86
+ * Pass `environment: setup.environment` from `registerEnvironment` to let the
87
+ * active scene drive the sky, fog and light.
88
+ */
89
+ export function createIWSDKScenes(world, options = {}) {
90
+ const port = new IWSDKScenePort(world, options);
91
+ return { manager: new SceneManager(port, options), port };
92
+ }
80
93
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAKlD,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 type { Entity, World } from \"@iwsdk/core\";\nimport { createSystem } from \"./create-system.js\";\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"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","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 type { Entity, World } from \"@iwsdk/core\";\nimport { createSystem } from \"./create-system.js\";\nimport type {\n AudioDirectorOptions,\n EnvironmentDirectorOptions,\n SceneManagerOptions,\n} from \"@realitycollective/webxr-environment\";\nimport { AudioDirector, EnvironmentDirector, SceneManager } 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\";\nimport type { IWSDKScenePortOptions } from \"./scene-port.js\";\nimport { IWSDKScenePort } from \"./scene-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 {\n IWSDKAssetFactory,\n IWSDKSceneContent,\n IWSDKSceneLoader,\n IWSDKScenePortOptions,\n} from \"./scene-port.js\";\nexport { IWSDKScenePort } from \"./scene-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\nexport interface IWSDKScenesSetup {\n readonly manager: SceneManager<Entity, Entity>;\n readonly port: IWSDKScenePort;\n}\n\n/**\n * Wire a scene manager to an IWSDK world. Scenes are `iwsdk.scene.v1`\n * documents built by IWSDK's own importer, each under a persistent root of\n * its own, so they live beside `world.loadLevel` rather than replacing it.\n * Pass `environment: setup.environment` from `registerEnvironment` to let the\n * active scene drive the sky, fog and light.\n */\nexport function createIWSDKScenes(\n world: World,\n options: SceneManagerOptions & IWSDKScenePortOptions = {},\n): IWSDKScenesSetup {\n const port = new IWSDKScenePort(world, options);\n return { manager: new SceneManager(port, options), port };\n}\n"]}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * `ScenePort` for IWSDK.
3
+ *
4
+ * ---------------------------------------------------------------------------
5
+ * A SCENE IS AN ENTITY TREE BESIDE THE LEVEL, NOT A LEVEL
6
+ * ---------------------------------------------------------------------------
7
+ * IWSDK's `world.loadLevel` replaces the one active level, which is a single
8
+ * load and nothing else. This port builds each scene under its own persistent
9
+ * root entity with IWSDK's own `SceneJSONImporter`, the loader `loadLevel`
10
+ * uses, so additive loads, a stack and preloading all work. The importer tags
11
+ * what it creates with `LevelTag`; the port removes that tag, because a scene
12
+ * this manager owns must not be destroyed by an unrelated `loadLevel`.
13
+ *
14
+ * Every entity the port creates is tracked per scene, because destroying an
15
+ * entity in IWSDK does not destroy the entities parented under it.
16
+ *
17
+ * ---------------------------------------------------------------------------
18
+ * HIDDEN MEANS NOT RENDERED AND NOT HIT-TESTABLE
19
+ * ---------------------------------------------------------------------------
20
+ * A transform entity cannot leave the graph - IWSDK's `TransformSystem`
21
+ * re-parents it on the next frame - so hiding is done in place, as in the
22
+ * three.js port: `visible` (which IWSDK routes into its `Visibility`
23
+ * component), an empty layer mask for three.js raycasters, and
24
+ * `pointerEvents = "none"` for the pmndrs pointer events IWSDK's input system
25
+ * uses, which call `raycast` directly and read only that property.
26
+ */
27
+ import type { Entity, World } from "@iwsdk/core";
28
+ import type { Object3D } from "three";
29
+ import type { SceneDefinition, ScenePort, WorldPose } from "@realitycollective/webxr-environment";
30
+ /** What a loader built: every node by id, each one an entity. */
31
+ export interface IWSDKSceneContent {
32
+ readonly nodes: readonly {
33
+ readonly id: string;
34
+ readonly entity: Entity;
35
+ }[];
36
+ }
37
+ /**
38
+ * Builds a scene's content under `parent`. Default: IWSDK's own
39
+ * `SceneJSONImporter`, reading `src` as an `iwsdk.scene.v1` document.
40
+ */
41
+ export type IWSDKSceneLoader = (def: SceneDefinition, parent: Entity, onProgress: (progress: number) => void) => Promise<IWSDKSceneContent>;
42
+ /** Makes a fresh object for a named asset, for `instantiate`. */
43
+ export type IWSDKAssetFactory = (asset: string) => Object3D;
44
+ export interface IWSDKScenePortOptions {
45
+ readonly load?: IWSDKSceneLoader;
46
+ /**
47
+ * Resolves an asset name for `instantiate`. Default: a clone of the glTF
48
+ * IWSDK's `AssetManager` holds under that key, which it has once the asset
49
+ * is in the world's manifest and preloaded.
50
+ */
51
+ readonly assets?: IWSDKAssetFactory;
52
+ }
53
+ export declare class IWSDKScenePort implements ScenePort<Entity, Entity> {
54
+ #private;
55
+ constructor(world: World, options?: IWSDKScenePortOptions);
56
+ build(def: SceneDefinition, visible: boolean, onProgress: (progress: number) => void): Promise<Entity>;
57
+ setVisible(scene: Entity, visible: boolean): void;
58
+ destroy(scene: Entity): void;
59
+ nodeIds(scene: Entity): readonly string[];
60
+ findNode(scene: Entity, nodeId: string): Entity | null;
61
+ setNodeActive(node: Entity, active: boolean): void;
62
+ instantiate(asset: string, pose: WorldPose, scene: Entity, parent: Entity | null): Entity;
63
+ destroyInstance(instance: Entity): void;
64
+ detachNode(scene: Entity, node: Entity): void;
65
+ destroyNode(node: Entity): void;
66
+ }
@@ -0,0 +1,163 @@
1
+ import { AssetManager, LevelTag, SceneJSONImporter } from "@iwsdk/core";
2
+ import { Group, Matrix4, Quaternion, Vector3 } from "three";
3
+ function isUnder(object, ancestor) {
4
+ for (let at = object; at !== null; at = at.parent)
5
+ if (at === ancestor)
6
+ return true;
7
+ return false;
8
+ }
9
+ function defaultAsset(asset) {
10
+ const gltf = AssetManager.getGLTF(asset);
11
+ if (gltf === null) {
12
+ throw new Error(`[iwsdk-environment] cannot instantiate "${asset}": it is not a preloaded glTF in the asset manifest`);
13
+ }
14
+ return gltf.scene.clone();
15
+ }
16
+ export class IWSDKScenePort {
17
+ #world;
18
+ #load;
19
+ #assets;
20
+ #scenes = new Map();
21
+ /** Entities under a node that `detachNode` moved out, destroyed with it. */
22
+ #detached = new Map();
23
+ #off = new WeakSet();
24
+ #saved = new WeakMap();
25
+ constructor(world, options = {}) {
26
+ this.#world = world;
27
+ this.#load =
28
+ options.load ??
29
+ (async (def, parent) => {
30
+ const result = await SceneJSONImporter.load(world, def.src, parent);
31
+ return { nodes: [...result.nodes.values()].map((node) => ({ id: node.nodeId, entity: node.entity })) };
32
+ });
33
+ this.#assets = options.assets ?? defaultAsset;
34
+ }
35
+ async build(def, visible, onProgress) {
36
+ const root = this.#world.createTransformEntity(new Group(), {
37
+ parent: this.#world.sceneEntity,
38
+ persistent: true,
39
+ });
40
+ root.object3D.name = `scene:${def.id}`;
41
+ if (!visible)
42
+ this.#setOff(root.object3D, true);
43
+ let content;
44
+ try {
45
+ content = await this.#load(def, root, onProgress);
46
+ }
47
+ catch (error) {
48
+ root.destroy();
49
+ throw error;
50
+ }
51
+ const record = { ids: [], nodes: new Map(), entities: new Set() };
52
+ for (const { id, entity } of content.nodes) {
53
+ if (entity.hasComponent(LevelTag))
54
+ entity.removeComponent(LevelTag);
55
+ record.ids.push(id);
56
+ record.entities.add(entity);
57
+ if (!record.nodes.has(id))
58
+ record.nodes.set(id, entity);
59
+ }
60
+ this.#scenes.set(root, record);
61
+ this.#refresh(root.object3D);
62
+ return root;
63
+ }
64
+ setVisible(scene, visible) {
65
+ this.#setOff(scene.object3D, !visible);
66
+ }
67
+ destroy(scene) {
68
+ const record = this.#scenes.get(scene);
69
+ this.#scenes.delete(scene);
70
+ for (const entity of record?.entities ?? [])
71
+ entity.destroy();
72
+ scene.destroy();
73
+ }
74
+ nodeIds(scene) {
75
+ return this.#scenes.get(scene)?.ids ?? [];
76
+ }
77
+ findNode(scene, nodeId) {
78
+ return this.#scenes.get(scene)?.nodes.get(nodeId) ?? null;
79
+ }
80
+ setNodeActive(node, active) {
81
+ this.#setOff(node.object3D, !active);
82
+ }
83
+ instantiate(asset, pose, scene, parent) {
84
+ const object = this.#assets(asset);
85
+ const target = parent ?? scene;
86
+ const targetObject = target.object3D;
87
+ targetObject.updateWorldMatrix(true, false);
88
+ const world = new Matrix4().compose(new Vector3(pose.position[0], pose.position[1], pose.position[2]), new Quaternion(pose.orientation[0], pose.orientation[1], pose.orientation[2], pose.orientation[3]), new Vector3(1, 1, 1));
89
+ world.premultiply(targetObject.matrixWorld.clone().invert());
90
+ world.decompose(object.position, object.quaternion, object.scale);
91
+ const entity = this.#world.createTransformEntity(object, { parent: target, persistent: true });
92
+ this.#scenes.get(scene)?.entities.add(entity);
93
+ this.#refresh(object);
94
+ return entity;
95
+ }
96
+ destroyInstance(instance) {
97
+ for (const record of this.#scenes.values())
98
+ record.entities.delete(instance);
99
+ instance.destroy();
100
+ }
101
+ detachNode(scene, node) {
102
+ const object = node.object3D;
103
+ this.#world.sceneEntity.object3D.attach(object);
104
+ const owned = new Set();
105
+ const record = this.#scenes.get(scene);
106
+ if (record !== undefined) {
107
+ for (const entity of record.entities) {
108
+ if (entity === node || (entity.object3D !== undefined && isUnder(entity.object3D, object))) {
109
+ record.entities.delete(entity);
110
+ if (entity !== node)
111
+ owned.add(entity);
112
+ }
113
+ }
114
+ for (const [id, found] of record.nodes)
115
+ if (found === node)
116
+ record.nodes.delete(id);
117
+ }
118
+ this.#detached.set(node, owned);
119
+ this.#refresh(object);
120
+ }
121
+ destroyNode(node) {
122
+ for (const entity of this.#detached.get(node) ?? [])
123
+ entity.destroy();
124
+ this.#detached.delete(node);
125
+ node.destroy();
126
+ }
127
+ #setOff(object, off) {
128
+ if (off)
129
+ this.#off.add(object);
130
+ else
131
+ this.#off.delete(object);
132
+ object.visible = !off;
133
+ this.#refresh(object);
134
+ }
135
+ #refresh(object) {
136
+ let inherited = false;
137
+ for (let at = object.parent; at !== null; at = at.parent) {
138
+ if (this.#off.has(at)) {
139
+ inherited = true;
140
+ break;
141
+ }
142
+ }
143
+ this.#visit(object, inherited);
144
+ }
145
+ #visit(object, inherited) {
146
+ const off = inherited || this.#off.has(object);
147
+ const target = object;
148
+ const saved = this.#saved.get(object);
149
+ if (off && saved === undefined) {
150
+ this.#saved.set(object, { mask: object.layers.mask, pointerEvents: target.pointerEvents });
151
+ object.layers.mask = 0;
152
+ target.pointerEvents = "none";
153
+ }
154
+ else if (!off && saved !== undefined) {
155
+ this.#saved.delete(object);
156
+ object.layers.mask = saved.mask;
157
+ target.pointerEvents = saved.pointerEvents;
158
+ }
159
+ for (const child of object.children)
160
+ this.#visit(child, off);
161
+ }
162
+ }
163
+ //# sourceMappingURL=scene-port.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scene-port.js","sourceRoot":"","sources":["../src/scene-port.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `ScenePort` for IWSDK.\n *\n * ---------------------------------------------------------------------------\n * A SCENE IS AN ENTITY TREE BESIDE THE LEVEL, NOT A LEVEL\n * ---------------------------------------------------------------------------\n * IWSDK's `world.loadLevel` replaces the one active level, which is a single\n * load and nothing else. This port builds each scene under its own persistent\n * root entity with IWSDK's own `SceneJSONImporter`, the loader `loadLevel`\n * uses, so additive loads, a stack and preloading all work. The importer tags\n * what it creates with `LevelTag`; the port removes that tag, because a scene\n * this manager owns must not be destroyed by an unrelated `loadLevel`.\n *\n * Every entity the port creates is tracked per scene, because destroying an\n * entity in IWSDK does not destroy the entities parented under it.\n *\n * ---------------------------------------------------------------------------\n * HIDDEN MEANS NOT RENDERED AND NOT HIT-TESTABLE\n * ---------------------------------------------------------------------------\n * A transform entity cannot leave the graph - IWSDK's `TransformSystem`\n * re-parents it on the next frame - so hiding is done in place, as in the\n * three.js port: `visible` (which IWSDK routes into its `Visibility`\n * component), an empty layer mask for three.js raycasters, and\n * `pointerEvents = \"none\"` for the pmndrs pointer events IWSDK's input system\n * uses, which call `raycast` directly and read only that property.\n */\nimport type { Entity, World } from \"@iwsdk/core\";\nimport { AssetManager, LevelTag, SceneJSONImporter } from \"@iwsdk/core\";\nimport type { Object3D } from \"three\";\nimport { Group, Matrix4, Quaternion, Vector3 } from \"three\";\nimport type { SceneDefinition, ScenePort, WorldPose } from \"@realitycollective/webxr-environment\";\n\n/** What a loader built: every node by id, each one an entity. */\nexport interface IWSDKSceneContent {\n readonly nodes: readonly { readonly id: string; readonly entity: Entity }[];\n}\n\n/**\n * Builds a scene's content under `parent`. Default: IWSDK's own\n * `SceneJSONImporter`, reading `src` as an `iwsdk.scene.v1` document.\n */\nexport type IWSDKSceneLoader = (\n def: SceneDefinition,\n parent: Entity,\n onProgress: (progress: number) => void,\n) => Promise<IWSDKSceneContent>;\n\n/** Makes a fresh object for a named asset, for `instantiate`. */\nexport type IWSDKAssetFactory = (asset: string) => Object3D;\n\nexport interface IWSDKScenePortOptions {\n readonly load?: IWSDKSceneLoader;\n /**\n * Resolves an asset name for `instantiate`. Default: a clone of the glTF\n * IWSDK's `AssetManager` holds under that key, which it has once the asset\n * is in the world's manifest and preloaded.\n */\n readonly assets?: IWSDKAssetFactory;\n}\n\ninterface SceneRecord {\n readonly ids: string[];\n readonly nodes: Map<string, Entity>;\n /** Every entity this scene owns, root excluded. */\n readonly entities: Set<Entity>;\n}\n\n/** IWSDK types `pointerEvents` narrowly; the port only saves and restores it. */\ninterface PointerTarget {\n pointerEvents?: unknown;\n}\n\ninterface Saved {\n readonly mask: number;\n readonly pointerEvents: unknown;\n}\n\nfunction isUnder(object: Object3D, ancestor: Object3D): boolean {\n for (let at: Object3D | null = object; at !== null; at = at.parent) if (at === ancestor) return true;\n return false;\n}\n\nfunction defaultAsset(asset: string): Object3D {\n const gltf = AssetManager.getGLTF(asset);\n if (gltf === null) {\n throw new Error(\n `[iwsdk-environment] cannot instantiate \"${asset}\": it is not a preloaded glTF in the asset manifest`,\n );\n }\n return gltf.scene.clone();\n}\n\nexport class IWSDKScenePort implements ScenePort<Entity, Entity> {\n readonly #world: World;\n readonly #load: IWSDKSceneLoader;\n readonly #assets: IWSDKAssetFactory;\n readonly #scenes = new Map<Entity, SceneRecord>();\n /** Entities under a node that `detachNode` moved out, destroyed with it. */\n readonly #detached = new Map<Entity, Set<Entity>>();\n readonly #off = new WeakSet<Object3D>();\n readonly #saved = new WeakMap<Object3D, Saved>();\n\n constructor(world: World, options: IWSDKScenePortOptions = {}) {\n this.#world = world;\n this.#load =\n options.load ??\n (async (def, parent) => {\n const result = await SceneJSONImporter.load(world, def.src, parent);\n return { nodes: [...result.nodes.values()].map((node) => ({ id: node.nodeId, entity: node.entity })) };\n });\n this.#assets = options.assets ?? defaultAsset;\n }\n\n async build(def: SceneDefinition, visible: boolean, onProgress: (progress: number) => void): Promise<Entity> {\n const root = this.#world.createTransformEntity(new Group(), {\n parent: this.#world.sceneEntity,\n persistent: true,\n });\n (root.object3D as Object3D).name = `scene:${def.id}`;\n if (!visible) this.#setOff(root.object3D as Object3D, true);\n let content: IWSDKSceneContent;\n try {\n content = await this.#load(def, root, onProgress);\n } catch (error) {\n root.destroy();\n throw error;\n }\n const record: SceneRecord = { ids: [], nodes: new Map(), entities: new Set() };\n for (const { id, entity } of content.nodes) {\n if (entity.hasComponent(LevelTag)) entity.removeComponent(LevelTag);\n record.ids.push(id);\n record.entities.add(entity);\n if (!record.nodes.has(id)) record.nodes.set(id, entity);\n }\n this.#scenes.set(root, record);\n this.#refresh(root.object3D as Object3D);\n return root;\n }\n\n setVisible(scene: Entity, visible: boolean): void {\n this.#setOff(scene.object3D as Object3D, !visible);\n }\n\n destroy(scene: Entity): void {\n const record = this.#scenes.get(scene);\n this.#scenes.delete(scene);\n for (const entity of record?.entities ?? []) entity.destroy();\n scene.destroy();\n }\n\n nodeIds(scene: Entity): readonly string[] {\n return this.#scenes.get(scene)?.ids ?? [];\n }\n\n findNode(scene: Entity, nodeId: string): Entity | null {\n return this.#scenes.get(scene)?.nodes.get(nodeId) ?? null;\n }\n\n setNodeActive(node: Entity, active: boolean): void {\n this.#setOff(node.object3D as Object3D, !active);\n }\n\n instantiate(asset: string, pose: WorldPose, scene: Entity, parent: Entity | null): Entity {\n const object = this.#assets(asset);\n const target = parent ?? scene;\n const targetObject = target.object3D as Object3D;\n targetObject.updateWorldMatrix(true, false);\n const world = new Matrix4().compose(\n new Vector3(pose.position[0], pose.position[1], pose.position[2]),\n new Quaternion(pose.orientation[0], pose.orientation[1], pose.orientation[2], pose.orientation[3]),\n new Vector3(1, 1, 1),\n );\n world.premultiply(targetObject.matrixWorld.clone().invert());\n world.decompose(object.position, object.quaternion, object.scale);\n const entity = this.#world.createTransformEntity(object, { parent: target, persistent: true });\n this.#scenes.get(scene)?.entities.add(entity);\n this.#refresh(object);\n return entity;\n }\n\n destroyInstance(instance: Entity): void {\n for (const record of this.#scenes.values()) record.entities.delete(instance);\n instance.destroy();\n }\n\n detachNode(scene: Entity, node: Entity): void {\n const object = node.object3D as Object3D;\n (this.#world.sceneEntity.object3D as Object3D).attach(object);\n const owned = new Set<Entity>();\n const record = this.#scenes.get(scene);\n if (record !== undefined) {\n for (const entity of record.entities) {\n if (entity === node || (entity.object3D !== undefined && isUnder(entity.object3D, object))) {\n record.entities.delete(entity);\n if (entity !== node) owned.add(entity);\n }\n }\n for (const [id, found] of record.nodes) if (found === node) record.nodes.delete(id);\n }\n this.#detached.set(node, owned);\n this.#refresh(object);\n }\n\n destroyNode(node: Entity): void {\n for (const entity of this.#detached.get(node) ?? []) entity.destroy();\n this.#detached.delete(node);\n node.destroy();\n }\n\n #setOff(object: Object3D, off: boolean): void {\n if (off) this.#off.add(object);\n else this.#off.delete(object);\n object.visible = !off;\n this.#refresh(object);\n }\n\n #refresh(object: Object3D): void {\n let inherited = false;\n for (let at = object.parent; at !== null; at = at.parent) {\n if (this.#off.has(at)) {\n inherited = true;\n break;\n }\n }\n this.#visit(object, inherited);\n }\n\n #visit(object: Object3D, inherited: boolean): void {\n const off = inherited || this.#off.has(object);\n const target = object as unknown as PointerTarget;\n const saved = this.#saved.get(object);\n if (off && saved === undefined) {\n this.#saved.set(object, { mask: object.layers.mask, pointerEvents: target.pointerEvents });\n object.layers.mask = 0;\n target.pointerEvents = \"none\";\n } else if (!off && saved !== undefined) {\n this.#saved.delete(object);\n object.layers.mask = saved.mask;\n target.pointerEvents = saved.pointerEvents;\n }\n for (const child of object.children) this.#visit(child, off);\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@realitycollective/iwsdk-environment",
3
- "version": "0.1.0",
3
+ "version": "0.1.1-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,7 +35,7 @@
35
35
  "typecheck": "tsc -p tsconfig.json --noEmit"
36
36
  },
37
37
  "dependencies": {
38
- "@realitycollective/webxr-environment": "^0.1.0"
38
+ "@realitycollective/webxr-environment": "^0.1.1-preview.1"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "@iwsdk/core": ">=0.5.0 <0.6.0",