@realitycollective/xrblocks-uiextensions 0.1.0-preview.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,72 @@
1
+ /**
2
+ * UixPanelDocument - minimal spatial hosting for interpreted UIKitML on
3
+ * plain three.js (the XR Blocks scene graph is plain three.js).
4
+ *
5
+ * Mirrors the behaviour of IWSDK's UIKitDocument where it matters to the
6
+ * core: a `Group` wrapping the interpreted component tree, per-frame
7
+ * `update`, scale-to-fit via {@link fitScale}, and `getElementById` backed
8
+ * by the components' `userData.id` (where UIKitML places markup ids -
9
+ * the same `userData` surface the core's `UixElement` contract reads).
10
+ */
11
+ import { Group } from 'three';
12
+ import { interpret } from '@pmndrs/uikitml';
13
+ import { fitScale } from './scale-math.js';
14
+ function naturalSize(component) {
15
+ const value = component.size?.value;
16
+ if (Array.isArray(value) && value.length >= 2) {
17
+ return [value[0], value[1]];
18
+ }
19
+ return [undefined, undefined];
20
+ }
21
+ export class UixPanelDocument extends Group {
22
+ rootComponent;
23
+ targetWidth = 1;
24
+ targetHeight = 1;
25
+ unsubscribeSize;
26
+ constructor(configJson, kit) {
27
+ super();
28
+ const component = interpret(configJson, kit);
29
+ if (!component) {
30
+ throw new Error('UIKitML config produced no interpretable root element.');
31
+ }
32
+ this.rootComponent = component;
33
+ this.add(this.rootComponent);
34
+ // Re-fit whenever layout measures the panel (size arrives async).
35
+ this.unsubscribeSize = this.rootComponent.size?.subscribe?.(() => this.applyScale());
36
+ }
37
+ get root() {
38
+ return this.rootComponent;
39
+ }
40
+ getElementById(id) {
41
+ let found;
42
+ this.rootComponent.traverse((object) => {
43
+ if (!found && object.userData?.['id'] === id) {
44
+ found = object;
45
+ }
46
+ });
47
+ return found;
48
+ }
49
+ setTargetDimensions(width, height) {
50
+ this.targetWidth = width;
51
+ this.targetHeight = height;
52
+ this.applyScale();
53
+ }
54
+ /** Drive per-frame - uikit expects milliseconds. */
55
+ update(deltaSeconds) {
56
+ this.rootComponent.update?.(deltaSeconds * 1000);
57
+ }
58
+ dispose() {
59
+ this.unsubscribeSize?.();
60
+ this.unsubscribeSize = undefined;
61
+ this.removeFromParent();
62
+ this.rootComponent.dispose?.();
63
+ }
64
+ applyScale() {
65
+ const [naturalWidth, naturalHeight] = naturalSize(this.rootComponent);
66
+ const scale = fitScale(this.targetWidth, this.targetHeight, naturalWidth, naturalHeight);
67
+ if (scale !== undefined) {
68
+ this.scale.setScalar(scale);
69
+ }
70
+ }
71
+ }
72
+ //# sourceMappingURL=panel-document.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"panel-document.js","sourceRoot":"","sources":["../src/panel-document.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * UixPanelDocument - minimal spatial hosting for interpreted UIKitML on\n * plain three.js (the XR Blocks scene graph is plain three.js).\n *\n * Mirrors the behaviour of IWSDK's UIKitDocument where it matters to the\n * core: a `Group` wrapping the interpreted component tree, per-frame\n * `update`, scale-to-fit via {@link fitScale}, and `getElementById` backed\n * by the components' `userData.id` (where UIKitML places markup ids -\n * the same `userData` surface the core's `UixElement` contract reads).\n */\nimport { Group, type Object3D } from 'three';\nimport { interpret, type Kit } from '@pmndrs/uikitml';\nimport type { PanelHandle, UixElement } from '@realitycollective/webxr-uiextensions';\nimport { fitScale } from './scale-math.js';\n\n/** The slice of a uikit component this adapter relies on. */\ninterface UikitComponentLike extends Object3D {\n update?(deltaMs: number): void;\n size?: {\n value: unknown;\n subscribe?(listener: () => void): () => void;\n };\n dispose?(): void;\n}\n\nfunction naturalSize(\n component: UikitComponentLike,\n): [number | undefined, number | undefined] {\n const value = component.size?.value;\n if (Array.isArray(value) && value.length >= 2) {\n return [value[0] as number, value[1] as number];\n }\n return [undefined, undefined];\n}\n\nexport class UixPanelDocument extends Group implements PanelHandle {\n readonly rootComponent: UikitComponentLike;\n private targetWidth = 1;\n private targetHeight = 1;\n private unsubscribeSize: (() => void) | undefined;\n\n constructor(configJson: unknown, kit?: Kit) {\n super();\n const component = interpret(\n configJson as Parameters<typeof interpret>[0],\n kit,\n );\n if (!component) {\n throw new Error('UIKitML config produced no interpretable root element.');\n }\n this.rootComponent = component as unknown as UikitComponentLike;\n this.add(this.rootComponent);\n // Re-fit whenever layout measures the panel (size arrives async).\n this.unsubscribeSize = this.rootComponent.size?.subscribe?.(() =>\n this.applyScale(),\n );\n }\n\n get root(): UixElement {\n return this.rootComponent as unknown as UixElement;\n }\n\n getElementById(id: string): UixElement | undefined {\n let found: UixElement | undefined;\n this.rootComponent.traverse((object: Object3D) => {\n if (!found && object.userData?.['id'] === id) {\n found = object as unknown as UixElement;\n }\n });\n return found;\n }\n\n setTargetDimensions(width: number, height: number): void {\n this.targetWidth = width;\n this.targetHeight = height;\n this.applyScale();\n }\n\n /** Drive per-frame - uikit expects milliseconds. */\n update(deltaSeconds: number): void {\n this.rootComponent.update?.(deltaSeconds * 1000);\n }\n\n dispose(): void {\n this.unsubscribeSize?.();\n this.unsubscribeSize = undefined;\n this.removeFromParent();\n this.rootComponent.dispose?.();\n }\n\n private applyScale(): void {\n const [naturalWidth, naturalHeight] = naturalSize(this.rootComponent);\n const scale = fitScale(\n this.targetWidth,\n this.targetHeight,\n naturalWidth,\n naturalHeight,\n );\n if (scale !== undefined) {\n this.scale.setScalar(scale);\n }\n }\n}\n"]}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Minimal pointer forwarding - turns engine raycast hits into uikit events.
3
+ *
4
+ * XR Blocks (and any plain three.js app) raycasts with its own machinery;
5
+ * uikit components are ordinary three.js Meshes, so they appear in those
6
+ * intersections. This module picks the correct interactive element from a
7
+ * hit list and dispatches DOM-ish events to it. v1 forwards clicks and
8
+ * hover enter/leave - enough for window chrome and `data-uix` controls.
9
+ * (Full pointer capture / poke semantics: use `@pmndrs/pointer-events`,
10
+ * which is what IWSDK itself builds on.)
11
+ */
12
+ import type { Object3D } from 'three';
13
+ /** The event surface uikit components expose (three.js EventDispatcher). */
14
+ interface InteractiveLike {
15
+ dispatchEvent?(event: {
16
+ type: string;
17
+ [key: string]: unknown;
18
+ }): void;
19
+ userData?: Record<string, unknown>;
20
+ parent?: unknown;
21
+ }
22
+ /**
23
+ * Walk up from a hit object to the nearest ancestor that can receive
24
+ * events and belongs to a uikit tree (identified by the `dataUid` UIKitML
25
+ * stamps into userData). Pure - testable with stub objects.
26
+ */
27
+ export declare function pickInteractive(hit: unknown): InteractiveLike | undefined;
28
+ /**
29
+ * Dispatch a click to the interactive element (if any) under the first hit
30
+ * of an intersection list (`Raycaster.intersectObject(panel, true)` order).
31
+ * Returns the element that received the event, for host-side bookkeeping.
32
+ */
33
+ export declare function forwardClick(intersections: ReadonlyArray<{
34
+ object: Object3D;
35
+ }>): InteractiveLike | undefined;
36
+ export {};
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Walk up from a hit object to the nearest ancestor that can receive
3
+ * events and belongs to a uikit tree (identified by the `dataUid` UIKitML
4
+ * stamps into userData). Pure - testable with stub objects.
5
+ */
6
+ export function pickInteractive(hit) {
7
+ let current = hit;
8
+ while (current) {
9
+ if (typeof current.dispatchEvent === 'function' &&
10
+ current.userData &&
11
+ current.userData['dataUid'] !== undefined) {
12
+ return current;
13
+ }
14
+ current = current.parent;
15
+ }
16
+ return undefined;
17
+ }
18
+ /**
19
+ * Dispatch a click to the interactive element (if any) under the first hit
20
+ * of an intersection list (`Raycaster.intersectObject(panel, true)` order).
21
+ * Returns the element that received the event, for host-side bookkeeping.
22
+ */
23
+ export function forwardClick(intersections) {
24
+ const first = intersections[0];
25
+ if (!first) {
26
+ return undefined;
27
+ }
28
+ const target = pickInteractive(first.object);
29
+ target?.dispatchEvent?.({ type: 'click' });
30
+ return target;
31
+ }
32
+ //# sourceMappingURL=pointer-forward.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pointer-forward.js","sourceRoot":"","sources":["../src/pointer-forward.ts"],"names":[],"mappings":"AAoBA;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,GAAY;IAC1C,IAAI,OAAO,GAAG,GAAkC,CAAC;IACjD,OAAO,OAAO,EAAE,CAAC;QACf,IACE,OAAO,OAAO,CAAC,aAAa,KAAK,UAAU;YAC3C,OAAO,CAAC,QAAQ;YAChB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,KAAK,SAAS,EACzC,CAAC;YACD,OAAO,OAAO,CAAC;QACjB,CAAC;QACD,OAAO,GAAG,OAAO,CAAC,MAAqC,CAAC;IAC1D,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAC1B,aAAkD;IAElD,MAAM,KAAK,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC;IAC/B,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,MAAM,MAAM,GAAG,eAAe,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAC7C,MAAM,EAAE,aAAa,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;IAC3C,OAAO,MAAM,CAAC;AAChB,CAAC","sourcesContent":["/**\n * Minimal pointer forwarding - turns engine raycast hits into uikit events.\n *\n * XR Blocks (and any plain three.js app) raycasts with its own machinery;\n * uikit components are ordinary three.js Meshes, so they appear in those\n * intersections. This module picks the correct interactive element from a\n * hit list and dispatches DOM-ish events to it. v1 forwards clicks and\n * hover enter/leave - enough for window chrome and `data-uix` controls.\n * (Full pointer capture / poke semantics: use `@pmndrs/pointer-events`,\n * which is what IWSDK itself builds on.)\n */\nimport type { Object3D } from 'three';\n\n/** The event surface uikit components expose (three.js EventDispatcher). */\ninterface InteractiveLike {\n dispatchEvent?(event: { type: string; [key: string]: unknown }): void;\n userData?: Record<string, unknown>;\n parent?: unknown;\n}\n\n/**\n * Walk up from a hit object to the nearest ancestor that can receive\n * events and belongs to a uikit tree (identified by the `dataUid` UIKitML\n * stamps into userData). Pure - testable with stub objects.\n */\nexport function pickInteractive(hit: unknown): InteractiveLike | undefined {\n let current = hit as InteractiveLike | undefined;\n while (current) {\n if (\n typeof current.dispatchEvent === 'function' &&\n current.userData &&\n current.userData['dataUid'] !== undefined\n ) {\n return current;\n }\n current = current.parent as InteractiveLike | undefined;\n }\n return undefined;\n}\n\n/**\n * Dispatch a click to the interactive element (if any) under the first hit\n * of an intersection list (`Raycaster.intersectObject(panel, true)` order).\n * Returns the element that received the event, for host-side bookkeeping.\n */\nexport function forwardClick(\n intersections: ReadonlyArray<{ object: Object3D }>,\n): InteractiveLike | undefined {\n const first = intersections[0];\n if (!first) {\n return undefined;\n }\n const target = pickInteractive(first.object);\n target?.dispatchEvent?.({ type: 'click' });\n return target;\n}\n"]}
@@ -0,0 +1,6 @@
1
+ /** The slice of `WebGLRenderer` this needs - keeps the helper testable. */
2
+ export interface UikitRenderer {
3
+ localClippingEnabled: boolean;
4
+ setTransparentSort(method: (a: unknown, b: unknown) => number): void;
5
+ }
6
+ export declare function configureRendererForUikit(renderer: UikitRenderer): void;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Renderer configuration required by uikit.
3
+ *
4
+ * uikit draws every panel - background plates, borders, images AND text
5
+ * glyphs - as TRANSPARENT meshes, and relies on `renderOrder` to stack them
6
+ * correctly. three.js's default transparent sort orders by distance to the
7
+ * camera, which is meaningless for coplanar UI layers: at grazing angles or
8
+ * close range the panel background can sort in front of its own text, so
9
+ * labels silently vanish while the panel still renders. `reversePainterSortStable`
10
+ * sorts by `renderOrder` instead, which is what uikit assigns.
11
+ *
12
+ * IWSDK applies exactly this configuration in its own UI system, which is
13
+ * why panels look right there; any hand-rolled three.js host (desktop, XR
14
+ * Blocks, tests) must do the same or it will hit the disappearing-text bug.
15
+ *
16
+ * Call once, right after creating the renderer:
17
+ *
18
+ * const renderer = new WebGLRenderer({ antialias: true });
19
+ * configureRendererForUikit(renderer);
20
+ */
21
+ import { reversePainterSortStable } from '@pmndrs/uikit';
22
+ export function configureRendererForUikit(renderer) {
23
+ // Stack UI layers by renderOrder, not by camera distance.
24
+ renderer.setTransparentSort(reversePainterSortStable);
25
+ // uikit clips panel content (scroll areas, overflow) with local clipping
26
+ // planes; three.js ignores those unless this is enabled.
27
+ renderer.localClippingEnabled = true;
28
+ }
29
+ //# sourceMappingURL=renderer-setup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"renderer-setup.js","sourceRoot":"","sources":["../src/renderer-setup.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,wBAAwB,EAAE,MAAM,eAAe,CAAC;AAQzD,MAAM,UAAU,yBAAyB,CAAC,QAAuB;IAC/D,0DAA0D;IAC1D,QAAQ,CAAC,kBAAkB,CACzB,wBAAyE,CAC1E,CAAC;IACF,yEAAyE;IACzE,yDAAyD;IACzD,QAAQ,CAAC,oBAAoB,GAAG,IAAI,CAAC;AACvC,CAAC","sourcesContent":["/**\n * Renderer configuration required by uikit.\n *\n * uikit draws every panel - background plates, borders, images AND text\n * glyphs - as TRANSPARENT meshes, and relies on `renderOrder` to stack them\n * correctly. three.js's default transparent sort orders by distance to the\n * camera, which is meaningless for coplanar UI layers: at grazing angles or\n * close range the panel background can sort in front of its own text, so\n * labels silently vanish while the panel still renders. `reversePainterSortStable`\n * sorts by `renderOrder` instead, which is what uikit assigns.\n *\n * IWSDK applies exactly this configuration in its own UI system, which is\n * why panels look right there; any hand-rolled three.js host (desktop, XR\n * Blocks, tests) must do the same or it will hit the disappearing-text bug.\n *\n * Call once, right after creating the renderer:\n *\n * const renderer = new WebGLRenderer({ antialias: true });\n * configureRendererForUikit(renderer);\n */\nimport { reversePainterSortStable } from '@pmndrs/uikit';\n\n/** The slice of `WebGLRenderer` this needs - keeps the helper testable. */\nexport interface UikitRenderer {\n localClippingEnabled: boolean;\n setTransparentSort(method: (a: unknown, b: unknown) => number): void;\n}\n\nexport function configureRendererForUikit(renderer: UikitRenderer): void {\n // Stack UI layers by renderOrder, not by camera distance.\n renderer.setTransparentSort(\n reversePainterSortStable as unknown as (a: unknown, b: unknown) => number,\n );\n // uikit clips panel content (scroll areas, overflow) with local clipping\n // planes; three.js ignores those unless this is enabled.\n renderer.localClippingEnabled = true;\n}\n"]}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Panel fit math - mirrors the IWSDK adapter's behaviour exactly so the same
3
+ * UIKitML renders at the same world size on both platforms: UIKit units are
4
+ * centimeters; the panel group is uniformly scaled to fit the requested
5
+ * width × height (meters) while preserving aspect ratio.
6
+ */
7
+ /**
8
+ * Uniform scale that fits a panel of `naturalWidth × naturalHeight` (UIKit
9
+ * cm units) into `targetWidth × targetHeight` meters. Returns undefined
10
+ * when any input is not yet usable (layout not measured, zero target).
11
+ */
12
+ export declare function fitScale(targetWidth: number, targetHeight: number, naturalWidth: number | undefined, naturalHeight: number | undefined): number | undefined;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Panel fit math - mirrors the IWSDK adapter's behaviour exactly so the same
3
+ * UIKitML renders at the same world size on both platforms: UIKit units are
4
+ * centimeters; the panel group is uniformly scaled to fit the requested
5
+ * width × height (meters) while preserving aspect ratio.
6
+ */
7
+ /**
8
+ * Uniform scale that fits a panel of `naturalWidth × naturalHeight` (UIKit
9
+ * cm units) into `targetWidth × targetHeight` meters. Returns undefined
10
+ * when any input is not yet usable (layout not measured, zero target).
11
+ */
12
+ export function fitScale(targetWidth, targetHeight, naturalWidth, naturalHeight) {
13
+ if (!(targetWidth > 0) ||
14
+ !(targetHeight > 0) ||
15
+ typeof naturalWidth !== 'number' ||
16
+ typeof naturalHeight !== 'number' ||
17
+ !(naturalWidth > 0) ||
18
+ !(naturalHeight > 0)) {
19
+ return undefined;
20
+ }
21
+ const widthMeters = naturalWidth / 100;
22
+ const heightMeters = naturalHeight / 100;
23
+ return Math.min(targetWidth / widthMeters, targetHeight / heightMeters);
24
+ }
25
+ //# sourceMappingURL=scale-math.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scale-math.js","sourceRoot":"","sources":["../src/scale-math.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;;GAIG;AACH,MAAM,UAAU,QAAQ,CACtB,WAAmB,EACnB,YAAoB,EACpB,YAAgC,EAChC,aAAiC;IAEjC,IACE,CAAC,CAAC,WAAW,GAAG,CAAC,CAAC;QAClB,CAAC,CAAC,YAAY,GAAG,CAAC,CAAC;QACnB,OAAO,YAAY,KAAK,QAAQ;QAChC,OAAO,aAAa,KAAK,QAAQ;QACjC,CAAC,CAAC,YAAY,GAAG,CAAC,CAAC;QACnB,CAAC,CAAC,aAAa,GAAG,CAAC,CAAC,EACpB,CAAC;QACD,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,MAAM,WAAW,GAAG,YAAY,GAAG,GAAG,CAAC;IACvC,MAAM,YAAY,GAAG,aAAa,GAAG,GAAG,CAAC;IACzC,OAAO,IAAI,CAAC,GAAG,CAAC,WAAW,GAAG,WAAW,EAAE,YAAY,GAAG,YAAY,CAAC,CAAC;AAC1E,CAAC","sourcesContent":["/**\n * Panel fit math - mirrors the IWSDK adapter's behaviour exactly so the same\n * UIKitML renders at the same world size on both platforms: UIKit units are\n * centimeters; the panel group is uniformly scaled to fit the requested\n * width × height (meters) while preserving aspect ratio.\n */\n\n/**\n * Uniform scale that fits a panel of `naturalWidth × naturalHeight` (UIKit\n * cm units) into `targetWidth × targetHeight` meters. Returns undefined\n * when any input is not yet usable (layout not measured, zero target).\n */\nexport function fitScale(\n targetWidth: number,\n targetHeight: number,\n naturalWidth: number | undefined,\n naturalHeight: number | undefined,\n): number | undefined {\n if (\n !(targetWidth > 0) ||\n !(targetHeight > 0) ||\n typeof naturalWidth !== 'number' ||\n typeof naturalHeight !== 'number' ||\n !(naturalWidth > 0) ||\n !(naturalHeight > 0)\n ) {\n return undefined;\n }\n const widthMeters = naturalWidth / 100;\n const heightMeters = naturalHeight / 100;\n return Math.min(targetWidth / widthMeters, targetHeight / heightMeters);\n}\n"]}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * XR Blocks glue.
3
+ *
4
+ * XR Blocks (https://github.com/google/xrblocks) is three.js-based: a
5
+ * Script's `scene`/`camera` are ordinary three.js objects, and `update()`
6
+ * runs per frame. This module binds a {@link UixWindowHost} to that shape
7
+ * WITHOUT importing `xrblocks` (loose structural types), so the package
8
+ * works identically in an XR Blocks Script and in any hand-rolled three.js
9
+ * WebXR app.
10
+ *
11
+ * Typical XR Blocks usage:
12
+ *
13
+ * import * as xb from 'xrblocks';
14
+ * import { connectUIExtensions } from '@realitycollective/xrblocks-uiextensions';
15
+ *
16
+ * class MyScript extends xb.Script {
17
+ * init() { this.uix = connectUIExtensions({ scene: this, camera: xb.core.camera }); }
18
+ * update() { this.uix.update(xb.core.timer.getDelta()); }
19
+ * }
20
+ */
21
+ import { type Object3D, type PerspectiveCamera } from 'three';
22
+ import type { HeadPoseSource } from '@realitycollective/webxr-uiextensions';
23
+ import type { Kit } from '@pmndrs/uikitml';
24
+ import { UixWindowHost } from './host.js';
25
+ /** The slice of an XR Blocks Script / three.js app the host binds to. */
26
+ export interface EngineContext {
27
+ /** Where windows are parented - an XR Blocks Script IS an Object3D. */
28
+ scene: Object3D;
29
+ /** The rendering camera; in XR the engine keeps it at the viewer pose. */
30
+ camera: Pick<PerspectiveCamera, 'getWorldPosition' | 'getWorldQuaternion'>;
31
+ /** Optional UIKitML component kit(s). */
32
+ kit?: Kit;
33
+ }
34
+ /** HeadPoseSource backed by a three.js camera. */
35
+ export declare function cameraHeadPoseSource(camera: EngineContext['camera']): HeadPoseSource;
36
+ /** Create a window host bound to an XR Blocks Script / three.js context. */
37
+ export declare function connectUIExtensions(context: EngineContext): UixWindowHost;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * XR Blocks glue.
3
+ *
4
+ * XR Blocks (https://github.com/google/xrblocks) is three.js-based: a
5
+ * Script's `scene`/`camera` are ordinary three.js objects, and `update()`
6
+ * runs per frame. This module binds a {@link UixWindowHost} to that shape
7
+ * WITHOUT importing `xrblocks` (loose structural types), so the package
8
+ * works identically in an XR Blocks Script and in any hand-rolled three.js
9
+ * WebXR app.
10
+ *
11
+ * Typical XR Blocks usage:
12
+ *
13
+ * import * as xb from 'xrblocks';
14
+ * import { connectUIExtensions } from '@realitycollective/xrblocks-uiextensions';
15
+ *
16
+ * class MyScript extends xb.Script {
17
+ * init() { this.uix = connectUIExtensions({ scene: this, camera: xb.core.camera }); }
18
+ * update() { this.uix.update(xb.core.timer.getDelta()); }
19
+ * }
20
+ */
21
+ import { Quaternion, Vector3 } from 'three';
22
+ import { UixWindowHost } from './host.js';
23
+ /** HeadPoseSource backed by a three.js camera. */
24
+ export function cameraHeadPoseSource(camera) {
25
+ const position = new Vector3();
26
+ const quaternion = new Quaternion();
27
+ return {
28
+ getHeadPose() {
29
+ camera.getWorldPosition(position);
30
+ camera.getWorldQuaternion(quaternion);
31
+ return {
32
+ position: [position.x, position.y, position.z],
33
+ quaternion: [quaternion.x, quaternion.y, quaternion.z, quaternion.w],
34
+ };
35
+ },
36
+ };
37
+ }
38
+ /** Create a window host bound to an XR Blocks Script / three.js context. */
39
+ export function connectUIExtensions(context) {
40
+ return new UixWindowHost({
41
+ scene: context.scene,
42
+ headPose: cameraHeadPoseSource(context.camera),
43
+ ...(context.kit ? { kit: context.kit } : {}),
44
+ });
45
+ }
46
+ //# sourceMappingURL=xrblocks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"xrblocks.js","sourceRoot":"","sources":["../src/xrblocks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,UAAU,EAAE,OAAO,EAAyC,MAAM,OAAO,CAAC;AAGnF,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAY1C,kDAAkD;AAClD,MAAM,UAAU,oBAAoB,CAClC,MAA+B;IAE/B,MAAM,QAAQ,GAAG,IAAI,OAAO,EAAE,CAAC;IAC/B,MAAM,UAAU,GAAG,IAAI,UAAU,EAAE,CAAC;IACpC,OAAO;QACL,WAAW;YACT,MAAM,CAAC,gBAAgB,CAAC,QAAQ,CAAC,CAAC;YAClC,MAAM,CAAC,kBAAkB,CAAC,UAAU,CAAC,CAAC;YACtC,OAAO;gBACL,QAAQ,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC;gBAC9C,UAAU,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC;aACrE,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,mBAAmB,CAAC,OAAsB;IACxD,OAAO,IAAI,aAAa,CAAC;QACvB,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,QAAQ,EAAE,oBAAoB,CAAC,OAAO,CAAC,MAAM,CAAC;QAC9C,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC7C,CAAC,CAAC;AACL,CAAC","sourcesContent":["/**\n * XR Blocks glue.\n *\n * XR Blocks (https://github.com/google/xrblocks) is three.js-based: a\n * Script's `scene`/`camera` are ordinary three.js objects, and `update()`\n * runs per frame. This module binds a {@link UixWindowHost} to that shape\n * WITHOUT importing `xrblocks` (loose structural types), so the package\n * works identically in an XR Blocks Script and in any hand-rolled three.js\n * WebXR app.\n *\n * Typical XR Blocks usage:\n *\n * import * as xb from 'xrblocks';\n * import { connectUIExtensions } from '@realitycollective/xrblocks-uiextensions';\n *\n * class MyScript extends xb.Script {\n * init() { this.uix = connectUIExtensions({ scene: this, camera: xb.core.camera }); }\n * update() { this.uix.update(xb.core.timer.getDelta()); }\n * }\n */\nimport { Quaternion, Vector3, type Object3D, type PerspectiveCamera } from 'three';\nimport type { HeadPose, HeadPoseSource } from '@realitycollective/webxr-uiextensions';\nimport type { Kit } from '@pmndrs/uikitml';\nimport { UixWindowHost } from './host.js';\n\n/** The slice of an XR Blocks Script / three.js app the host binds to. */\nexport interface EngineContext {\n /** Where windows are parented - an XR Blocks Script IS an Object3D. */\n scene: Object3D;\n /** The rendering camera; in XR the engine keeps it at the viewer pose. */\n camera: Pick<PerspectiveCamera, 'getWorldPosition' | 'getWorldQuaternion'>;\n /** Optional UIKitML component kit(s). */\n kit?: Kit;\n}\n\n/** HeadPoseSource backed by a three.js camera. */\nexport function cameraHeadPoseSource(\n camera: EngineContext['camera'],\n): HeadPoseSource {\n const position = new Vector3();\n const quaternion = new Quaternion();\n return {\n getHeadPose(): HeadPose {\n camera.getWorldPosition(position);\n camera.getWorldQuaternion(quaternion);\n return {\n position: [position.x, position.y, position.z],\n quaternion: [quaternion.x, quaternion.y, quaternion.z, quaternion.w],\n };\n },\n };\n}\n\n/** Create a window host bound to an XR Blocks Script / three.js context. */\nexport function connectUIExtensions(context: EngineContext): UixWindowHost {\n return new UixWindowHost({\n scene: context.scene,\n headPose: cameraHeadPoseSource(context.camera),\n ...(context.kit ? { kit: context.kit } : {}),\n });\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@realitycollective/xrblocks-uiextensions",
3
+ "version": "0.1.0-preview.0",
4
+ "description": "EXPERIMENTAL Google XR Blocks / plain three.js adapter for the Reality Collective UI Extensions - hosts the engine-free @realitycollective/webxr-uiextensions windowing core (UIKitML panels, window chrome, follow mode) in any three.js WebXR scene, including XR Blocks Scripts.",
5
+ "keywords": [
6
+ "realitycollective",
7
+ "xrblocks",
8
+ "androidxr",
9
+ "webxr",
10
+ "threejs",
11
+ "ui",
12
+ "windowing",
13
+ "spatial-ui",
14
+ "uikitml",
15
+ "typescript"
16
+ ],
17
+ "license": "MIT",
18
+ "author": "Reality Collective",
19
+ "type": "module",
20
+ "main": "./dist/index.js",
21
+ "types": "./dist/index.d.ts",
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/index.d.ts",
25
+ "default": "./dist/index.js"
26
+ }
27
+ },
28
+ "sideEffects": false,
29
+ "files": [
30
+ "dist",
31
+ "CHANGELOG.md"
32
+ ],
33
+ "scripts": {
34
+ "build": "tsc -p tsconfig.build.json",
35
+ "typecheck": "tsc -p tsconfig.json --noEmit"
36
+ },
37
+ "dependencies": {
38
+ "@pmndrs/uikitml": "^0.1.12",
39
+ "@realitycollective/webxr-uiextensions": "^0.1.0-preview.0"
40
+ },
41
+ "peerDependencies": {
42
+ "three": ">=0.170.0"
43
+ },
44
+ "devDependencies": {
45
+ "three": "npm:super-three@0.181.0",
46
+ "@types/three": "*",
47
+ "typescript": "^5.5.0"
48
+ },
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "git+https://github.com/realitycollective/WebXR-UIExtensions.git",
52
+ "directory": "packages/xrblocks-uiextensions"
53
+ },
54
+ "homepage": "https://github.com/realitycollective/WebXR-UIExtensions#readme",
55
+ "bugs": {
56
+ "url": "https://github.com/realitycollective/WebXR-UIExtensions/issues"
57
+ },
58
+ "publishConfig": {
59
+ "access": "public"
60
+ },
61
+ "engines": {
62
+ "node": ">=20.19.0 <21.0.0-0 || >=22.12.0 <23.0.0-0 || >=24.0.0"
63
+ }
64
+ }