@realitycollective/iwsdk-uiextensions 0.1.0 → 0.1.1-preview.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,9 +1,35 @@
1
1
  # Changelog
2
2
 
3
- Change log for the Reality Collective WebXR UI 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 UI 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
+ - `uixElementContractCases()`, a shared conformance suite for `UixElement`, the interface the core's controls drive panel elements through, with `CONTRACT_PANEL_MARKUP` and the `UixElementContractSubject`, `UixElementContractDriver` and `UixElementContractCase` types. Every platform builds the same small panel from the markup its own way, and six cases check it: children follow the markup in order, `tagOf` reads a `uix-*` tag and nothing for a built-in one, `data-uix-*` attributes arrive camelCased in `userData`, `setProperties` reaches what the platform shows, one event reaches every listener on its element and no other, and a core stepper upgraded from the markup follows clicks to its maximum. It runs against real uikit elements on IWSDK and on XR Blocks and three.js, built headlessly the way each platform builds them, and against `NativeWindowHost` over the fake native app. It found the native defect under Fixed.
12
+ - `dataAttributeKey()`, the one rule for where a `data-*` attribute lands in `userData`: `data-uix-min` becomes `uixMin`, and an already camelCased `dataUixMin` gives the same key. The IWSDK component set and the native proxy elements both use it; the IWSDK copy was private.
13
+ - `sceneTargetContractCases()`, a shared conformance suite for `SceneTarget`, shipped as data beside `windowHostContractCases()`, with `CONTRACT_SCENE` and the `SceneTargetContractSetup` and `SceneTargetContractCase` types. A client builds its UI with the core's `applyScene()` and never asks which platform draws it, so every host must turn the same descriptor into the same windows. The suite checks that through the host's `WindowManager`: every descriptor window is opened under its id and title, a window named into a region is in that region, a window keeps its dock mode and chrome, and a window given both a region and a dock mode is world-locked in that region. The IWSDK, XR Blocks and native hosts all pass it.
14
+ - `MemoryWindowHost`, `MemoryWindowHandle`, `MemoryPanel` and `createMemoryWindowHostSetup()` in `@realitycollective/webxr-uiextensions`, an in-memory `WindowHost` and the family's mock. The package shipped `windowHostContractCases()` but nothing to run them against without an engine, so every headless consumer and every new adapter had to write a host first. The mock opens windows on a real `WindowManager`. A window's panel attaches when the test calls `attach(id)`, as on IWSDK, and closing a window through the manager disposes its panel and drops it from `onPanelReady` replay. `dispose()` closes every window it opened and forgets its listeners, as every platform host does. `createMemoryWindowHostSetup()` returns a fresh `WindowHostContractSetup`, so the suite runs in one loop. It passes every contract case, which `test/memory-window-host.test.ts` checks, and it is inside the 100% coverage gate. It came from a team building a native host, whose version passed the suite under Node and Hermes.
15
+ - `dispose()` on `WindowHost`, implemented by every platform host: `IwsdkSceneHost`, `UixWindowHost` (XR Blocks and three.js) and `NativeWindowHost`. It means the same thing everywhere: close every window the host opened through its `WindowManager`, so each goes down the normal close path, then release the host's own subscriptions and listeners, so `onPanelReady` replays nothing afterwards. It is safe to call twice. The XR Blocks host also removes the region groups it added to the scene. The IWSDK host silences its readiness system, which stays registered on the world, and forgets itself, so a later `createSceneHost(world)` builds a fresh host. No host could be torn down before, so an app that rebuilt its UI leaked listeners and scene objects on every platform. `windowHostContractCases()` gains a seventh case that holds every host to it. This adds a required member to the `WindowHost` interface, so a host written outside this repository must implement it.
16
+ - `@realitycollective/native-uiextensions` - the native app adapter, for an OpenXR or visionOS app that embeds a JavaScript engine such as Hermes, renders panels itself and installs `globalThis.__rcHost`, per the Reality Collective native host contract shared across the WebXR family repositories. `NativeWindowHost` implements the core `WindowHost` and `SceneTarget`, as the IWSDK and XR Blocks hosts do, so `applyScene` builds the same scene on native; regions go to the host as `createRegion` and are removed on `dispose`. Bare panels and windows build a tree of internal proxy `UixElement`s over the host's `NativeElementNode` tree, so the existing `data-uix` control upgraders work unchanged; `setProperties` forwards to `host.setProperties`, and `addEventListener` is routed from the host's single `onElementEvent` callback by `(panelId, elementHandle)`. Every `WindowManager` state change (open, focus, minimize, hide, dock, chrome, hand menu) mirrors to the host as `applyWindow(record)`; closing a window disposes its panel (once attached) and calls `closeWindow`. No engine dependency - the package reads and writes only plain data across the host boundary. Ships with an in-memory fake of the `ui` slice under `test/helpers` and runs the shared `windowHostContractCases()` against it.
17
+ - `demos/webxr-multiplatform` - `?uix-engine=<engine>&uix-autostart=1` boots that pipeline at once instead of showing the launch screen. It exists for the post-deploy smoke test, which now loads the desktop and IWSDK pipelines of the deployed lab as well as its launch screen; before this a pipeline that failed to start was invisible to CI, because nothing pressed START. The XR Blocks pipeline is not smoked: xrblocks renders into `<body>` rather than the mount point and logs a `console.error` about the three.js revision it wants, both of which the smoke test counts as failures.
18
+
19
+ ### Changed
20
+
21
+ - `tagOf()` reads nothing for a built-in element on every platform, as its doc always said. The XR Blocks and three.js parser records a built-in tag such as `span` the same way as a custom one, so `tagOf` read `"span"` there and nothing on IWSDK. A custom element name always holds a hyphen, so `tagOf` now returns a name only when it has one.
22
+ - `WindowManager` holds one rule for regions: a window in a region is always world-locked, because the region places it. Opening a window into a region, or `dockTo`, makes it world-locked whatever dock mode it asked for; `dockTo` emits `regionChanged` and then, if the mode changed, `dockChanged`. Setting a follow mode with `setDockMode` or `togglePin` takes a docked window out of its region first. The hosts disagreed before this: the XR Blocks host forced a docked window to world-locked on its own, while the IWSDK and native hosts left the manager's record in the requested follow mode. The same scene descriptor therefore laid out differently by platform. The XR Blocks host's own copy of the rule is gone. The native host now sends the native app the manager's dock mode for a new window, not the one the options asked for.
23
+ - `@realitycollective/iwsdk-uiextensions` - `TouchPointerLike` (the parameter type of `sampleOf`) is exported, and `UITouchGuardSystem` uses the core's `Hand` type rather than a private duplicate. `@realitycollective/xrblocks-uiextensions` exports `UikitComponentLike` (`UixPanelDocument.rootComponent`) and `InteractiveLike` (the return type of `pickInteractive`); `@realitycollective/uix-devtools` exports `CompileOptions` (the options of `compilePanelSource`). Each was a module-private type that appeared in an exported signature, so a consumer had nothing to name and TypeDoc reported it as referenced but undocumented.
24
+
25
+ ### Fixed
26
+
27
+ - `@realitycollective/native-uiextensions` - the `data-uix-*` attributes of a native panel never reached the controls. `NativeElementNode` had no field for them, so on native every control ran on its defaults: a stepper lost its range, step and starting value, a toggle its labels and colours, and every control its `data-uix-id`, so `controls.stepper("count")` found nothing. `NativeElementNode` gains `attributes`, the element's `data-*` attributes as the markup wrote them, and the proxy lifts them into `userData` with `dataAttributeKey()`. The native app must send them.
28
+
29
+ - `demos/webxr-multiplatform` - the IWSDK pipeline failed to start on the deployed lab, for every visitor, with `Failed to resolve module specifier "three-mesh-bvh"`. The Vite config externalised `three-mesh-bvh` alongside the optional integrations xrblocks imports lazily, but `@iwsdk/core` imports it for real, so the IWSDK chunk shipped a bare import the browser cannot resolve. It resolves in the workspace through `@iwsdk/core`'s own dependency and is now bundled like `three-pathfinding`. Found while photographing the live lab for the Reality Collective site.
30
+ - `@realitycollective/iwsdk-uiextensions` - the README and `Examples/controls` still showed the `data-uix="stepper"` / `data-uix-role` attribute form that the core stopped upgrading when controls became custom elements, so the shipped example silently produced no controls. Both now use `<uix-stepper>`, `<uix-toggle>`, `<uix-expandable-label>` and `<uix-log-view>` with their part elements, and both register `uixComponentSet` when creating the world, which IWSDK 0.5 needs before it will parse a panel that uses a control. The README says why.
31
+ - `docs/developer-cycle.md` - the publishing section described GitHub Packages ("no npmjs.com for now"); the packages publish to npmjs.com under the `@realitycollective` scope through `publish-npm.yml`, and the section now describes that workflow, its two dist-tags and the consumer install commands.
32
+
7
33
  ## [0.1.0] - 2026-09-17
8
34
 
9
35
  ### Added
@@ -52,6 +78,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
52
78
  - `@realitycollective/webxr-uiextensions` takes its geometry vocabulary from `@realitycollective/webxr-input` at `^0.1.1` rather than redeclaring it. `Vec3Tuple`, `QuatTuple`, `HeadPose`, `HeadPoseSource` and `PointerSample` are now that package's types, re-exported under the same names, so no import changes for a consumer. `PointerSample` is its `RayTuple`, which is what lets an input provider written against the shared contracts feed this contract unchanged. It is the core's only runtime dependency: the contracts package is engine-free and carries none of its own, and `test/architecture.test.ts` now allows exactly that one name and fails on any other.
53
79
  - `PointerSample` states its ownership rule: a delivered sample belongs to the listener and the source never writes to it again, so the core's hold-to-drag and drag maths may keep a press-time sample without copying. It mirrors the rule `@realitycollective/webxr-input` 0.1.3 writes on `InputSourceSnapshot`, so a provider feeding both contracts has one promise to keep.
54
80
 
81
+ [0.1.1]: https://github.com/realitycollective/WebXR-UIExtensions/compare/v0.1.0...development
55
82
  [0.1.0]: https://github.com/realitycollective/WebXR-UIExtensions/releases/tag/v0.1.0
56
83
 
57
84
  ### Fixed
@@ -23,27 +23,27 @@
23
23
  <div class="panel">
24
24
  <div class="row">
25
25
  <span class="label">Volume</span>
26
- <div data-uix="stepper" data-uix-id="volume" data-uix-min="0" data-uix-max="10" class="stepper">
27
- <div data-uix-role="decrement" class="stepper-button">-</div>
28
- <span data-uix-role="value" class="stepper-value">.</span>
29
- <div data-uix-role="increment" class="stepper-button">+</div>
30
- </div>
26
+ <uix-stepper data-uix-id="volume" data-uix-min="0" data-uix-max="10" class="stepper">
27
+ <uix-decrement class="stepper-button">-</uix-decrement>
28
+ <uix-value class="stepper-value">.</uix-value>
29
+ <uix-increment class="stepper-button">+</uix-increment>
30
+ </uix-stepper>
31
31
  </div>
32
32
  <div class="row">
33
33
  <span class="label">Music</span>
34
- <div data-uix="toggle" data-uix-id="music" data-uix-on-color="#1f6d3d" data-uix-off-color="#5a3037" class="toggle">
35
- <span data-uix-role="label" class="toggle-label">.</span>
36
- </div>
34
+ <uix-toggle data-uix-id="music" data-uix-on-color="#1f6d3d" data-uix-off-color="#5a3037" class="toggle">
35
+ <uix-label class="toggle-label">.</uix-label>
36
+ </uix-toggle>
37
37
  </div>
