@realitycollective/iwsdk-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.

Potentially problematic release.


This version of @realitycollective/iwsdk-uiextensions might be problematic. Click here for more details.

Files changed (46) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/Examples/README.md +13 -0
  3. package/Examples/basic-window/main.ts +31 -0
  4. package/Examples/basic-window/window.uikitml +59 -0
  5. package/Examples/controls/controls.uikitml +50 -0
  6. package/Examples/controls/main.ts +44 -0
  7. package/Examples/dock-regions/main.ts +55 -0
  8. package/LICENSE +21 -0
  9. package/README.md +117 -0
  10. package/dist/components.d.ts +189 -0
  11. package/dist/components.js +97 -0
  12. package/dist/components.js.map +1 -0
  13. package/dist/factory.d.ts +55 -0
  14. package/dist/factory.js +63 -0
  15. package/dist/factory.js.map +1 -0
  16. package/dist/index.d.ts +24 -0
  17. package/dist/index.js +27 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/manager-registry.d.ts +8 -0
  20. package/dist/manager-registry.js +11 -0
  21. package/dist/manager-registry.js.map +1 -0
  22. package/dist/region-registry-store.d.ts +7 -0
  23. package/dist/region-registry-store.js +11 -0
  24. package/dist/region-registry-store.js.map +1 -0
  25. package/dist/register.d.ts +19 -0
  26. package/dist/register.js +20 -0
  27. package/dist/register.js.map +1 -0
  28. package/dist/scene-host.d.ts +23 -0
  29. package/dist/scene-host.js +145 -0
  30. package/dist/scene-host.js.map +1 -0
  31. package/dist/systems/controls-system.d.ts +24 -0
  32. package/dist/systems/controls-system.js +24 -0
  33. package/dist/systems/controls-system.js.map +1 -0
  34. package/dist/systems/dock-region-system.d.ts +230 -0
  35. package/dist/systems/dock-region-system.js +102 -0
  36. package/dist/systems/dock-region-system.js.map +1 -0
  37. package/dist/systems/dock-system.d.ts +205 -0
  38. package/dist/systems/dock-system.js +135 -0
  39. package/dist/systems/dock-system.js.map +1 -0
  40. package/dist/systems/drag-system.d.ts +280 -0
  41. package/dist/systems/drag-system.js +167 -0
  42. package/dist/systems/drag-system.js.map +1 -0
  43. package/dist/systems/window-system.d.ts +455 -0
  44. package/dist/systems/window-system.js +244 -0
  45. package/dist/systems/window-system.js.map +1 -0
  46. package/package.json +65 -0