38
- <div data-uix="expandable-label" data-uix-id="details" data-uix-lines="2" data-uix-chars-per-line="40"
38
+ <uix-expandable-label data-uix-id="details" data-uix-lines="2" data-uix-chars-per-line="40"
39
39
  data-uix-text="Long descriptive text collapses to two lines with an ellipsis, and the more/less affordance expands it in place - no scrolling, no clipping."
40
40
  class="long">
41
- <span data-uix-role="text" class="long-text">.</span>
42
- <div data-uix-role="toggle" class="long-toggle">more</div>
43
- </div>
44
- <div data-uix="log-view" data-uix-id="log" class="log">
45
- <span data-uix-role="line" class="log-line">.</span>
46
- <span data-uix-role="line" class="log-line">.</span>
47
- <span data-uix-role="line" class="log-line">.</span>
48
- </div>
41
+ <uix-text class="long-text">.</uix-text>
42
+ <uix-more class="long-toggle">more</uix-more>
43
+ </uix-expandable-label>
44
+ <uix-log-view data-uix-id="log" class="log">
45
+ <uix-line class="log-line">.</uix-line>
46
+ <uix-line class="log-line">.</uix-line>
47
+ <uix-line class="log-line">.</uix-line>
48
+ </uix-log-view>
49
49
  </div>
@@ -1,19 +1,25 @@
1
1
  /**
2
- * Controls - data-uix markup upgrades on any IWSDK panel.
2
+ * Controls - `<uix-*>` custom elements upgraded on any IWSDK panel.
3
3
  *
4
4
  * The controls work in plain panels too (no window required): the
5
5
  * UIControlsSystem upgrades every loaded PanelDocument.
6
+ *
7
+ * IWSDK 0.5 parses panels against a component schema and rejects any tag it
8
+ * does not know, so a panel that uses a control must register
9
+ * `uixComponentSet` when the world is created. Without it the panel fails to
10
+ * parse and never attaches, with no error in the scene.
6
11
  */
7
12
  import { PanelDocument, PanelUI, UIKitDocument, World, createSystem, eq } from '@iwsdk/core';
8
13
  import {
9
14
  registerUIExtensions,
15
+ uixComponentSet,
10
16
  upgradePanel,
11
17
  type UixElement,
12
18
  } from '@realitycollective/iwsdk-uiextensions';
13
19
 
14
20
  export async function start(container: HTMLDivElement) {
15
21
  const world = await World.create(container, {
16
- features: { spatialUI: true },
22
+ features: { spatialUI: { kit: 'horizon', componentSets: [uixComponentSet] } },
17
23
  });
18
24
  registerUIExtensions(world);
19
25
 
package/README.md CHANGED
@@ -13,7 +13,7 @@ Windowing, docking, layout regions and extra controls for [Meta's Immersive Web
13
13
  | **Dock states** | `world-locked` (place in space) ⇄ `body-follow` (lazy follow) ⇄ `head-locked`, realised with the IWSDK's own `Follower`/`ScreenSpace` |
14
14
  | **Manipulation** | Drag windows by the title bar with the far ray or a near grab (controller squeeze, hand pinch), powered by `@pmndrs/handle`, the same library behind IWSDK grabbing; billboard-while-dragging, drop-to-dock |
15
15
  | **Layout regions** | Named regions (row / column / grid slots) windows snap into; regions can themselves follow the player |
16
- | **Controls** | `data-uix` markup upgrades: **stepper**, **toggle**, **expandable multi-line label**, **log/list view** - plus everything UIKitML already has (buttons, inputs, textareas, images, and the horizon kit's Slider/Checkbox/…) |
16
+ | **Controls** | `<uix-*>` custom-element upgrades: **stepper**, **toggle**, **expandable multi-line label**, **log/list view** - plus everything UIKitML already has (buttons, inputs, textareas, images, and the horizon kit's Slider/Checkbox/…) |
17
17
 
18
18
  Everything is authored in plain UIKitML (HTML/CSS-like) - no new markup language, no custom renderer, no wrapper widgets around things the IWSDK already does.
19
19
 
@@ -85,14 +85,24 @@ Windows are ordinary UIKitML panels; the chrome is discovered by well-known elem
85
85
 
86
86
  ### Controls markup
87
87
 
88
- Annotate any element with `data-uix` and the `UIControlsSystem` upgrades it - in any panel, not just windows:
88
+ A control is a custom element, `<uix-stepper>`, `<uix-toggle>`, `<uix-expandable-label>` or `<uix-log-view>`, with its parts as elements too (`<uix-decrement>`, `<uix-value>`, `<uix-increment>`, `<uix-label>`, `<uix-text>`, `<uix-more>`, `<uix-line>`). Parameters stay `data-uix-*` attributes. The `UIControlsSystem` upgrades every control it finds, in any panel, not just windows:
89
89
 
90
90
  ```html
91
- <div data-uix="stepper" data-uix-id="health" data-uix-min="0" data-uix-max="100" data-uix-step="10">
92
- <button data-uix-role="decrement">-</button>
93
- <span data-uix-role="value">.</span>
94
- <button data-uix-role="increment">+</button>
95
- </div>
91
+ <uix-stepper data-uix-id="health" data-uix-min="0" data-uix-max="100" data-uix-step="10">
92
+ <uix-decrement>-</uix-decrement>
93
+ <uix-value>.</uix-value>
94
+ <uix-increment>+</uix-increment>
95
+ </uix-stepper>
96
+ ```
97
+
98
+ IWSDK 0.5 validates every tag against a component schema, so a world whose panels use controls must register the set, or the panel fails to parse and never attaches:
99
+
100
+ ```ts
101
+ import { uixComponentSet } from '@realitycollective/iwsdk-uiextensions';
102
+
103
+ const world = await World.create(container, {
104
+ features: { spatialUI: { kit: 'horizon', componentSets: [uixComponentSet] } },
105
+ });
96
106
  ```
97
107
 
98
108
  ```ts
@@ -102,7 +112,7 @@ const controls = panelControlsFor(document); // the panel's UIKitDocument
102
112
  controls.stepper('health').events.on('change', (hp) => setHealth(hp));
103
113
  ```
104
114
 
105
- > UIKitML note: every dynamic-text element needs a literal placeholder child (`<span data-uix-role="value">.</span>`) or no Text node is created.
115
+ > UIKitML note: every dynamic-text element needs a literal placeholder child (`<uix-value>.</uix-value>`) or no Text node is created.
106
116
 
107
117
  **Every title-bar button is off by default.** Keep all four in the markup, then enable the ones a window should have with `closable`, `minimizable`, `pinnable` and `dockable` at spawn, or later with `windows.setChrome(id, { pin: true })`. A disabled button is hidden and its click ignored; enabling one needs no rewiring.
108
118
 
@@ -22,6 +22,7 @@
22
22
  */
23
23
  import { htmlComponentSet } from '@drawcall/uikitml';
24
24
  import { z } from 'zod';
25
+ import { dataAttributeKey } from '@realitycollective/webxr-uiextensions';
25
26
  /**
26
27
  * Base container, taken from the parser's own `<div>` definition rather than
27
28
  * imported from `@pmndrs/uikit` directly - that keeps a single copy of uikit
@@ -49,21 +50,6 @@ const ROLE_TAGS = [
49
50
  'uix-text',
50
51
  'uix-more',
51
52
  ];
52
- /**
53
- * `data-uix-min` → `uixMin`, matching how the other parser fills userData.
54
- *
55
- * The parser hands properties over already camelCased, so `data-uix-min`
56
- * arrives as `dataUixMin`; both spellings are normalised here.
57
- */
58
- function toUserDataKey(attribute) {
59
- if (/^data[A-Z]/.test(attribute)) {
60
- const rest = attribute.slice(4);
61
- return rest.charAt(0).toLowerCase() + rest.slice(1);
62
- }
63
- return attribute
64
- .replace(/^data-/, '')
65
- .replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
66
- }
67
53
  /**
68
54
  * A plain uikit Container that records what it was declared as.
69
55
  *
@@ -78,7 +64,7 @@ function controlComponent(tag) {
78
64
  const userData = {};
79
65
  for (const [key, value] of Object.entries(properties)) {
80
66
  if (key.startsWith('data-') || /^data[A-Z]/.test(key)) {
81
- userData[toUserDataKey(key)] = value;
67
+ userData[dataAttributeKey(key)] = value;
82
68
  }
83
69
  else {
84
70
  uikitProps[key] = value;
@@ -1 +1 @@
1
- {"version":3,"file":"component-set.js","sourceRoot":"","sources":["../src/component-set.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EAAE,gBAAgB,EAA+C,MAAM,mBAAmB,CAAC;AAClG,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;GAIG;AACH,MAAM,aAAa,GAAG,gBAAgB,CAAC,KAAK,CAAC;IAC3C,EAAE,SAEH,CAAC;AAEF,8EAA8E;AAC9E,MAAM,YAAY,GAAG;IACnB,aAAa;IACb,YAAY;IACZ,sBAAsB;IACtB,cAAc;CACN,CAAC;AAEX,MAAM,SAAS,GAAG;IAChB,eAAe;IACf,WAAW;IACX,eAAe;IACf,WAAW;IACX,UAAU;IACV,QAAQ;IACR,UAAU;IACV,WAAW;IACX,YAAY;IACZ,UAAU;IACV,UAAU;CACF,CAAC;AAEX;;;;;GAKG;AACH,SAAS,aAAa,CAAC,SAAiB;IACtC,IAAI,YAAY,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;QACjC,MAAM,IAAI,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAChC,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACtD,CAAC;IACD,OAAO,SAAS;SACb,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC;SACrB,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC,EAAE,CAAS,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;AAChE,CAAC;AAED;;;;;;GAMG;AACH,SAAS,gBAAgB,CAAC,GAAW;IACnC,OAAO,MAAM,mBAAoB,SAAQ,aAAa;QACpD,YAAY,aAAsC,EAAE;YAClD,MAAM,UAAU,GAA4B,EAAE,CAAC;YAC/C,MAAM,QAAQ,GAA4B,EAAE,CAAC;YAC7C,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC;gBACtD,IAAI,GAAG,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;oBACtD,QAAQ,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,GAAG,KAAK,CAAC;gBACvC,CAAC;qBAAM,CAAC;oBACN,UAAU,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;gBAC1B,CAAC;YACH,CAAC;YACD,KAAK,CAAC,UAAU,CAAC,CAAC;YAClB,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE;gBACrC,aAAa,EAAE,EAAE,aAAa,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG,EAAE;aACtD,CAAC,CAAC;QACL,CAAC;KACF,CAAC;AACJ,CAAC;AAED,4EAA4E;AAC5E,MAAM,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;AAE1C,SAAS,MAAM,CAAC,GAAW;IACzB,OAAO;QACL,SAAS,EAAE,gBAAgB,CAAC,GAAG,CAAgD;QAC/E,MAAM;QACN,eAAe,EAAE,IAAI;KACtB,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,MAAM,eAAe,GAAiB,MAAM,CAAC,WAAW,CAC7D,CAAC,GAAG,YAAY,EAAE,GAAG,SAAS,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CACjE,CAAC","sourcesContent":["/**\n * The UIKitML component set for `<uix-*>` control elements.\n *\n * IWSDK 0.5 parses panels with `@drawcall/uikitml`, which validates every tag\n * against a component schema and rejects anything it does not know - including\n * every `data-*` attribute on a built-in tag. Declaring the control elements\n * here is the supported way to extend that vocabulary.\n *\n * The three.js and XR Blocks adapters need no equivalent: their parser accepts\n * custom tags unregistered, exposing the tag on\n * `userData.customElement.componentName`. This set reproduces that convention\n * on the IWSDK side - the constructor below stamps the same key and lifts\n * `data-uix-*` attributes into `userData` - so ONE markup file drives every\n * adapter and the engine-free upgraders read it identically everywhere.\n *\n * Register it when creating the world:\n * ```ts\n * const world = await World.create(container, {\n * features: { spatialUI: { kit: 'horizon', componentSets: [uixComponentSet] } },\n * });\n * ```\n */\nimport { htmlComponentSet, type ComponentSet, type ComponentDefinition } from '@drawcall/uikitml';\nimport { z } from 'zod';\n\n/**\n * Base container, taken from the parser's own `<div>` definition rather than\n * imported from `@pmndrs/uikit` directly - that keeps a single copy of uikit\n * in the tree, whichever version `@iwsdk/core` resolves.\n */\nconst BaseContainer = htmlComponentSet['div']\n ?.component as unknown as new (properties?: Record<string, unknown>) => {\n userData: Record<string, unknown>;\n};\n\n/** Control elements an author writes, and the parts that live inside them. */\nconst CONTROL_TAGS = [\n 'uix-stepper',\n 'uix-toggle',\n 'uix-expandable-label',\n 'uix-log-view',\n] as const;\n\nconst ROLE_TAGS = [\n 'uix-decrement',\n 'uix-value',\n 'uix-increment',\n 'uix-label',\n 'uix-line',\n 'uix-up',\n 'uix-down',\n 'uix-clear',\n 'uix-status',\n 'uix-text',\n 'uix-more',\n] as const;\n\n/**\n * `data-uix-min` → `uixMin`, matching how the other parser fills userData.\n *\n * The parser hands properties over already camelCased, so `data-uix-min`\n * arrives as `dataUixMin`; both spellings are normalised here.\n */\nfunction toUserDataKey(attribute: string): string {\n if (/^data[A-Z]/.test(attribute)) {\n const rest = attribute.slice(4);\n return rest.charAt(0).toLowerCase() + rest.slice(1);\n }\n return attribute\n .replace(/^data-/, '')\n .replace(/-([a-z0-9])/g, (_, c: string) => c.toUpperCase());\n}\n\n/**\n * A plain uikit Container that records what it was declared as.\n *\n * Anything the schema let through that is not a uikit property - the\n * `data-uix-*` parameters - is moved into `userData` rather than handed to\n * uikit, which would warn about properties it does not recognise.\n */\nfunction controlComponent(tag: string): typeof BaseContainer {\n return class UixControlContainer extends BaseContainer {\n constructor(properties: Record<string, unknown> = {}) {\n const uikitProps: Record<string, unknown> = {};\n const userData: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(properties)) {\n if (key.startsWith('data-') || /^data[A-Z]/.test(key)) {\n userData[toUserDataKey(key)] = value;\n } else {\n uikitProps[key] = value;\n }\n }\n super(uikitProps);\n Object.assign(this.userData, userData, {\n customElement: { componentName: tag, sourceTag: tag },\n });\n }\n };\n}\n\n/** Permissive: the parameters are ours, and the upgraders validate them. */\nconst schema = z.object({}).passthrough();\n\nfunction define(tag: string): ComponentDefinition {\n return {\n component: controlComponent(tag) as unknown as ComponentDefinition['component'],\n schema,\n canHaveChildren: true,\n };\n}\n\n/**\n * Pass to `spatialUI.componentSets` so `<uix-*>` elements parse on IWSDK 0.5.\n * Without it, a panel using any control fails to parse and never attaches.\n */\nexport const uixComponentSet: ComponentSet = Object.fromEntries(\n [...CONTROL_TAGS, ...ROLE_TAGS].map((tag) => [tag, define(tag)]),\n);\n"]}
1
+ {"version":3,"file":"component-set.js","sourceRoot":"","sources":["../src/component-set.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EAAE,gBAAgB,EAA+C,MAAM,mBAAmB,CAAC;AAClG,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAEzE;;;;GAIG;AACH,MAAM,aAAa,GAAG,gBAAgB,CAAC,KAAK,CAAC;IAC3C,EAAE,SAEH,CAAC;AAEF,8EAA8E;AAC9E,MAAM,YAAY,GAAG;IACnB,aAAa;IACb,YAAY;IACZ,sBAAsB;IACtB,cAAc;CACN,CAAC;AAEX,MAAM,SAAS,GAAG;IAChB,eAAe;IACf,WAAW;IACX,eAAe;IACf,WAAW;IACX,UAAU;IACV,QAAQ;IACR,UAAU;IACV,WAAW;IACX,YAAY;IACZ,UAAU;IACV,UAAU;CACF,CAAC;AAEX;;;;;;GAMG;AACH,SAAS,gBAAgB,CAAC,GAAW;IACnC,OAAO,MAAM,mBAAoB,SAAQ,aAAa;QACpD,YAAY,aAAsC,EAAE;YAClD,MAAM,UAAU,GAA4B,EAAE,CAAC;YAC/C,MAAM,QAAQ,GAA4B,EAAE,CAAC;YAC7C,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC;gBACtD,IAAI,GAAG,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;oBACtD,QAAQ,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC,GAAG,KAAK,CAAC;gBAC1C,CAAC;qBAAM,CAAC;oBACN,UAAU,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;gBAC1B,CAAC;YACH,CAAC;YACD,KAAK,CAAC,UAAU,CAAC,CAAC;YAClB,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE;gBACrC,aAAa,EAAE,EAAE,aAAa,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG,EAAE;aACtD,CAAC,CAAC;QACL,CAAC;KACF,CAAC;AACJ,CAAC;AAED,4EAA4E;AAC5E,MAAM,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;AAE1C,SAAS,MAAM,CAAC,GAAW;IACzB,OAAO;QACL,SAAS,EAAE,gBAAgB,CAAC,GAAG,CAAgD;QAC/E,MAAM;QACN,eAAe,EAAE,IAAI;KACtB,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,MAAM,eAAe,GAAiB,MAAM,CAAC,WAAW,CAC7D,CAAC,GAAG,YAAY,EAAE,GAAG,SAAS,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CACjE,CAAC","sourcesContent":["/**\n * The UIKitML component set for `<uix-*>` control elements.\n *\n * IWSDK 0.5 parses panels with `@drawcall/uikitml`, which validates every tag\n * against a component schema and rejects anything it does not know - including\n * every `data-*` attribute on a built-in tag. Declaring the control elements\n * here is the supported way to extend that vocabulary.\n *\n * The three.js and XR Blocks adapters need no equivalent: their parser accepts\n * custom tags unregistered, exposing the tag on\n * `userData.customElement.componentName`. This set reproduces that convention\n * on the IWSDK side - the constructor below stamps the same key and lifts\n * `data-uix-*` attributes into `userData` - so ONE markup file drives every\n * adapter and the engine-free upgraders read it identically everywhere.\n *\n * Register it when creating the world:\n * ```ts\n * const world = await World.create(container, {\n * features: { spatialUI: { kit: 'horizon', componentSets: [uixComponentSet] } },\n * });\n * ```\n */\nimport { htmlComponentSet, type ComponentSet, type ComponentDefinition } from '@drawcall/uikitml';\nimport { z } from 'zod';\nimport { dataAttributeKey } from '@realitycollective/webxr-uiextensions';\n\n/**\n * Base container, taken from the parser's own `<div>` definition rather than\n * imported from `@pmndrs/uikit` directly - that keeps a single copy of uikit\n * in the tree, whichever version `@iwsdk/core` resolves.\n */\nconst BaseContainer = htmlComponentSet['div']\n ?.component as unknown as new (properties?: Record<string, unknown>) => {\n userData: Record<string, unknown>;\n};\n\n/** Control elements an author writes, and the parts that live inside them. */\nconst CONTROL_TAGS = [\n 'uix-stepper',\n 'uix-toggle',\n 'uix-expandable-label',\n 'uix-log-view',\n] as const;\n\nconst ROLE_TAGS = [\n 'uix-decrement',\n 'uix-value',\n 'uix-increment',\n 'uix-label',\n 'uix-line',\n 'uix-up',\n 'uix-down',\n 'uix-clear',\n 'uix-status',\n 'uix-text',\n 'uix-more',\n] as const;\n\n/**\n * A plain uikit Container that records what it was declared as.\n *\n * Anything the schema let through that is not a uikit property - the\n * `data-uix-*` parameters - is moved into `userData` rather than handed to\n * uikit, which would warn about properties it does not recognise.\n */\nfunction controlComponent(tag: string): typeof BaseContainer {\n return class UixControlContainer extends BaseContainer {\n constructor(properties: Record<string, unknown> = {}) {\n const uikitProps: Record<string, unknown> = {};\n const userData: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(properties)) {\n if (key.startsWith('data-') || /^data[A-Z]/.test(key)) {\n userData[dataAttributeKey(key)] = value;\n } else {\n uikitProps[key] = value;\n }\n }\n super(uikitProps);\n Object.assign(this.userData, userData, {\n customElement: { componentName: tag, sourceTag: tag },\n });\n }\n };\n}\n\n/** Permissive: the parameters are ours, and the upgraders validate them. */\nconst schema = z.object({}).passthrough();\n\nfunction define(tag: string): ComponentDefinition {\n return {\n component: controlComponent(tag) as unknown as ComponentDefinition['component'],\n schema,\n canHaveChildren: true,\n };\n}\n\n/**\n * Pass to `spatialUI.componentSets` so `<uix-*>` elements parse on IWSDK 0.5.\n * Without it, a panel using any control fails to parse and never attaches.\n */\nexport const uixComponentSet: ComponentSet = Object.fromEntries(\n [...CONTROL_TAGS, ...ROLE_TAGS].map((tag) => [tag, define(tag)]),\n);\n"]}
@@ -81,7 +81,15 @@ function buildSceneHost(world) {
81
81
  const readyListeners = new Set();
82
82
  const ready = new Map();
83
83
  const seen = new WeakSet();
84
+ /** Ids of the windows this host spawned, closed by `dispose`. */
85
+ const spawned = new Set();
86
+ let disposed = false;
84
87
  const announce = (entity) => {
88
+ // The readiness system stays registered on the world after `dispose`
89
+ // (IWSDK has no per-system removal here), so it goes quiet instead.
90
+ if (disposed) {
91
+ return;
92
+ }
85
93
  const document = PanelDocument.data.document[entity.index];
86
94
  if (!document || seen.has(document)) {
87
95
  return;
@@ -113,8 +121,10 @@ function buildSceneHost(world) {
113
121
  world.registerSystem(createReadySystem(announce));
114
122
  // A window closed through the manager is gone: stop replaying it to late
115
123
  // subscribers. (The window system destroys the entity on the same event.)
116
- windowManagerFor(world).events.on('closed', ({ id }) => {
124
+ const manager = windowManagerFor(world);
125
+ const stopClosed = manager.events.on('closed', ({ id }) => {
117
126
  ready.delete(id);
127
+ spawned.delete(id);
118
128
  });
119
129
  let windowSequence = 0;
120
130
  const host = {
@@ -126,6 +136,29 @@ function buildSceneHost(world) {
126
136
  throw new Error('[uix] createPanel() is not supported on the IWSDK host - spawn a window ' +
127
137
  '(spawnWindow/createUIWindow) so IWSDK owns the panel lifecycle.');
128
138
  },
139
+ // Leave nothing behind: close every window this host spawned through the
140
+ // world's manager (the window system destroys each entity on that event),
141
+ // silence the readiness system, release the manager subscription, and
142
+ // forget this host so `createSceneHost` builds a fresh one. Safe to call
143
+ // twice.
144
+ dispose() {
145
+ if (disposed) {
146
+ return;
147
+ }
148
+ // The window system opens a window's record only once its panel
149
+ // attaches, so a window still loading has nothing to close yet.
150
+ for (const id of [...spawned]) {
151
+ if (manager.has(id)) {
152
+ manager.close(id);
153
+ }
154
+ }
155
+ disposed = true;
156
+ spawned.clear();
157
+ ready.clear();
158
+ readyListeners.clear();
159
+ stopClosed();
160
+ hosts.delete(world);
161
+ },
129
162
  onPanelReady(listener) {
130
163
  readyListeners.add(listener);
131
164
  for (const event of ready.values()) {
@@ -136,6 +169,7 @@ function buildSceneHost(world) {
136
169
  createWindow(options) {
137
170
  const id = options.id ?? `uix-window-${(windowSequence += 1)}`;
138
171
  const entity = createUIWindow(world, { ...options, id });
172
+ spawned.add(id);
139
173
  return {
140
174
  entity,
141
175
  id,
@@ -1 +1 @@
1
- {"version":3,"file":"scene-host.js","sourceRoot":"","sources":["../src/scene-host.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EACL,aAAa,EACb,OAAO,GAIR,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EACL,YAAY,GASb,MAAM,uCAAuC,CAAC;AAC/C,OAAO,EACL,gBAAgB,EAChB,cAAc,GAEf,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAC3C,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAEzD,kDAAkD;AAClD,SAAS,cAAc,CAAC,QAAuB;IAC7C,OAAO;QACL,IAAI,IAAI;YACN,OAAO,QAAQ,CAAC,WAAoC,CAAC;QACvD,CAAC;QACD,cAAc,EAAE,CAAC,EAAE,EAAE,EAAE,CACpB,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAuB,IAAI,SAAS;QACjE,mBAAmB,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,CACrC,QAAQ,CAAC,mBAAmB,CAAC,KAAK,EAAE,MAAM,CAAC;QAC7C,OAAO,EAAE,GAAG,EAAE;YACZ,uDAAuD;QACzD,CAAC;KACF,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,MAAc;IAC3C,MAAM,QAAQ,GAAG,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAE5C,CAAC;IACd,OAAO,QAAQ,CAAC,CAAC,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AACzD,CAAC;AAED;;;GAGG;AACH,SAAS,iBAAiB,CAAC,OAAiC;IAC1D,OAAO,MAAM,mBAAoB,SAAQ,YAAY,CAAC;QACpD,MAAM,EAAE,EAAE,QAAQ,EAAE,CAAC,OAAO,EAAE,aAAa,CAAC,EAAE;KAC/C,CAAC;QACS,IAAI;YACX,IAAI,CAAC,YAAY,CAAC,IAAI,CACpB,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,SAAS,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CACtE,CAAC;YACF,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;QACpE,CAAC;KACF,CAAC;AACJ,CAAC;AAkBD,MAAM,KAAK,GAAG,IAAI,OAAO,EAAyB,CAAC;AAEnD;;;;;;;;;GASG;AACH,MAAM,UAAU,eAAe,CAAC,KAAY;IAC1C,MAAM,QAAQ,GAAG,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAClC,IAAI,QAAQ,EAAE,CAAC;QACb,OAAO,QAAQ,CAAC;IAClB,CAAC;IACD,MAAM,IAAI,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;IACnC,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IACvB,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,cAAc,CAAC,KAAY;IAClC,MAAM,cAAc,GAAG,IAAI,GAAG,EAAoC,CAAC;IACnE,MAAM,KAAK,GAAG,IAAI,GAAG,EAA2B,CAAC;IACjD,MAAM,IAAI,GAAG,IAAI,OAAO,EAAU,CAAC;IAEnC,MAAM,QAAQ,GAAG,CAAC,MAAc,EAAQ,EAAE;QACxC,MAAM,QAAQ,GAAG,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAE5C,CAAC;QACd,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;YACpC,OAAO;QACT,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACnB,sEAAsE;QACtE,uEAAuE;QACvE,+DAA+D;QAC/D,MAAM,QAAQ,GAAG,MAAM,CAAC,YAAY,CAAC,QAAQ,CAAC;YAC5C,CAAC,CAAE,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAY;YAClD,CAAC,CAAC,EAAE,CAAC;QACP,MAAM,IAAI,GAAuB,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC;QAC/D,MAAM,EAAE,GAAG,QAAQ,IAAK,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAY,CAAC;QACrE,MAAM,KAAK,GAAG,cAAc,CAAC,QAAQ,CAAC,CAAC;QACvC,uEAAuE;QACvE,sEAAsE;QACtE,YAAY,CAAC,QAAQ,EAAE,QAAQ,CAAC,WAAoC,CAAC,CAAC;QACtE,MAAM,KAAK,GAAoB,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;QACnD,KAAK,CAAC,GAAG,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;QACrB,KAAK,MAAM,QAAQ,IAAI,cAAc,EAAE,CAAC;YACtC,IAAI,CAAC;gBACH,QAAQ,CAAC,KAAK,CAAC,CAAC;YAClB,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,OAAO,CAAC,KAAK,CAAC,0CAA0C,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;YACzE,CAAC;QACH,CAAC;IACH,CAAC,CAAC;IAEF,KAAK,CAAC,cAAc,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CAAC,CAAC;IAElD,yEAAyE;IACzE,0EAA0E;IAC1E,gBAAgB,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE;QACrD,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IACnB,CAAC,CAAC,CAAC;IAEH,IAAI,cAAc,GAAG,CAAC,CAAC;IAEvB,MAAM,IAAI,GAAmB;QAC3B,KAAK;QAEL,yEAAyE;QACzE,6DAA6D;QAC7D,wBAAwB,EAAE,KAAK;QAE/B,WAAW;YACT,MAAM,IAAI,KAAK,CACb,0EAA0E;gBACxE,iEAAiE,CACpE,CAAC;QACJ,CAAC;QAED,YAAY,CAAC,QAAQ;YACnB,cAAc,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YAC7B,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC;gBACnC,QAAQ,CAAC,KAAK,CAAC,CAAC;YAClB,CAAC;YACD,OAAO,GAAG,EAAE,CAAC,KAAK,cAAc,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QACpD,CAAC;QAED,YAAY,CAAC,OAA4B;YACvC,MAAM,EAAE,GAAG,OAAO,CAAC,EAAE,IAAI,cAAc,CAAC,cAAc,IAAI,CAAC,CAAC,EAAE,CAAC;YAC/D,MAAM,MAAM,GAAG,cAAc,CAAC,KAAK,EAAE,EAAE,GAAG,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC;YACzD,OAAO;gBACL,MAAM;gBACN,EAAE;gBACF,IAAI,KAAK;oBACP,OAAO,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,KAAK,CAAC;gBAC9B,CAAC;gBACD,OAAO,CAAC,QAAsC;oBAC5C,MAAM,QAAQ,GAAG,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,KAAK,CAAC;oBACtC,IAAI,QAAQ,EAAE,CAAC;wBACb,QAAQ,CAAC,QAAQ,CAAC,CAAC;wBACnB,OAAO,GAAG,EAAE,GAAE,CAAC,CAAC;oBAClB,CAAC;oBACD,mEAAmE;oBACnE,4DAA4D;oBAC5D,MAAM,WAAW,GAAG,IAAI,CAAC,YAAY,CAAC,CAAC,KAAK,EAAE,EAAE;wBAC9C,IAAI,KAAK,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC;4BACpB,WAAW,EAAE,CAAC;4BACd,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;wBACxB,CAAC;oBACH,CAAC,CAAC,CAAC;oBACH,OAAO,WAAW,CAAC;gBACrB,CAAC;aACF,CAAC;QACJ,CAAC;QAED,WAAW,CAAC,MAAmB;YAC7B,gBAAgB,CAAC,KAAK,EAAE;gBACtB,EAAE,EAAE,MAAM,CAAC,EAAE;gBACb,GAAG,CAAC,MAAM,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC3D,GAAG,CAAC,MAAM,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC9D,GAAG,CAAC,MAAM,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACpE,GAAG,CAAC,MAAM,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACvE,GAAG,CAAC,MAAM,CAAC,UAAU,KAAK,SAAS;oBACjC,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,CAAC,UAAU,EAAE;oBACnC,CAAC,CAAC,EAAE,CAAC;gBACP,GAAG,CAAC,MAAM,CAAC,QAAQ,KAAK,SAAS;oBAC/B,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,GAAG,MAAM,CAAC,QAAQ,CAA6B,EAAE;oBAChE,CAAC,CAAC,EAAE,CAAC;gBACP,GAAG,CAAC,MAAM,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACjE,GAAG,CAAC,MAAM,CAAC,YAAY,KAAK,SAAS;oBACnC,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC,GAAG,MAAM,CAAC,YAAY,CAA6B,EAAE;oBACxE,CAAC,CAAC,EAAE,CAAC;aACR,CAAC,CAAC;QACL,CAAC;QAED,WAAW,CAAC,MAAmB;YAC7B,IAAI,CAAC,YAAY,CAAC;gBAChB,EAAE,EAAE,MAAM,CAAC,EAAE;gBACb,KAAK,EAAE,MAAM,CAAC,KAAK;gBACnB,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,GAAG,CAAC,MAAM,CAAC,QAAQ,KAAK,SAAS;oBAC/B,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,GAAG,MAAM,CAAC,QAAQ,CAA6B,EAAE;oBAChE,CAAC,CAAC,EAAE,CAAC;gBACP,GAAG,CAAC,MAAM,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACvE,GAAG,CAAC,MAAM,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC1E,GAAG,CAAC,MAAM,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACvE,GAAG,CAAC,MAAM,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACjE,GAAG,CAAC,MAAM,CAAC,YAAY,KAAK,SAAS;oBACnC,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC,GAAG,MAAM,CAAC,YAAY,CAA6B,EAAE;oBACxE,CAAC,CAAC,EAAE,CAAC;gBACP,GAAG,CAAC,MAAM,CAAC,WAAW,KAAK,SAAS;oBAClC,CAAC,CAAC,EAAE,WAAW,EAAE,MAAM,CAAC,WAAW,EAAE;oBACrC,CAAC,CAAC,EAAE,CAAC;gBACP,GAAG,CAAC,MAAM,CAAC,eAAe,KAAK,SAAS;oBACtC,CAAC,CAAC,EAAE,eAAe,EAAE,MAAM,CAAC,eAAe,EAAE;oBAC7C,CAAC,CAAC,EAAE,CAAC;gBACP,GAAG,CAAC,MAAM,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACpE,GAAG,CAAC,MAAM,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACvE,GAAG,CAAC,MAAM,CAAC,WAAW,KAAK,SAAS;oBAClC,CAAC,CAAC,EAAE,WAAW,EAAE,MAAM,CAAC,WAAW,EAAE;oBACrC,CAAC,CAAC,EAAE,CAAC;gBACP,GAAG,CAAC,MAAM,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACvE,GAAG,CAAC,MAAM,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACvE,GAAG,CAAC,MAAM,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aACxE,CAAC,CAAC;QACL,CAAC;KACF,CAAC;IAEF,OAAO,IAAI,CAAC;AACd,CAAC","sourcesContent":["/**\n * IWSDK implementation of the core's `WindowHost` + `SceneTarget`.\n *\n * This is what makes a portable `SceneDescriptor` build the SAME playground\n * on IWSDK as on plain three.js, and lets app behaviour be wired through\n * `onPanelReady` instead of hand-written ECS queries:\n *\n * const host = createSceneHost(world);\n * applyScene(host, PLAYGROUND);\n * host.onPanelReady(({ id, panel }) => wireMyPanel(id, panel));\n *\n * Readiness is detected with a single ECS query on `PanelUI + PanelDocument`\n * - the engine-specific mechanism stays here, behind the portable interface.\n * Bare panels (a `PanelUI` entity with no `UIWindow`) are announced too, with\n * `kind: 'panel'` and their config path as the id, so devtools and hand-built\n * entities show up in the same stream as managed windows.\n */\nimport {\n PanelDocument,\n PanelUI,\n UIKitDocument,\n type Entity,\n type World,\n} from '@iwsdk/core';\nimport { createSystem } from './create-system.js';\nimport {\n upgradePanel,\n type PanelHandle,\n type PanelReadyEvent,\n type SceneRegion,\n type SceneTarget,\n type SceneWindow,\n type UixElement,\n type WindowHandle,\n type WindowHost,\n} from '@realitycollective/webxr-uiextensions';\nimport {\n createDockRegion,\n createUIWindow,\n type CreateWindowOptions,\n} from './factory.js';\nimport { UIWindow } from './components.js';\nimport { windowManagerFor } from './manager-registry.js';\n\n/** Panel handle over an IWSDK `UIKitDocument`. */\nfunction panelHandleFor(document: UIKitDocument): PanelHandle {\n return {\n get root(): UixElement {\n return document.rootElement as unknown as UixElement;\n },\n getElementById: (id) =>\n (document.getElementById(id) as UixElement | null) ?? undefined,\n setTargetDimensions: (width, height) =>\n document.setTargetDimensions(width, height),\n dispose: () => {\n /* IWSDK owns the document lifecycle via the entity. */\n },\n };\n}\n\n/**\n * The live panel on an entity, or `undefined` while IWSDK is still loading\n * its document. Use it to reach a panel from an entity you already hold;\n * `onPanelReady` stays the way to be TOLD when one appears.\n */\nexport function getPanelHandle(entity: Entity): PanelHandle | undefined {\n const document = PanelDocument.data.document[entity.index] as\n | UIKitDocument\n | undefined;\n return document ? panelHandleFor(document) : undefined;\n}\n\n/**\n * Bridges panel loading into `onPanelReady`. Registered once per host; the\n * query fires as each panel's document is attached by IWSDK's UI system.\n */\nfunction createReadySystem(onReady: (entity: Entity) => void) {\n return class UixPanelReadySystem extends createSystem({\n panels: { required: [PanelUI, PanelDocument] },\n }) {\n override init(): void {\n this.cleanupFuncs.push(\n this.queries.panels.subscribe('qualify', (entity) => onReady(entity)),\n );\n this.queries.panels.entities.forEach((entity) => onReady(entity));\n }\n };\n}\n\n/** A window spawned by {@link IwsdkSceneHost.createWindow}. */\nexport interface IwsdkWindowHandle extends WindowHandle {\n /** The ECS entity carrying `PanelUI`, `UIWindow` and the interaction tags. */\n readonly entity: Entity;\n}\n\nexport interface IwsdkSceneHost extends WindowHost, SceneTarget {\n readonly world: World;\n /**\n * Spawn a managed window and get a handle back straight away. IWSDK loads\n * the markup over later frames, so `handle.panel` is `undefined` at first\n * and `handle.onReady` is how you wait for it without an ECS query.\n */\n createWindow(options: CreateWindowOptions): IwsdkWindowHandle;\n}\n\nconst hosts = new WeakMap<World, IwsdkSceneHost>();\n\n/**\n * The host for a world: built on the first call, and the same instance every\n * call after that. Call AFTER `registerUIExtensions(world)`.\n *\n * One host per world is deliberate. A host owns an ECS system, the record of\n * which documents it has already announced, and the panel upgrade pass, so a\n * second independent host on the same world would run a second copy of all\n * three. Two modules can each ask for the host without coordinating, exactly\n * as they do for the window manager registry.\n */\nexport function createSceneHost(world: World): IwsdkSceneHost {\n const existing = hosts.get(world);\n if (existing) {\n return existing;\n }\n const host = buildSceneHost(world);\n hosts.set(world, host);\n return host;\n}\n\nfunction buildSceneHost(world: World): IwsdkSceneHost {\n const readyListeners = new Set<(event: PanelReadyEvent) => void>();\n const ready = new Map<string, PanelReadyEvent>();\n const seen = new WeakSet<object>();\n\n const announce = (entity: Entity): void => {\n const document = PanelDocument.data.document[entity.index] as\n | UIKitDocument\n | undefined;\n if (!document || seen.has(document)) {\n return;\n }\n seen.add(document);\n // Window id comes from the UIWindow component the factory attached. A\n // panel without one is a bare panel - still worth announcing, keyed by\n // its config path, which is the only stable id IWSDK gives it.\n const windowId = entity.hasComponent(UIWindow)\n ? (UIWindow.data.windowId[entity.index] as string)\n : '';\n const kind: 'window' | 'panel' = windowId ? 'window' : 'panel';\n const id = windowId || (PanelUI.data.config[entity.index] as string);\n const panel = panelHandleFor(document);\n // Upgrade `data-uix` controls exactly as the ECS controls system does,\n // so wiring code sees the same handles on every adapter (idempotent).\n upgradePanel(document, document.rootElement as unknown as UixElement);\n const event: PanelReadyEvent = { id, panel, kind };\n ready.set(id, event);\n for (const listener of readyListeners) {\n try {\n listener(event);\n } catch (error) {\n console.error(`[uix] panel-ready listener failed for \"${id}\":`, error);\n }\n }\n };\n\n world.registerSystem(createReadySystem(announce));\n\n // A window closed through the manager is gone: stop replaying it to late\n // subscribers. (The window system destroys the entity on the same event.)\n windowManagerFor(world).events.on('closed', ({ id }) => {\n ready.delete(id);\n });\n\n let windowSequence = 0;\n\n const host: IwsdkSceneHost = {\n world,\n\n // IWSDK owns panel lifecycles through the ECS, so there is no bare-panel\n // path here - `createPanel` throws rather than half-working.\n supportsStandalonePanels: false,\n\n createPanel(): PanelHandle {\n throw new Error(\n '[uix] createPanel() is not supported on the IWSDK host - spawn a window ' +\n '(spawnWindow/createUIWindow) so IWSDK owns the panel lifecycle.',\n );\n },\n\n onPanelReady(listener) {\n readyListeners.add(listener);\n for (const event of ready.values()) {\n listener(event);\n }\n return () => void readyListeners.delete(listener);\n },\n\n createWindow(options: CreateWindowOptions): IwsdkWindowHandle {\n const id = options.id ?? `uix-window-${(windowSequence += 1)}`;\n const entity = createUIWindow(world, { ...options, id });\n return {\n entity,\n id,\n get panel(): PanelHandle | undefined {\n return ready.get(id)?.panel;\n },\n onReady(listener: (panel: PanelHandle) => void): () => void {\n const existing = ready.get(id)?.panel;\n if (existing) {\n listener(existing);\n return () => {};\n }\n // `onPanelReady` replays panels that are already live, but this id\n // is not one of them, so the callback only ever runs later.\n const unsubscribe = host.onPanelReady((event) => {\n if (event.id === id) {\n unsubscribe();\n listener(event.panel);\n }\n });\n return unsubscribe;\n },\n };\n },\n\n spawnRegion(region: SceneRegion): void {\n createDockRegion(world, {\n id: region.id,\n ...(region.flow !== undefined ? { flow: region.flow } : {}),\n ...(region.pitch !== undefined ? { pitch: region.pitch } : {}),\n ...(region.columns !== undefined ? { columns: region.columns } : {}),\n ...(region.capacity !== undefined ? { capacity: region.capacity } : {}),\n ...(region.snapRadius !== undefined\n ? { snapRadius: region.snapRadius }\n : {}),\n ...(region.position !== undefined\n ? { position: [...region.position] as [number, number, number] }\n : {}),\n ...(region.follow !== undefined ? { follow: region.follow } : {}),\n ...(region.followOffset !== undefined\n ? { followOffset: [...region.followOffset] as [number, number, number] }\n : {}),\n });\n },\n\n spawnWindow(window: SceneWindow): void {\n host.createWindow({\n id: window.id,\n title: window.title,\n config: window.config,\n ...(window.position !== undefined\n ? { position: [...window.position] as [number, number, number] }\n : {}),\n ...(window.maxWidth !== undefined ? { maxWidth: window.maxWidth } : {}),\n ...(window.maxHeight !== undefined ? { maxHeight: window.maxHeight } : {}),\n ...(window.dockMode !== undefined ? { dockMode: window.dockMode } : {}),\n ...(window.region !== undefined ? { region: window.region } : {}),\n ...(window.followOffset !== undefined\n ? { followOffset: [...window.followOffset] as [number, number, number] }\n : {}),\n ...(window.followSpeed !== undefined\n ? { followSpeed: window.followSpeed }\n : {}),\n ...(window.followTolerance !== undefined\n ? { followTolerance: window.followTolerance }\n : {}),\n ...(window.movable !== undefined ? { movable: window.movable } : {}),\n ...(window.closable !== undefined ? { closable: window.closable } : {}),\n ...(window.minimizable !== undefined\n ? { minimizable: window.minimizable }\n : {}),\n ...(window.pinnable !== undefined ? { pinnable: window.pinnable } : {}),\n ...(window.dockable !== undefined ? { dockable: window.dockable } : {}),\n ...(window.handMenu !== undefined ? { handMenu: window.handMenu } : {}),\n });\n },\n };\n\n return host;\n}\n"]}
1
+ {"version":3,"file":"scene-host.js","sourceRoot":"","sources":["../src/scene-host.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * IWSDK implementation of the core's `WindowHost` + `SceneTarget`.\n *\n * This is what makes a portable `SceneDescriptor` build the SAME playground\n * on IWSDK as on plain three.js, and lets app behaviour be wired through\n * `onPanelReady` instead of hand-written ECS queries:\n *\n * const host = createSceneHost(world);\n * applyScene(host, PLAYGROUND);\n * host.onPanelReady(({ id, panel }) => wireMyPanel(id, panel));\n *\n * Readiness is detected with a single ECS query on `PanelUI + PanelDocument`\n * - the engine-specific mechanism stays here, behind the portable interface.\n * Bare panels (a `PanelUI` entity with no `UIWindow`) are announced too, with\n * `kind: 'panel'` and their config path as the id, so devtools and hand-built\n * entities show up in the same stream as managed windows.\n */\nimport {\n PanelDocument,\n PanelUI,\n UIKitDocument,\n type Entity,\n type World,\n} from '@iwsdk/core';\nimport { createSystem } from './create-system.js';\nimport {\n upgradePanel,\n type PanelHandle,\n type PanelReadyEvent,\n type SceneRegion,\n type SceneTarget,\n type SceneWindow,\n type UixElement,\n type WindowHandle,\n type WindowHost,\n} from '@realitycollective/webxr-uiextensions';\nimport {\n createDockRegion,\n createUIWindow,\n type CreateWindowOptions,\n} from './factory.js';\nimport { UIWindow } from './components.js';\nimport { windowManagerFor } from './manager-registry.js';\n\n/** Panel handle over an IWSDK `UIKitDocument`. */\nfunction panelHandleFor(document: UIKitDocument): PanelHandle {\n return {\n get root(): UixElement {\n return document.rootElement as unknown as UixElement;\n },\n getElementById: (id) =>\n (document.getElementById(id) as UixElement | null) ?? undefined,\n setTargetDimensions: (width, height) =>\n document.setTargetDimensions(width, height),\n dispose: () => {\n /* IWSDK owns the document lifecycle via the entity. */\n },\n };\n}\n\n/**\n * The live panel on an entity, or `undefined` while IWSDK is still loading\n * its document. Use it to reach a panel from an entity you already hold;\n * `onPanelReady` stays the way to be TOLD when one appears.\n */\nexport function getPanelHandle(entity: Entity): PanelHandle | undefined {\n const document = PanelDocument.data.document[entity.index] as\n | UIKitDocument\n | undefined;\n return document ? panelHandleFor(document) : undefined;\n}\n\n/**\n * Bridges panel loading into `onPanelReady`. Registered once per host; the\n * query fires as each panel's document is attached by IWSDK's UI system.\n */\nfunction createReadySystem(onReady: (entity: Entity) => void) {\n return class UixPanelReadySystem extends createSystem({\n panels: { required: [PanelUI, PanelDocument] },\n }) {\n override init(): void {\n this.cleanupFuncs.push(\n this.queries.panels.subscribe('qualify', (entity) => onReady(entity)),\n );\n this.queries.panels.entities.forEach((entity) => onReady(entity));\n }\n };\n}\n\n/** A window spawned by {@link IwsdkSceneHost.createWindow}. */\nexport interface IwsdkWindowHandle extends WindowHandle {\n /** The ECS entity carrying `PanelUI`, `UIWindow` and the interaction tags. */\n readonly entity: Entity;\n}\n\nexport interface IwsdkSceneHost extends WindowHost, SceneTarget {\n readonly world: World;\n /**\n * Spawn a managed window and get a handle back straight away. IWSDK loads\n * the markup over later frames, so `handle.panel` is `undefined` at first\n * and `handle.onReady` is how you wait for it without an ECS query.\n */\n createWindow(options: CreateWindowOptions): IwsdkWindowHandle;\n}\n\nconst hosts = new WeakMap<World, IwsdkSceneHost>();\n\n/**\n * The host for a world: built on the first call, and the same instance every\n * call after that. Call AFTER `registerUIExtensions(world)`.\n *\n * One host per world is deliberate. A host owns an ECS system, the record of\n * which documents it has already announced, and the panel upgrade pass, so a\n * second independent host on the same world would run a second copy of all\n * three. Two modules can each ask for the host without coordinating, exactly\n * as they do for the window manager registry.\n */\nexport function createSceneHost(world: World): IwsdkSceneHost {\n const existing = hosts.get(world);\n if (existing) {\n return existing;\n }\n const host = buildSceneHost(world);\n hosts.set(world, host);\n return host;\n}\n\nfunction buildSceneHost(world: World): IwsdkSceneHost {\n const readyListeners = new Set<(event: PanelReadyEvent) => void>();\n const ready = new Map<string, PanelReadyEvent>();\n const seen = new WeakSet<object>();\n /** Ids of the windows this host spawned, closed by `dispose`. */\n const spawned = new Set<string>();\n let disposed = false;\n\n const announce = (entity: Entity): void => {\n // The readiness system stays registered on the world after `dispose`\n // (IWSDK has no per-system removal here), so it goes quiet instead.\n if (disposed) {\n return;\n }\n const document = PanelDocument.data.document[entity.index] as\n | UIKitDocument\n | undefined;\n if (!document || seen.has(document)) {\n return;\n }\n seen.add(document);\n // Window id comes from the UIWindow component the factory attached. A\n // panel without one is a bare panel - still worth announcing, keyed by\n // its config path, which is the only stable id IWSDK gives it.\n const windowId = entity.hasComponent(UIWindow)\n ? (UIWindow.data.windowId[entity.index] as string)\n : '';\n const kind: 'window' | 'panel' = windowId ? 'window' : 'panel';\n const id = windowId || (PanelUI.data.config[entity.index] as string);\n const panel = panelHandleFor(document);\n // Upgrade `data-uix` controls exactly as the ECS controls system does,\n // so wiring code sees the same handles on every adapter (idempotent).\n upgradePanel(document, document.rootElement as unknown as UixElement);\n const event: PanelReadyEvent = { id, panel, kind };\n ready.set(id, event);\n for (const listener of readyListeners) {\n try {\n listener(event);\n } catch (error) {\n console.error(`[uix] panel-ready listener failed for \"${id}\":`, error);\n }\n }\n };\n\n world.registerSystem(createReadySystem(announce));\n\n // A window closed through the manager is gone: stop replaying it to late\n // subscribers. (The window system destroys the entity on the same event.)\n const manager = windowManagerFor(world);\n const stopClosed = manager.events.on('closed', ({ id }) => {\n ready.delete(id);\n spawned.delete(id);\n });\n\n let windowSequence = 0;\n\n const host: IwsdkSceneHost = {\n world,\n\n // IWSDK owns panel lifecycles through the ECS, so there is no bare-panel\n // path here - `createPanel` throws rather than half-working.\n supportsStandalonePanels: false,\n\n createPanel(): PanelHandle {\n throw new Error(\n '[uix] createPanel() is not supported on the IWSDK host - spawn a window ' +\n '(spawnWindow/createUIWindow) so IWSDK owns the panel lifecycle.',\n );\n },\n\n // Leave nothing behind: close every window this host spawned through the\n // world's manager (the window system destroys each entity on that event),\n // silence the readiness system, release the manager subscription, and\n // forget this host so `createSceneHost` builds a fresh one. Safe to call\n // twice.\n dispose(): void {\n if (disposed) {\n return;\n }\n // The window system opens a window's record only once its panel\n // attaches, so a window still loading has nothing to close yet.\n for (const id of [...spawned]) {\n if (manager.has(id)) {\n manager.close(id);\n }\n }\n disposed = true;\n spawned.clear();\n ready.clear();\n readyListeners.clear();\n stopClosed();\n hosts.delete(world);\n },\n\n onPanelReady(listener) {\n readyListeners.add(listener);\n for (const event of ready.values()) {\n listener(event);\n }\n return () => void readyListeners.delete(listener);\n },\n\n createWindow(options: CreateWindowOptions): IwsdkWindowHandle {\n const id = options.id ?? `uix-window-${(windowSequence += 1)}`;\n const entity = createUIWindow(world, { ...options, id });\n spawned.add(id);\n return {\n entity,\n id,\n get panel(): PanelHandle | undefined {\n return ready.get(id)?.panel;\n },\n onReady(listener: (panel: PanelHandle) => void): () => void {\n const existing = ready.get(id)?.panel;\n if (existing) {\n listener(existing);\n return () => {};\n }\n // `onPanelReady` replays panels that are already live, but this id\n // is not one of them, so the callback only ever runs later.\n const unsubscribe = host.onPanelReady((event) => {\n if (event.id === id) {\n unsubscribe();\n listener(event.panel);\n }\n });\n return unsubscribe;\n },\n };\n },\n\n spawnRegion(region: SceneRegion): void {\n createDockRegion(world, {\n id: region.id,\n ...(region.flow !== undefined ? { flow: region.flow } : {}),\n ...(region.pitch !== undefined ? { pitch: region.pitch } : {}),\n ...(region.columns !== undefined ? { columns: region.columns } : {}),\n ...(region.capacity !== undefined ? { capacity: region.capacity } : {}),\n ...(region.snapRadius !== undefined\n ? { snapRadius: region.snapRadius }\n : {}),\n ...(region.position !== undefined\n ? { position: [...region.position] as [number, number, number] }\n : {}),\n ...(region.follow !== undefined ? { follow: region.follow } : {}),\n ...(region.followOffset !== undefined\n ? { followOffset: [...region.followOffset] as [number, number, number] }\n : {}),\n });\n },\n\n spawnWindow(window: SceneWindow): void {\n host.createWindow({\n id: window.id,\n title: window.title,\n config: window.config,\n ...(window.position !== undefined\n ? { position: [...window.position] as [number, number, number] }\n : {}),\n ...(window.maxWidth !== undefined ? { maxWidth: window.maxWidth } : {}),\n ...(window.maxHeight !== undefined ? { maxHeight: window.maxHeight } : {}),\n ...(window.dockMode !== undefined ? { dockMode: window.dockMode } : {}),\n ...(window.region !== undefined ? { region: window.region } : {}),\n ...(window.followOffset !== undefined\n ? { followOffset: [...window.followOffset] as [number, number, number] }\n : {}),\n ...(window.followSpeed !== undefined\n ? { followSpeed: window.followSpeed }\n : {}),\n ...(window.followTolerance !== undefined\n ? { followTolerance: window.followTolerance }\n : {}),\n ...(window.movable !== undefined ? { movable: window.movable } : {}),\n ...(window.closable !== undefined ? { closable: window.closable } : {}),\n ...(window.minimizable !== undefined\n ? { minimizable: window.minimizable }\n : {}),\n ...(window.pinnable !== undefined ? { pinnable: window.pinnable } : {}),\n ...(window.dockable !== undefined ? { dockable: window.dockable } : {}),\n ...(window.handMenu !== undefined ? { handMenu: window.handMenu } : {}),\n });\n },\n };\n\n return host;\n}\n"]}
@@ -81,7 +81,8 @@ export class UIDockRegionSystem extends createSystem({
81
81
  entity.removeComponent(UIDockedTo);
82
82
  return;
83
83
  }
84
- // Docked windows are world-locked by definition (the region may follow).
84
+ // Docked windows are world-locked, the core manager's rule (the region
85
+ // may follow). Mirror it on the component before the record catches up.
85
86
  if (entity.getValue(UIWindow, 'dockMode') !== DockMode.WorldLocked) {
86
87
  entity.setValue(UIWindow, 'dockMode', DockMode.WorldLocked);
87
88
  }
@@ -1 +1 @@
1
- {"version":3,"file":"dock-region-system.js","sourceRoot":"","sources":["../../src/systems/dock-region-system.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * UIDockRegionSystem - keeps the region registry in sync with `UIDockRegion`\n * entities and lays docked windows out in their region's slots each frame.\n *\n * Regions are ordinary entities: give one a `Follower` and its docked\n * windows become a body-locked toolbar; leave it static for a world-locked\n * console wall.\n */\nimport type { Entity } from '@iwsdk/core';\nimport { createSystem } from '../create-system.js';\nimport { Quaternion, Vector3 } from 'three';\nimport { UIDockRegion, UIDockedTo, UIWindow } from '../components.js';\nimport { DockMode } from '@realitycollective/webxr-uiextensions';\nimport type { RegionDefinition, RegionFlow } from '@realitycollective/webxr-uiextensions';\nimport { regionRegistryFor } from '../region-registry-store.js';\nimport { windowManagerFor } from '../manager-registry.js';\nimport type { RegionRegistry } from '@realitycollective/webxr-uiextensions';\n\nlet autoRegionId = 0;\n\nexport class UIDockRegionSystem extends createSystem({\n regions: { required: [UIDockRegion] },\n docked: { required: [UIWindow, UIDockedTo] },\n}) {\n private registry!: RegionRegistry;\n private regionEntities = new Map<string, Entity>();\n private slotPosition = new Vector3();\n private regionQuaternion = new Quaternion();\n\n override init(): void {\n this.registry = regionRegistryFor(this.world);\n\n this.cleanupFuncs.push(\n this.queries.regions.subscribe('qualify', (entity) => this.addRegion(entity)),\n this.queries.regions.subscribe('disqualify', (entity) => this.removeRegion(entity)),\n this.queries.docked.subscribe('qualify', (entity) => this.dockWindow(entity)),\n this.queries.docked.subscribe('disqualify', (entity) => this.undockWindow(entity)),\n );\n this.queries.regions.entities.forEach((entity) => this.addRegion(entity));\n this.queries.docked.entities.forEach((entity) => this.dockWindow(entity));\n }\n\n override update(): void {\n for (const entity of this.queries.docked.entities) {\n const windowId = entity.getValue(UIWindow, 'windowId') as string;\n const offset = this.registry.offsetOf(windowId);\n const regionId = this.registry.regionOf(windowId);\n const regionEntity = regionId === undefined ? undefined : this.regionEntities.get(regionId);\n const regionObject = regionEntity?.object3D;\n const object = entity.object3D;\n if (!offset || !regionObject || !object || !object.parent) {\n continue;\n }\n // Slot offset is region-local; express it in the window's parent space.\n this.slotPosition.set(offset[0], offset[1], offset[2]);\n regionObject.localToWorld(this.slotPosition);\n object.parent.worldToLocal(this.slotPosition);\n object.position.copy(this.slotPosition);\n regionObject.getWorldQuaternion(this.regionQuaternion);\n object.quaternion.copy(this.regionQuaternion);\n }\n }\n\n private addRegion(entity: Entity): void {\n let id = entity.getValue(UIDockRegion, 'regionId') as string;\n if (!id) {\n id = `uix-region-${autoRegionId++}`;\n entity.setValue(UIDockRegion, 'regionId', id);\n }\n if (this.registry.get(id)) {\n return;\n }\n const definition: Partial<RegionDefinition> = {\n flow: entity.getValue(UIDockRegion, 'flow') as RegionFlow,\n pitch: entity.getValue(UIDockRegion, 'pitch') as number,\n columns: Math.max(1, Math.round(entity.getValue(UIDockRegion, 'columns') as number)),\n capacity: Math.max(0, Math.round(entity.getValue(UIDockRegion, 'capacity') as number)),\n snapRadius: entity.getValue(UIDockRegion, 'snapRadius') as number,\n };\n this.registry.register(id, definition);\n this.regionEntities.set(id, entity);\n }\n\n private removeRegion(entity: Entity): void {\n const id = entity.getValue(UIDockRegion, 'regionId') as string;\n if (id && this.regionEntities.get(id) === entity) {\n this.registry.unregister(id);\n this.regionEntities.delete(id);\n }\n }\n\n private dockWindow(entity: Entity): void {\n const windowId = entity.getValue(UIWindow, 'windowId') as string;\n const regionId = entity.getValue(UIDockedTo, 'regionId') as string;\n if (!windowId || !regionId || !this.registry.get(regionId)) {\n // Unknown region (or the window has no id yet) - docking is meaningless.\n entity.removeComponent(UIDockedTo);\n return;\n }\n try {\n this.registry.dock(windowId, regionId);\n } catch {\n // Region is full: reject the dock.\n entity.removeComponent(UIDockedTo);\n return;\n }\n // Docked windows are world-locked by definition (the region may follow).\n if (entity.getValue(UIWindow, 'dockMode') !== DockMode.WorldLocked) {\n entity.setValue(UIWindow, 'dockMode', DockMode.WorldLocked);\n }\n // Write the placement back so the record shows it (a drag-drop dock, or\n // a spawn-docked window). A no-op when the manager asked for it.\n const manager = windowManagerFor(this.world);\n if (manager.has(windowId)) {\n manager.dockTo(windowId, regionId);\n }\n }\n\n private undockWindow(entity: Entity): void {\n const windowId = entity.getValue(UIWindow, 'windowId') as string;\n if (!windowId) {\n return;\n }\n this.registry.undock(windowId);\n // Same write-back on the way out: a drag lifted it, or the dock was\n // rejected (unknown or full region) and the record must not claim it.\n const manager = windowManagerFor(this.world);\n if (manager.has(windowId)) {\n manager.undock(windowId);\n }\n }\n}\n"]}
1
+ {"version":3,"file":"dock-region-system.js","sourceRoot":"","sources":["../../src/systems/dock-region-system.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * UIDockRegionSystem - keeps the region registry in sync with `UIDockRegion`\n * entities and lays docked windows out in their region's slots each frame.\n *\n * Regions are ordinary entities: give one a `Follower` and its docked\n * windows become a body-locked toolbar; leave it static for a world-locked\n * console wall.\n */\nimport type { Entity } from '@iwsdk/core';\nimport { createSystem } from '../create-system.js';\nimport { Quaternion, Vector3 } from 'three';\nimport { UIDockRegion, UIDockedTo, UIWindow } from '../components.js';\nimport { DockMode } from '@realitycollective/webxr-uiextensions';\nimport type { RegionDefinition, RegionFlow } from '@realitycollective/webxr-uiextensions';\nimport { regionRegistryFor } from '../region-registry-store.js';\nimport { windowManagerFor } from '../manager-registry.js';\nimport type { RegionRegistry } from '@realitycollective/webxr-uiextensions';\n\nlet autoRegionId = 0;\n\nexport class UIDockRegionSystem extends createSystem({\n regions: { required: [UIDockRegion] },\n docked: { required: [UIWindow, UIDockedTo] },\n}) {\n private registry!: RegionRegistry;\n private regionEntities = new Map<string, Entity>();\n private slotPosition = new Vector3();\n private regionQuaternion = new Quaternion();\n\n override init(): void {\n this.registry = regionRegistryFor(this.world);\n\n this.cleanupFuncs.push(\n this.queries.regions.subscribe('qualify', (entity) => this.addRegion(entity)),\n this.queries.regions.subscribe('disqualify', (entity) => this.removeRegion(entity)),\n this.queries.docked.subscribe('qualify', (entity) => this.dockWindow(entity)),\n this.queries.docked.subscribe('disqualify', (entity) => this.undockWindow(entity)),\n );\n this.queries.regions.entities.forEach((entity) => this.addRegion(entity));\n this.queries.docked.entities.forEach((entity) => this.dockWindow(entity));\n }\n\n override update(): void {\n for (const entity of this.queries.docked.entities) {\n const windowId = entity.getValue(UIWindow, 'windowId') as string;\n const offset = this.registry.offsetOf(windowId);\n const regionId = this.registry.regionOf(windowId);\n const regionEntity = regionId === undefined ? undefined : this.regionEntities.get(regionId);\n const regionObject = regionEntity?.object3D;\n const object = entity.object3D;\n if (!offset || !regionObject || !object || !object.parent) {\n continue;\n }\n // Slot offset is region-local; express it in the window's parent space.\n this.slotPosition.set(offset[0], offset[1], offset[2]);\n regionObject.localToWorld(this.slotPosition);\n object.parent.worldToLocal(this.slotPosition);\n object.position.copy(this.slotPosition);\n regionObject.getWorldQuaternion(this.regionQuaternion);\n object.quaternion.copy(this.regionQuaternion);\n }\n }\n\n private addRegion(entity: Entity): void {\n let id = entity.getValue(UIDockRegion, 'regionId') as string;\n if (!id) {\n id = `uix-region-${autoRegionId++}`;\n entity.setValue(UIDockRegion, 'regionId', id);\n }\n if (this.registry.get(id)) {\n return;\n }\n const definition: Partial<RegionDefinition> = {\n flow: entity.getValue(UIDockRegion, 'flow') as RegionFlow,\n pitch: entity.getValue(UIDockRegion, 'pitch') as number,\n columns: Math.max(1, Math.round(entity.getValue(UIDockRegion, 'columns') as number)),\n capacity: Math.max(0, Math.round(entity.getValue(UIDockRegion, 'capacity') as number)),\n snapRadius: entity.getValue(UIDockRegion, 'snapRadius') as number,\n };\n this.registry.register(id, definition);\n this.regionEntities.set(id, entity);\n }\n\n private removeRegion(entity: Entity): void {\n const id = entity.getValue(UIDockRegion, 'regionId') as string;\n if (id && this.regionEntities.get(id) === entity) {\n this.registry.unregister(id);\n this.regionEntities.delete(id);\n }\n }\n\n private dockWindow(entity: Entity): void {\n const windowId = entity.getValue(UIWindow, 'windowId') as string;\n const regionId = entity.getValue(UIDockedTo, 'regionId') as string;\n if (!windowId || !regionId || !this.registry.get(regionId)) {\n // Unknown region (or the window has no id yet) - docking is meaningless.\n entity.removeComponent(UIDockedTo);\n return;\n }\n try {\n this.registry.dock(windowId, regionId);\n } catch {\n // Region is full: reject the dock.\n entity.removeComponent(UIDockedTo);\n return;\n }\n // Docked windows are world-locked, the core manager's rule (the region\n // may follow). Mirror it on the component before the record catches up.\n if (entity.getValue(UIWindow, 'dockMode') !== DockMode.WorldLocked) {\n entity.setValue(UIWindow, 'dockMode', DockMode.WorldLocked);\n }\n // Write the placement back so the record shows it (a drag-drop dock, or\n // a spawn-docked window). A no-op when the manager asked for it.\n const manager = windowManagerFor(this.world);\n if (manager.has(windowId)) {\n manager.dockTo(windowId, regionId);\n }\n }\n\n private undockWindow(entity: Entity): void {\n const windowId = entity.getValue(UIWindow, 'windowId') as string;\n if (!windowId) {\n return;\n }\n this.registry.undock(windowId);\n // Same write-back on the way out: a drag lifted it, or the dock was\n // rejected (unknown or full region) and the record must not claim it.\n const manager = windowManagerFor(this.world);\n if (manager.has(windowId)) {\n manager.undock(windowId);\n }\n }\n}\n"]}
@@ -26,10 +26,13 @@
26
26
  */
27
27
  import { Types } from '@iwsdk/core';
28
28
  import { Vector3, type Object3D } from 'three';
29
- import { TouchPress, type TouchSample } from '@realitycollective/webxr-uiextensions';
30
- type Handedness = 'left' | 'right';
31
- /** The slice of a pointer-events `Pointer` this system drives. */
32
- interface TouchPointerLike {
29
+ import { type Hand, TouchPress, type TouchSample } from '@realitycollective/webxr-uiextensions';
30
+ /**
31
+ * The slice of a pointer-events `Pointer` this system drives. Exported
32
+ * because it is the parameter type of `sampleOf`, so a caller feeding the
33
+ * sampler from a fake pointer in a test has a name for the shape.
34
+ */
35
+ export interface TouchPointerLike {
33
36
  down(event: {
34
37
  timeStamp: number;
35
38
  button: number;
@@ -86,7 +89,7 @@ export declare class UITouchGuardSystem extends UITouchGuardSystem_base {
86
89
  init(): void;
87
90
  update(_delta: number, time: number): void;
88
91
  /** The state machine for a hand, for inspection and tests. */
89
- pressFor(hand: Handedness): TouchPress<Object3D> | undefined;
92
+ pressFor(hand: Hand): TouchPress<Object3D> | undefined;
90
93
  }
91
94
  /**
92
95
  * This frame's signed distance from the fingertip to the surface it is over,
@@ -1 +1 @@
1
- {"version":3,"file":"touch-guard-system.js","sourceRoot":"","sources":["../../src/systems/touch-guard-system.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * UITouchGuardSystem - press / hold / release for IWSDK's near (poke)\n * pointers, driven by the core's `TouchPress` state machine.\n *\n * What IWSDK does on its own. Its touch pointer (`@pmndrs/pointer-events`)\n * presses when the fingertip's distance to a panel drops under 2 cm and\n * releases when it rises above, and that distance has no sign: uikit's panel\n * spherecast projects the fingertip onto the panel plane and measures how\n * far it is, either side. A finger pushed through a panel and pulled back\n * therefore fires press, release, press, release - two clicks for one\n * gesture - and a finger arriving from behind presses too.\n *\n * What this system does. It takes over the two touch pointers' `down` and\n * `up`: IWSDK's own calls become no-ops, and each frame (right after\n * `InputSystem` has moved the pointers) the fingertip's SIGNED distance to\n * what it is over is fed to a `TouchPress`, whose transitions call the\n * original `down` / `up`. The pointer still delivers `pointerdown`,\n * `pointerup` and `click` to the element exactly as before, so nothing\n * downstream changes; only when they fire does. The guarantees are the\n * machine's: a press only from the front, one press per touch until a\n * release, release on coming back out or on losing contact.\n *\n * Every poke target in the app benefits, IWSDK's own panels included, since\n * they share the same two pointers. Off with\n * `registerUIExtensions(world, { touchGuard: false })`.\n */\nimport { Types } from '@iwsdk/core';\nimport { createSystem } from '../create-system.js';\nimport { Quaternion, Vector3, type Object3D } from 'three';\nimport {\n DEFAULT_TOUCH_PRESS,\n TouchPress,\n type TouchSample,\n} from '@realitycollective/webxr-uiextensions';\n\ntype Handedness = 'left' | 'right';\n\n/** The slice of a pointer-events `Pointer` this system drives. */\ninterface TouchPointerLike {\n down(event: { timeStamp: number; button: number }): void;\n up(event: { timeStamp: number; button: number }): void;\n getIntersection():\n | {\n object: Object3D & { isVoidObject?: boolean };\n point: Vector3;\n pointerPosition?: Vector3;\n normal?: Vector3;\n distance: number;\n }\n | undefined;\n}\n\ninterface Guard {\n pointer: TouchPointerLike;\n press: TouchPress<Object3D>;\n down: TouchPointerLike['down'];\n up: TouchPointerLike['up'];\n}\n\nconst HANDS: readonly Handedness[] = ['left', 'right'];\nconst HELPER_NORMAL = new Vector3();\nconst HELPER_OFFSET = new Vector3();\nconst HELPER_QUATERNION = new Quaternion();\n\nexport class UITouchGuardSystem extends createSystem(\n {},\n {\n /** Signed distance in front (meters) at or under which a touch presses. */\n pressDistance: { type: Types.Float32, default: DEFAULT_TOUCH_PRESS.pressDistance },\n /** Signed distance in front the finger must come back past to release. */\n releaseDistance: { type: Types.Float32, default: DEFAULT_TOUCH_PRESS.releaseDistance },\n /** Let a fingertip arriving from behind a panel press. */\n allowFromBehind: { type: Types.Boolean, default: DEFAULT_TOUCH_PRESS.allowFromBehind },\n },\n) {\n private guards = new Map<Handedness, Guard>();\n\n override init(): void {\n const xr = (this.input as typeof this.input | undefined)?.xr;\n if (!xr) {\n return; // headless, or no XR input: nothing to guard\n }\n for (const hand of HANDS) {\n const pointer = xr.multiPointers[hand].getPointer('touch') as unknown as TouchPointerLike;\n const guard: Guard = {\n pointer,\n press: new TouchPress<Object3D>({\n pressDistance: this.config.pressDistance.value,\n releaseDistance: this.config.releaseDistance.value,\n allowFromBehind: this.config.allowFromBehind.value,\n }),\n down: pointer.down.bind(pointer),\n up: pointer.up.bind(pointer),\n };\n // IWSDK's unsigned-distance press and its forced release on lost hover\n // both land here and are dropped; the machine below decides instead.\n pointer.down = () => {};\n pointer.up = () => {};\n this.guards.set(hand, guard);\n }\n this.cleanupFuncs.push(() => {\n for (const guard of this.guards.values()) {\n guard.pointer.down = guard.down;\n guard.pointer.up = guard.up;\n guard.press.reset();\n }\n this.guards.clear();\n });\n }\n\n override update(_delta: number, time: number): void {\n const event = { timeStamp: time * 1000, button: 0 };\n for (const guard of this.guards.values()) {\n const result = guard.press.update(sampleOf(guard.pointer));\n if (result.pressed) {\n guard.down(event);\n }\n if (result.released) {\n guard.up(event);\n }\n }\n }\n\n /** The state machine for a hand, for inspection and tests. */\n pressFor(hand: Handedness): TouchPress<Object3D> | undefined {\n return this.guards.get(hand)?.press;\n }\n}\n\n/**\n * This frame's signed distance from the fingertip to the surface it is over,\n * positive in front. The surface normal is the intersection's local normal\n * (uikit panels report +Z) taken to world space; a target with no normal is\n * treated as faced from the front, so a plain mesh keeps IWSDK's behaviour.\n */\nexport function sampleOf(pointer: TouchPointerLike): TouchSample<Object3D> | undefined {\n const intersection = pointer.getIntersection();\n if (!intersection || intersection.object.isVoidObject) {\n return undefined;\n }\n const { object, point, pointerPosition, normal } = intersection;\n if (!normal || !pointerPosition) {\n return { signedDistance: intersection.distance, target: object };\n }\n object.getWorldQuaternion(HELPER_QUATERNION);\n HELPER_NORMAL.copy(normal).applyQuaternion(HELPER_QUATERNION).normalize();\n HELPER_OFFSET.copy(pointerPosition).sub(point);\n return { signedDistance: HELPER_OFFSET.dot(HELPER_NORMAL), target: object };\n}\n"]}
1
+ {"version":3,"file":"touch-guard-system.js","sourceRoot":"","sources":["../../src/systems/touch-guard-system.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * UITouchGuardSystem - press / hold / release for IWSDK's near (poke)\n * pointers, driven by the core's `TouchPress` state machine.\n *\n * What IWSDK does on its own. Its touch pointer (`@pmndrs/pointer-events`)\n * presses when the fingertip's distance to a panel drops under 2 cm and\n * releases when it rises above, and that distance has no sign: uikit's panel\n * spherecast projects the fingertip onto the panel plane and measures how\n * far it is, either side. A finger pushed through a panel and pulled back\n * therefore fires press, release, press, release - two clicks for one\n * gesture - and a finger arriving from behind presses too.\n *\n * What this system does. It takes over the two touch pointers' `down` and\n * `up`: IWSDK's own calls become no-ops, and each frame (right after\n * `InputSystem` has moved the pointers) the fingertip's SIGNED distance to\n * what it is over is fed to a `TouchPress`, whose transitions call the\n * original `down` / `up`. The pointer still delivers `pointerdown`,\n * `pointerup` and `click` to the element exactly as before, so nothing\n * downstream changes; only when they fire does. The guarantees are the\n * machine's: a press only from the front, one press per touch until a\n * release, release on coming back out or on losing contact.\n *\n * Every poke target in the app benefits, IWSDK's own panels included, since\n * they share the same two pointers. Off with\n * `registerUIExtensions(world, { touchGuard: false })`.\n */\nimport { Types } from '@iwsdk/core';\nimport { createSystem } from '../create-system.js';\nimport { Quaternion, Vector3, type Object3D } from 'three';\nimport {\n DEFAULT_TOUCH_PRESS,\n type Hand,\n TouchPress,\n type TouchSample,\n} from '@realitycollective/webxr-uiextensions';\n\n/**\n * The slice of a pointer-events `Pointer` this system drives. Exported\n * because it is the parameter type of `sampleOf`, so a caller feeding the\n * sampler from a fake pointer in a test has a name for the shape.\n */\nexport interface TouchPointerLike {\n down(event: { timeStamp: number; button: number }): void;\n up(event: { timeStamp: number; button: number }): void;\n getIntersection():\n | {\n object: Object3D & { isVoidObject?: boolean };\n point: Vector3;\n pointerPosition?: Vector3;\n normal?: Vector3;\n distance: number;\n }\n | undefined;\n}\n\ninterface Guard {\n pointer: TouchPointerLike;\n press: TouchPress<Object3D>;\n down: TouchPointerLike['down'];\n up: TouchPointerLike['up'];\n}\n\nconst HANDS: readonly Hand[] = ['left', 'right'];\nconst HELPER_NORMAL = new Vector3();\nconst HELPER_OFFSET = new Vector3();\nconst HELPER_QUATERNION = new Quaternion();\n\nexport class UITouchGuardSystem extends createSystem(\n {},\n {\n /** Signed distance in front (meters) at or under which a touch presses. */\n pressDistance: { type: Types.Float32, default: DEFAULT_TOUCH_PRESS.pressDistance },\n /** Signed distance in front the finger must come back past to release. */\n releaseDistance: { type: Types.Float32, default: DEFAULT_TOUCH_PRESS.releaseDistance },\n /** Let a fingertip arriving from behind a panel press. */\n allowFromBehind: { type: Types.Boolean, default: DEFAULT_TOUCH_PRESS.allowFromBehind },\n },\n) {\n private guards = new Map<Hand, Guard>();\n\n override init(): void {\n const xr = (this.input as typeof this.input | undefined)?.xr;\n if (!xr) {\n return; // headless, or no XR input: nothing to guard\n }\n for (const hand of HANDS) {\n const pointer = xr.multiPointers[hand].getPointer('touch') as unknown as TouchPointerLike;\n const guard: Guard = {\n pointer,\n press: new TouchPress<Object3D>({\n pressDistance: this.config.pressDistance.value,\n releaseDistance: this.config.releaseDistance.value,\n allowFromBehind: this.config.allowFromBehind.value,\n }),\n down: pointer.down.bind(pointer),\n up: pointer.up.bind(pointer),\n };\n // IWSDK's unsigned-distance press and its forced release on lost hover\n // both land here and are dropped; the machine below decides instead.\n pointer.down = () => {};\n pointer.up = () => {};\n this.guards.set(hand, guard);\n }\n this.cleanupFuncs.push(() => {\n for (const guard of this.guards.values()) {\n guard.pointer.down = guard.down;\n guard.pointer.up = guard.up;\n guard.press.reset();\n }\n this.guards.clear();\n });\n }\n\n override update(_delta: number, time: number): void {\n const event = { timeStamp: time * 1000, button: 0 };\n for (const guard of this.guards.values()) {\n const result = guard.press.update(sampleOf(guard.pointer));\n if (result.pressed) {\n guard.down(event);\n }\n if (result.released) {\n guard.up(event);\n }\n }\n }\n\n /** The state machine for a hand, for inspection and tests. */\n pressFor(hand: Hand): TouchPress<Object3D> | undefined {\n return this.guards.get(hand)?.press;\n }\n}\n\n/**\n * This frame's signed distance from the fingertip to the surface it is over,\n * positive in front. The surface normal is the intersection's local normal\n * (uikit panels report +Z) taken to world space; a target with no normal is\n * treated as faced from the front, so a plain mesh keeps IWSDK's behaviour.\n */\nexport function sampleOf(pointer: TouchPointerLike): TouchSample<Object3D> | undefined {\n const intersection = pointer.getIntersection();\n if (!intersection || intersection.object.isVoidObject) {\n return undefined;\n }\n const { object, point, pointerPosition, normal } = intersection;\n if (!normal || !pointerPosition) {\n return { signedDistance: intersection.distance, target: object };\n }\n object.getWorldQuaternion(HELPER_QUATERNION);\n HELPER_NORMAL.copy(normal).applyQuaternion(HELPER_QUATERNION).normalize();\n HELPER_OFFSET.copy(pointerPosition).sub(point);\n return { signedDistance: HELPER_OFFSET.dot(HELPER_NORMAL), target: object };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@realitycollective/iwsdk-uiextensions",
3
- "version": "0.1.0",
3
+ "version": "0.1.1-preview.0",
4
4
  "description": "Meta IWSDK adapter for the Reality Collective UI Extensions - windowing, docking, layout regions and extra controls, binding the engine-free @realitycollective/webxr-uiextensions core onto IWSDK's ECS, UIKitML and interaction systems.",
5
5
  "keywords": [
6
6
  "realitycollective",
@@ -39,7 +39,7 @@
39
39
  "@drawcall/uikitml": "0.1.8",
40
40
  "zod": "^4.4.3",
41
41
  "@pmndrs/handle": "^6.6.16",
42
- "@realitycollective/webxr-uiextensions": "^0.1.0"
42
+ "@realitycollective/webxr-uiextensions": "^0.1.1-preview.0"
43
43
  },
44
44
  "peerDependencies": {
45
45
  "@iwsdk/core": ">=0.5.0 <0.6.0",