@@ -0,0 +1,135 @@
1
+ /**
2
+ * UIDockSystem - realises dock modes by composing IWSDK components.
3
+ *
4
+ * `body-follow` and `head-locked` are implemented with the IWSDK's own
5
+ * `Follower` / `ScreenSpace` components (reuse, not recreation); this system
6
+ * only plans and applies transitions with the pure dock state machine.
7
+ */
8
+ import { Euler, Follower, FollowBehavior, Quaternion, ScreenSpace, Vector3, createSystem, } from '@iwsdk/core';
9
+ import { UIWindow, UIWindowState } from '../components.js';
10
+ import { DockMode, isDockMode, planTransition, recipeFor, } from '@realitycollective/webxr-uiextensions';
11
+ import { faceViewerYaw } from '@realitycollective/webxr-uiextensions';
12
+ import { windowManagerFor } from '../manager-registry.js';
13
+ export class UIDockSystem extends createSystem({
14
+ windows: { required: [UIWindow, UIWindowState] },
15
+ }) {
16
+ manager;
17
+ windowPosition = new Vector3();
18
+ viewerPosition = new Vector3();
19
+ init() {
20
+ this.manager = windowManagerFor(this.world);
21
+ }
22
+ update() {
23
+ for (const entity of this.queries.windows.entities) {
24
+ const requested = entity.getValue(UIWindow, 'dockMode');
25
+ const applied = entity.getValue(UIWindowState, 'appliedDockMode');
26
+ if (!isDockMode(requested) || applied === requested) {
27
+ continue;
28
+ }
29
+ this.apply(entity, applied, requested);
30
+ }
31
+ }
32
+ apply(entity, applied, requested) {
33
+ if (isDockMode(applied)) {
34
+ const plan = planTransition(applied, requested);
35
+ if (!plan) {
36
+ return;
37
+ }
38
+ if (plan.removeFollower && entity.hasComponent(Follower)) {
39
+ entity.removeComponent(Follower);
40
+ }
41
+ if (plan.removeFollower && requested === DockMode.WorldLocked) {
42
+ // Pinning in place: leave the window facing the user at the spot it
43
+ // was pinned, instead of freezing whatever lagging follow rotation
44
+ // it happened to have this frame.
45
+ this.faceViewer(entity);
46
+ }
47
+ if (plan.removeScreenSpace && entity.hasComponent(ScreenSpace)) {
48
+ entity.removeComponent(ScreenSpace);
49
+ }
50
+ if (plan.addFollower) {
51
+ // Unpinning: keep the window where the user left it, expressed
52
+ // relative to their head, instead of snapping to the default offset.
53
+ this.addFollower(entity, { fromCurrentPose: true });
54
+ }
55
+ if (plan.addScreenSpace) {
56
+ entity.addComponent(ScreenSpace);
57
+ }
58
+ if (plan.snap && entity.hasComponent(Follower)) {
59
+ entity.setValue(Follower, 'needsPositionSync', true);
60
+ }
61
+ }
62
+ else {
63
+ // First application after adoption: build the requested recipe from
64
+ // scratch (the entity carries no dock components we own yet).
65
+ const recipe = recipeFor(requested);
66
+ if (recipe.follower && !entity.hasComponent(Follower)) {
67
+ this.addFollower(entity);
68
+ }
69
+ if (!recipe.follower && entity.hasComponent(Follower)) {
70
+ entity.removeComponent(Follower);
71
+ }
72
+ if (recipe.screenSpace && !entity.hasComponent(ScreenSpace)) {
73
+ entity.addComponent(ScreenSpace);
74
+ }
75
+ if (!recipe.screenSpace && entity.hasComponent(ScreenSpace)) {
76
+ entity.removeComponent(ScreenSpace);
77
+ }
78
+ }
79
+ entity.setValue(UIWindowState, 'appliedDockMode', requested);
80
+ const id = entity.getValue(UIWindow, 'windowId');
81
+ if (id && this.manager.has(id)) {
82
+ this.manager.setDockMode(id, requested);
83
+ }
84
+ }
85
+ faceViewer(entity) {
86
+ const object = entity.object3D;
87
+ if (!object) {
88
+ return;
89
+ }
90
+ object.getWorldPosition(this.windowPosition);
91
+ this.camera.getWorldPosition(this.viewerPosition);
92
+ const yaw = faceViewerYaw([this.windowPosition.x, this.windowPosition.y, this.windowPosition.z], [this.viewerPosition.x, this.viewerPosition.y, this.viewerPosition.z], object.rotation.y);
93
+ object.rotation.set(0, yaw, 0);
94
+ }
95
+ offsetHelper = new Vector3();
96
+ yawQuaternion = new Quaternion();
97
+ yawEuler = new Euler();
98
+ addFollower(entity, options = {}) {
99
+ const configured = entity.getVectorView(UIWindow, 'followOffset');
100
+ let offset = [
101
+ configured[0] ?? 0,
102
+ configured[1] ?? 0,
103
+ configured[2] ?? 0,
104
+ ];
105
+ const object = entity.object3D;
106
+ if (options.fromCurrentPose && object) {
107
+ // Current window position in the viewer's yaw-local frame (matching
108
+ // how Follower's PivotY applies offsets), so following resumes from
109
+ // the same bearing and distance the window was unpinned at.
110
+ object.getWorldPosition(this.offsetHelper);
111
+ this.camera.getWorldPosition(this.windowPosition);
112
+ this.camera.getWorldQuaternion(this.yawQuaternion);
113
+ this.yawEuler.setFromQuaternion(this.yawQuaternion, 'YXZ');
114
+ this.yawEuler.x = 0;
115
+ this.yawEuler.z = 0;
116
+ this.yawQuaternion.setFromEuler(this.yawEuler).conjugate();
117
+ this.offsetHelper.sub(this.windowPosition).applyQuaternion(this.yawQuaternion);
118
+ // Degenerate (window at the viewer): keep the configured offset.
119
+ if (this.offsetHelper.lengthSq() > 0.01) {
120
+ offset = [this.offsetHelper.x, this.offsetHelper.y, this.offsetHelper.z];
121
+ }
122
+ }
123
+ entity.addComponent(Follower, {
124
+ // The camera, not player.head: three.js syncs the camera to the
125
+ // headset pose in-session, while the head rig sits at the origin
126
+ // outside XR (desktop) - following it would park windows at the floor.
127
+ target: this.camera,
128
+ offsetPosition: offset,
129
+ behavior: FollowBehavior.PivotY,
130
+ speed: entity.getValue(UIWindow, 'followSpeed'),
131
+ tolerance: entity.getValue(UIWindow, 'followTolerance'),
132
+ });
133
+ }
134
+ }
135
+ //# sourceMappingURL=dock-system.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dock-system.js","sourceRoot":"","sources":["../../src/systems/dock-system.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * UIDockSystem - realises dock modes by composing IWSDK components.\n *\n * `body-follow` and `head-locked` are implemented with the IWSDK's own\n * `Follower` / `ScreenSpace` components (reuse, not recreation); this system\n * only plans and applies transitions with the pure dock state machine.\n */\nimport {\n Euler,\n Follower,\n FollowBehavior,\n Quaternion,\n ScreenSpace,\n Vector3,\n createSystem,\n type Entity,\n} from '@iwsdk/core';\nimport { UIWindow, UIWindowState } from '../components.js';\nimport {\n DockMode,\n isDockMode,\n planTransition,\n recipeFor,\n type DockModeValue,\n} from '@realitycollective/webxr-uiextensions';\nimport { faceViewerYaw } from '@realitycollective/webxr-uiextensions';\nimport { windowManagerFor } from '../manager-registry.js';\nimport type { WindowManager } from '@realitycollective/webxr-uiextensions';\n\nexport class UIDockSystem extends createSystem({\n windows: { required: [UIWindow, UIWindowState] },\n}) {\n private manager!: WindowManager;\n private windowPosition = new Vector3();\n private viewerPosition = new Vector3();\n\n override init(): void {\n this.manager = windowManagerFor(this.world);\n }\n\n override update(): void {\n for (const entity of this.queries.windows.entities) {\n const requested = entity.getValue(UIWindow, 'dockMode') as DockModeValue;\n const applied = entity.getValue(UIWindowState, 'appliedDockMode') as string;\n if (!isDockMode(requested) || applied === requested) {\n continue;\n }\n this.apply(entity, applied, requested);\n }\n }\n\n private apply(entity: Entity, applied: string, requested: DockModeValue): void {\n if (isDockMode(applied)) {\n const plan = planTransition(applied, requested);\n if (!plan) {\n return;\n }\n if (plan.removeFollower && entity.hasComponent(Follower)) {\n entity.removeComponent(Follower);\n }\n if (plan.removeFollower && requested === DockMode.WorldLocked) {\n // Pinning in place: leave the window facing the user at the spot it\n // was pinned, instead of freezing whatever lagging follow rotation\n // it happened to have this frame.\n this.faceViewer(entity);\n }\n if (plan.removeScreenSpace && entity.hasComponent(ScreenSpace)) {\n entity.removeComponent(ScreenSpace);\n }\n if (plan.addFollower) {\n // Unpinning: keep the window where the user left it, expressed\n // relative to their head, instead of snapping to the default offset.\n this.addFollower(entity, { fromCurrentPose: true });\n }\n if (plan.addScreenSpace) {\n entity.addComponent(ScreenSpace);\n }\n if (plan.snap && entity.hasComponent(Follower)) {\n entity.setValue(Follower, 'needsPositionSync', true);\n }\n } else {\n // First application after adoption: build the requested recipe from\n // scratch (the entity carries no dock components we own yet).\n const recipe = recipeFor(requested);\n if (recipe.follower && !entity.hasComponent(Follower)) {\n this.addFollower(entity);\n }\n if (!recipe.follower && entity.hasComponent(Follower)) {\n entity.removeComponent(Follower);\n }\n if (recipe.screenSpace && !entity.hasComponent(ScreenSpace)) {\n entity.addComponent(ScreenSpace);\n }\n if (!recipe.screenSpace && entity.hasComponent(ScreenSpace)) {\n entity.removeComponent(ScreenSpace);\n }\n }\n\n entity.setValue(UIWindowState, 'appliedDockMode', requested);\n\n const id = entity.getValue(UIWindow, 'windowId') as string;\n if (id && this.manager.has(id)) {\n this.manager.setDockMode(id, requested);\n }\n }\n\n private faceViewer(entity: Entity): void {\n const object = entity.object3D;\n if (!object) {\n return;\n }\n object.getWorldPosition(this.windowPosition);\n this.camera.getWorldPosition(this.viewerPosition);\n const yaw = faceViewerYaw(\n [this.windowPosition.x, this.windowPosition.y, this.windowPosition.z],\n [this.viewerPosition.x, this.viewerPosition.y, this.viewerPosition.z],\n object.rotation.y,\n );\n object.rotation.set(0, yaw, 0);\n }\n\n private offsetHelper = new Vector3();\n private yawQuaternion = new Quaternion();\n private yawEuler = new Euler();\n\n private addFollower(entity: Entity, options: { fromCurrentPose?: boolean } = {}): void {\n const configured = entity.getVectorView(UIWindow, 'followOffset');\n let offset: [number, number, number] = [\n configured[0] ?? 0,\n configured[1] ?? 0,\n configured[2] ?? 0,\n ];\n\n const object = entity.object3D;\n if (options.fromCurrentPose && object) {\n // Current window position in the viewer's yaw-local frame (matching\n // how Follower's PivotY applies offsets), so following resumes from\n // the same bearing and distance the window was unpinned at.\n object.getWorldPosition(this.offsetHelper);\n this.camera.getWorldPosition(this.windowPosition);\n this.camera.getWorldQuaternion(this.yawQuaternion);\n this.yawEuler.setFromQuaternion(this.yawQuaternion, 'YXZ');\n this.yawEuler.x = 0;\n this.yawEuler.z = 0;\n this.yawQuaternion.setFromEuler(this.yawEuler).conjugate();\n this.offsetHelper.sub(this.windowPosition).applyQuaternion(this.yawQuaternion);\n // Degenerate (window at the viewer): keep the configured offset.\n if (this.offsetHelper.lengthSq() > 0.01) {\n offset = [this.offsetHelper.x, this.offsetHelper.y, this.offsetHelper.z];\n }\n }\n\n entity.addComponent(Follower, {\n // The camera, not player.head: three.js syncs the camera to the\n // headset pose in-session, while the head rig sits at the origin\n // outside XR (desktop) - following it would park windows at the floor.\n target: this.camera,\n offsetPosition: offset,\n behavior: FollowBehavior.PivotY,\n speed: entity.getValue(UIWindow, 'followSpeed') as number,\n tolerance: entity.getValue(UIWindow, 'followTolerance') as number,\n });\n }\n}\n"]}
@@ -0,0 +1,280 @@
1
+ declare const UIDragSystem_base: import("elics").SystemConstructor<import("elics").SystemSchema, {
2
+ windows: {
3
+ required: (import("elics").Component<{
4
+ windowId: {
5
+ type: import("@iwsdk/core").Types.String;
6
+ default: string;
7
+ };
8
+ title: {
9
+ type: import("@iwsdk/core").Types.String;
10
+ default: string;
11
+ };
12
+ dockMode: {
13
+ type: import("@iwsdk/core").Types.Enum;
14
+ enum: {
15
+ readonly WorldLocked: "world-locked";
16
+ readonly BodyFollow: "body-follow";
17
+ readonly HeadLocked: "head-locked";
18
+ };
19
+ default: "world-locked";
20
+ };
21
+ movable: {
22
+ type: import("@iwsdk/core").Types.Boolean;
23
+ default: true;
24
+ };
25
+ closable: {
26
+ type: import("@iwsdk/core").Types.Boolean;
27
+ default: true;
28
+ };
29
+ minimizable: {
30
+ type: import("@iwsdk/core").Types.Boolean;
31
+ default: true;
32
+ };
33
+ pinnable: {
34
+ type: import("@iwsdk/core").Types.Boolean;
35
+ default: true;
36
+ };
37
+ billboardWhileDragging: {
38
+ type: import("@iwsdk/core").Types.Boolean;
39
+ default: true;
40
+ };
41
+ dragDelay: {
42
+ type: import("@iwsdk/core").Types.Float32;
43
+ default: number;
44
+ };
45
+ followOffset: {
46
+ type: import("@iwsdk/core").Types.Vec3;
47
+ default: [number, number, number];
48
+ };
49
+ followSpeed: {
50
+ type: import("@iwsdk/core").Types.Float32;
51
+ default: number;
52
+ };
53
+ followTolerance: {
54
+ type: import("@iwsdk/core").Types.Float32;
55
+ default: number;
56
+ };
57
+ focusBias: {
58
+ type: import("@iwsdk/core").Types.Float32;
59
+ default: number;
60
+ };
61
+ targetWidth: {
62
+ type: import("@iwsdk/core").Types.Float32;
63
+ default: number;
64
+ };
65
+ targetHeight: {
66
+ type: import("@iwsdk/core").Types.Float32;
67
+ default: number;
68
+ };
69
+ }> | import("elics").Component<{
70
+ appliedDockMode: {
71
+ type: import("@iwsdk/core").Types.String;
72
+ default: string;
73
+ };
74
+ chromeWired: {
75
+ type: import("@iwsdk/core").Types.Boolean;
76
+ default: false;
77
+ };
78
+ homeDockMode: {
79
+ type: import("@iwsdk/core").Types.String;
80
+ default: string;
81
+ };
82
+ homeRegion: {
83
+ type: import("@iwsdk/core").Types.String;
84
+ default: string;
85
+ };
86
+ homePosition: {
87
+ type: import("@iwsdk/core").Types.Vec3;
88
+ default: [number, number, number];
89
+ };
90
+ homeYaw: {
91
+ type: import("@iwsdk/core").Types.Float32;
92
+ default: number;
93
+ };
94
+ }> | import("elics").Component<{
95
+ document: {
96
+ type: import("@iwsdk/core").Types.Object;
97
+ default: undefined;
98
+ };
99
+ }>)[];
100
+ };
101
+ regions: {
102
+ required: import("elics").Component<{
103
+ regionId: {
104
+ type: import("@iwsdk/core").Types.String;
105
+ default: string;
106
+ };
107
+ flow: {
108
+ type: import("@iwsdk/core").Types.Enum;
109
+ enum: {
110
+ readonly Row: "row";
111
+ readonly Column: "column";
112
+ readonly Grid: "grid";
113
+ };
114
+ default: "column";
115
+ };
116
+ pitch: {
117
+ type: import("@iwsdk/core").Types.Float32;
118
+ default: number;
119
+ };
120
+ columns: {
121
+ type: import("@iwsdk/core").Types.Float32;
122
+ default: number;
123
+ };
124
+ capacity: {
125
+ type: import("@iwsdk/core").Types.Float32;
126
+ default: number;
127
+ };
128
+ snapRadius: {
129
+ type: import("@iwsdk/core").Types.Float32;
130
+ default: number;
131
+ };
132
+ }>[];
133
+ };
134
+ }, World, import("@iwsdk/core").System<import("elics").SystemSchema, {
135
+ windows: {
136
+ required: (import("elics").Component<{
137
+ windowId: {
138
+ type: import("@iwsdk/core").Types.String;
139
+ default: string;
140
+ };
141
+ title: {
142
+ type: import("@iwsdk/core").Types.String;
143
+ default: string;
144
+ };
145
+ dockMode: {
146
+ type: import("@iwsdk/core").Types.Enum;
147
+ enum: {
148
+ readonly WorldLocked: "world-locked";
149
+ readonly BodyFollow: "body-follow";
150
+ readonly HeadLocked: "head-locked";
151
+ };
152
+ default: "world-locked";
153
+ };
154
+ movable: {
155
+ type: import("@iwsdk/core").Types.Boolean;
156
+ default: true;
157
+ };
158
+ closable: {
159
+ type: import("@iwsdk/core").Types.Boolean;
160
+ default: true;
161
+ };
162
+ minimizable: {
163
+ type: import("@iwsdk/core").Types.Boolean;
164
+ default: true;
165
+ };
166
+ pinnable: {
167
+ type: import("@iwsdk/core").Types.Boolean;
168
+ default: true;
169
+ };
170
+ billboardWhileDragging: {
171
+ type: import("@iwsdk/core").Types.Boolean;
172
+ default: true;
173
+ };
174
+ dragDelay: {
175
+ type: import("@iwsdk/core").Types.Float32;
176
+ default: number;
177
+ };
178
+ followOffset: {
179
+ type: import("@iwsdk/core").Types.Vec3;
180
+ default: [number, number, number];
181
+ };
182
+ followSpeed: {
183
+ type: import("@iwsdk/core").Types.Float32;
184
+ default: number;
185
+ };
186
+ followTolerance: {
187
+ type: import("@iwsdk/core").Types.Float32;
188
+ default: number;
189
+ };
190
+ focusBias: {
191
+ type: import("@iwsdk/core").Types.Float32;
192
+ default: number;
193
+ };
194
+ targetWidth: {
195
+ type: import("@iwsdk/core").Types.Float32;
196
+ default: number;
197
+ };
198
+ targetHeight: {
199
+ type: import("@iwsdk/core").Types.Float32;
200
+ default: number;
201
+ };
202
+ }> | import("elics").Component<{
203
+ appliedDockMode: {
204
+ type: import("@iwsdk/core").Types.String;
205
+ default: string;
206
+ };
207
+ chromeWired: {
208
+ type: import("@iwsdk/core").Types.Boolean;
209
+ default: false;
210
+ };
211
+ homeDockMode: {
212
+ type: import("@iwsdk/core").Types.String;
213
+ default: string;
214
+ };
215
+ homeRegion: {
216
+ type: import("@iwsdk/core").Types.String;
217
+ default: string;
218
+ };
219
+ homePosition: {
220
+ type: import("@iwsdk/core").Types.Vec3;
221
+ default: [number, number, number];
222
+ };
223
+ homeYaw: {
224
+ type: import("@iwsdk/core").Types.Float32;
225
+ default: number;
226
+ };
227
+ }> | import("elics").Component<{
228
+ document: {
229
+ type: import("@iwsdk/core").Types.Object;
230
+ default: undefined;
231
+ };
232
+ }>)[];
233
+ };
234
+ regions: {
235
+ required: import("elics").Component<{
236
+ regionId: {
237
+ type: import("@iwsdk/core").Types.String;
238
+ default: string;
239
+ };
240
+ flow: {
241
+ type: import("@iwsdk/core").Types.Enum;
242
+ enum: {
243
+ readonly Row: "row";
244
+ readonly Column: "column";
245
+ readonly Grid: "grid";
246
+ };
247
+ default: "column";
248
+ };
249
+ pitch: {
250
+ type: import("@iwsdk/core").Types.Float32;
251
+ default: number;
252
+ };
253
+ columns: {
254
+ type: import("@iwsdk/core").Types.Float32;
255
+ default: number;
256
+ };
257
+ capacity: {
258
+ type: import("@iwsdk/core").Types.Float32;
259
+ default: number;
260
+ };
261
+ snapRadius: {
262
+ type: import("@iwsdk/core").Types.Float32;
263
+ default: number;
264
+ };
265
+ }>[];
266
+ };
267
+ }>>;
268
+ export declare class UIDragSystem extends UIDragSystem_base {
269
+ private states;
270
+ private worldPosition;
271
+ private viewerPosition;
272
+ init(): void;
273
+ update(delta: number): void;
274
+ private attach;
275
+ private detach;
276
+ private onDragStart;
277
+ private whileDragging;
278
+ private onDragEnd;
279
+ }
280
+ export {};
@@ -0,0 +1,167 @@
1
+ /**
2
+ * UIDragSystem - drag windows by the title bar.
3
+ *
4
+ * Reuses `@pmndrs/handle` (the exact library behind the IWSDK's grab
5
+ * systems): a `HandleStore` targets the window entity's object while being
6
+ * *bound* to the title-bar uikit element, so grabbing the title bar with the
7
+ * ray (or a pinch) moves the whole window with the same feel as native IWSDK
8
+ * grabbing. The system pumps its own stores, so it works even when the app
9
+ * doesn't enable `features.grabbing`.
10
+ *
11
+ * Dragging implies placing: on grab the window leaves any dock region and
12
+ * switches to `world-locked`; on release near a region it docks there.
13
+ */
14
+ import { HandleStore } from '@pmndrs/handle';
15
+ import { PanelDocument, Vector3, createSystem, } from '@iwsdk/core';
16
+ import { UIDockRegion, UIDockedTo, UIWindow, UIWindowState } from '../components.js';
17
+ import { WINDOW_CHROME_IDS } from '@realitycollective/webxr-uiextensions';
18
+ import { DockMode } from '@realitycollective/webxr-uiextensions';
19
+ import { faceViewerYaw } from '@realitycollective/webxr-uiextensions';
20
+ import { HoldToDrag } from '@realitycollective/webxr-uiextensions';
21
+ import { regionRegistryFor } from '../region-registry-store.js';
22
+ import { windowManagerFor } from '../manager-registry.js';
23
+ export class UIDragSystem extends createSystem({
24
+ windows: { required: [UIWindow, UIWindowState, PanelDocument] },
25
+ regions: { required: [UIDockRegion] },
26
+ }) {
27
+ states = new Map();
28
+ worldPosition = new Vector3();
29
+ viewerPosition = new Vector3();
30
+ init() {
31
+ this.cleanupFuncs.push(this.queries.windows.subscribe('qualify', (entity) => {
32
+ this.attach(entity);
33
+ }), this.queries.windows.subscribe('disqualify', (entity) => {
34
+ this.detach(entity);
35
+ }));
36
+ this.queries.windows.entities.forEach((entity) => this.attach(entity));
37
+ }
38
+ update(delta) {
39
+ for (const [entity, state] of this.states) {
40
+ const held = state.store.inputState.size > 0;
41
+ const { phase, began, ended } = state.hold.update(held, delta);
42
+ if (began) {
43
+ this.onDragStart(entity);
44
+ }
45
+ if (phase === 'dragging') {
46
+ // The store only runs while an actual drag is in progress: presses
47
+ // shorter than the hold delay never move the window (they're
48
+ // clicks), and skipping the release-frame update stops the handle
49
+ // from restoring its grab-time rotation over our billboard yaw.
50
+ state.store.update(delta);
51
+ this.whileDragging(entity);
52
+ }
53
+ if (ended) {
54
+ this.onDragEnd(entity);
55
+ }
56
+ }
57
+ }
58
+ attach(entity) {
59
+ if (this.states.has(entity) || !entity.getValue(UIWindow, 'movable')) {
60
+ return;
61
+ }
62
+ const document = PanelDocument.data.document[entity.index];
63
+ const titlebar = document?.getElementById(WINDOW_CHROME_IDS.titlebar);
64
+ const object = entity.object3D;
65
+ if (!titlebar || !object) {
66
+ return;
67
+ }
68
+ const store = new HandleStore(object, () => ({
69
+ translate: true,
70
+ rotate: false,
71
+ scale: false,
72
+ multitouch: false,
73
+ projectRays: false,
74
+ }));
75
+ // The title-bar uikit element is an Object3D - bind it as the grab
76
+ // surface while the store's target stays the window root.
77
+ store.bind(titlebar);
78
+ this.states.set(entity, {
79
+ store,
80
+ hold: new HoldToDrag(Math.max(0, entity.getValue(UIWindow, 'dragDelay'))),
81
+ });
82
+ }
83
+ detach(entity) {
84
+ const state = this.states.get(entity);
85
+ if (!state) {
86
+ return;
87
+ }
88
+ try {
89
+ state.store.cancel();
90
+ }
91
+ catch {
92
+ // Handle stores throw when cancelled with no active input; ignore.
93
+ }
94
+ this.states.delete(entity);
95
+ }
96
+ onDragStart(entity) {
97
+ const id = entity.getValue(UIWindow, 'windowId');
98
+ const manager = windowManagerFor(this.world);
99
+ if (id && manager.has(id)) {
100
+ manager.focus(id);
101
+ // Mark dragging BEFORE the dock change so the pin label reads "PIN"
102
+ // (loose in hand) throughout the drag, not "UNPIN".
103
+ manager.setDragging(id, true);
104
+ }
105
+ // Grabbing takes the window out of any region and out of follow mode -
106
+ // the user is placing it by hand.
107
+ if (entity.hasComponent(UIDockedTo)) {
108
+ entity.removeComponent(UIDockedTo);
109
+ }
110
+ if (entity.getValue(UIWindow, 'dockMode') !== DockMode.WorldLocked) {
111
+ entity.setValue(UIWindow, 'dockMode', DockMode.WorldLocked);
112
+ }
113
+ }
114
+ whileDragging(entity) {
115
+ if (!entity.getValue(UIWindow, 'billboardWhileDragging')) {
116
+ return;
117
+ }
118
+ const object = entity.object3D;
119
+ if (!object) {
120
+ return;
121
+ }
122
+ object.getWorldPosition(this.worldPosition);
123
+ this.camera.getWorldPosition(this.viewerPosition);
124
+ const yaw = faceViewerYaw([this.worldPosition.x, this.worldPosition.y, this.worldPosition.z], [this.viewerPosition.x, this.viewerPosition.y, this.viewerPosition.z], object.rotation.y);
125
+ object.rotation.set(0, yaw, 0);
126
+ }
127
+ onDragEnd(entity) {
128
+ const id = entity.getValue(UIWindow, 'windowId');
129
+ const manager = windowManagerFor(this.world);
130
+ if (id && manager.has(id)) {
131
+ manager.setDragging(id, false); // released → placed → label "UNPIN"
132
+ }
133
+ // Settle facing the viewer at the drop spot - the window keeps the
134
+ // orientation it was dragged in, not its pre-drag rotation.
135
+ this.whileDragging(entity);
136
+ const object = entity.object3D;
137
+ if (!object) {
138
+ return;
139
+ }
140
+ object.getWorldPosition(this.worldPosition);
141
+ const dropPoint = [
142
+ this.worldPosition.x,
143
+ this.worldPosition.y,
144
+ this.worldPosition.z,
145
+ ];
146
+ const registry = regionRegistryFor(this.world);
147
+ const origins = new Map();
148
+ this.queries.regions.entities.forEach((regionEntity) => {
149
+ const regionId = regionEntity.getValue(UIDockRegion, 'regionId');
150
+ const regionObject = regionEntity.object3D;
151
+ if (!regionId || !regionObject) {
152
+ return;
153
+ }
154
+ regionObject.getWorldPosition(this.viewerPosition);
155
+ origins.set(regionId, [
156
+ this.viewerPosition.x,
157
+ this.viewerPosition.y,
158
+ this.viewerPosition.z,
159
+ ]);
160
+ });
161
+ const captured = registry.capture(dropPoint, origins);
162
+ if (captured !== undefined) {
163
+ entity.addComponent(UIDockedTo, { regionId: captured });
164
+ }
165
+ }
166
+ }
167
+ //# sourceMappingURL=drag-system.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drag-system.js","sourceRoot":"","sources":["../../src/systems/drag-system.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * UIDragSystem - drag windows by the title bar.\n *\n * Reuses `@pmndrs/handle` (the exact library behind the IWSDK's grab\n * systems): a `HandleStore` targets the window entity's object while being\n * *bound* to the title-bar uikit element, so grabbing the title bar with the\n * ray (or a pinch) moves the whole window with the same feel as native IWSDK\n * grabbing. The system pumps its own stores, so it works even when the app\n * doesn't enable `features.grabbing`.\n *\n * Dragging implies placing: on grab the window leaves any dock region and\n * switches to `world-locked`; on release near a region it docks there.\n */\nimport { HandleStore } from '@pmndrs/handle';\nimport {\n PanelDocument,\n UIKitDocument,\n Object3D,\n Vector3,\n createSystem,\n type Entity,\n} from '@iwsdk/core';\nimport { UIDockRegion, UIDockedTo, UIWindow, UIWindowState } from '../components.js';\nimport { WINDOW_CHROME_IDS } from '@realitycollective/webxr-uiextensions';\nimport { DockMode } from '@realitycollective/webxr-uiextensions';\nimport { faceViewerYaw } from '@realitycollective/webxr-uiextensions';\nimport { HoldToDrag } from '@realitycollective/webxr-uiextensions';\nimport type { Vec3 } from '@realitycollective/webxr-uiextensions';\nimport { regionRegistryFor } from '../region-registry-store.js';\nimport { windowManagerFor } from '../manager-registry.js';\n\ninterface DragState {\n store: HandleStore<unknown>;\n hold: HoldToDrag;\n}\n\nexport class UIDragSystem extends createSystem({\n windows: { required: [UIWindow, UIWindowState, PanelDocument] },\n regions: { required: [UIDockRegion] },\n}) {\n private states = new Map<Entity, DragState>();\n private worldPosition = new Vector3();\n private viewerPosition = new Vector3();\n\n override init(): void {\n this.cleanupFuncs.push(\n this.queries.windows.subscribe('qualify', (entity) => {\n this.attach(entity);\n }),\n this.queries.windows.subscribe('disqualify', (entity) => {\n this.detach(entity);\n }),\n );\n this.queries.windows.entities.forEach((entity) => this.attach(entity));\n }\n\n override update(delta: number): void {\n for (const [entity, state] of this.states) {\n const held = state.store.inputState.size > 0;\n const { phase, began, ended } = state.hold.update(held, delta);\n\n if (began) {\n this.onDragStart(entity);\n }\n if (phase === 'dragging') {\n // The store only runs while an actual drag is in progress: presses\n // shorter than the hold delay never move the window (they're\n // clicks), and skipping the release-frame update stops the handle\n // from restoring its grab-time rotation over our billboard yaw.\n state.store.update(delta);\n this.whileDragging(entity);\n }\n if (ended) {\n this.onDragEnd(entity);\n }\n }\n }\n\n private attach(entity: Entity): void {\n if (this.states.has(entity) || !entity.getValue(UIWindow, 'movable')) {\n return;\n }\n const document = PanelDocument.data.document[entity.index] as\n | UIKitDocument\n | undefined;\n const titlebar = document?.getElementById(WINDOW_CHROME_IDS.titlebar);\n const object = entity.object3D;\n if (!titlebar || !object) {\n return;\n }\n const store = new HandleStore(object, () => ({\n translate: true as const,\n rotate: false as const,\n scale: false as const,\n multitouch: false,\n projectRays: false,\n }));\n // The title-bar uikit element is an Object3D - bind it as the grab\n // surface while the store's target stays the window root.\n store.bind(titlebar as unknown as Object3D<never>);\n this.states.set(entity, {\n store,\n hold: new HoldToDrag(Math.max(0, entity.getValue(UIWindow, 'dragDelay') as number)),\n });\n }\n\n private detach(entity: Entity): void {\n const state = this.states.get(entity);\n if (!state) {\n return;\n }\n try {\n state.store.cancel();\n } catch {\n // Handle stores throw when cancelled with no active input; ignore.\n }\n this.states.delete(entity);\n }\n\n private onDragStart(entity: Entity): void {\n const id = entity.getValue(UIWindow, 'windowId') as string;\n const manager = windowManagerFor(this.world);\n if (id && manager.has(id)) {\n manager.focus(id);\n // Mark dragging BEFORE the dock change so the pin label reads \"PIN\"\n // (loose in hand) throughout the drag, not \"UNPIN\".\n manager.setDragging(id, true);\n }\n // Grabbing takes the window out of any region and out of follow mode -\n // the user is placing it by hand.\n if (entity.hasComponent(UIDockedTo)) {\n entity.removeComponent(UIDockedTo);\n }\n if (entity.getValue(UIWindow, 'dockMode') !== DockMode.WorldLocked) {\n entity.setValue(UIWindow, 'dockMode', DockMode.WorldLocked);\n }\n }\n\n private whileDragging(entity: Entity): void {\n if (!entity.getValue(UIWindow, 'billboardWhileDragging')) {\n return;\n }\n const object = entity.object3D;\n if (!object) {\n return;\n }\n object.getWorldPosition(this.worldPosition);\n this.camera.getWorldPosition(this.viewerPosition);\n const yaw = faceViewerYaw(\n [this.worldPosition.x, this.worldPosition.y, this.worldPosition.z],\n [this.viewerPosition.x, this.viewerPosition.y, this.viewerPosition.z],\n object.rotation.y,\n );\n object.rotation.set(0, yaw, 0);\n }\n\n private onDragEnd(entity: Entity): void {\n const id = entity.getValue(UIWindow, 'windowId') as string;\n const manager = windowManagerFor(this.world);\n if (id && manager.has(id)) {\n manager.setDragging(id, false); // released → placed → label \"UNPIN\"\n }\n // Settle facing the viewer at the drop spot - the window keeps the\n // orientation it was dragged in, not its pre-drag rotation.\n this.whileDragging(entity);\n const object = entity.object3D;\n if (!object) {\n return;\n }\n object.getWorldPosition(this.worldPosition);\n const dropPoint: Vec3 = [\n this.worldPosition.x,\n this.worldPosition.y,\n this.worldPosition.z,\n ];\n\n const registry = regionRegistryFor(this.world);\n const origins = new Map<string, Vec3>();\n this.queries.regions.entities.forEach((regionEntity) => {\n const regionId = regionEntity.getValue(UIDockRegion, 'regionId') as string;\n const regionObject = regionEntity.object3D;\n if (!regionId || !regionObject) {\n return;\n }\n regionObject.getWorldPosition(this.viewerPosition);\n origins.set(regionId, [\n this.viewerPosition.x,\n this.viewerPosition.y,\n this.viewerPosition.z,\n ]);\n });\n\n const captured = registry.capture(dropPoint, origins);\n if (captured !== undefined) {\n entity.addComponent(UIDockedTo, { regionId: captured });\n }\n }\n}\n"]}