@playcanvas/web-components 0.3.0 → 0.5.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.
Files changed (40) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +84 -84
  3. package/dist/components/light-component.d.ts +48 -0
  4. package/dist/components/splat-component.d.ts +0 -13
  5. package/dist/pwc.cjs +275 -207
  6. package/dist/pwc.cjs.map +1 -1
  7. package/dist/pwc.js +275 -207
  8. package/dist/pwc.js.map +1 -1
  9. package/dist/pwc.min.js +1 -1
  10. package/dist/pwc.min.js.map +1 -1
  11. package/dist/pwc.mjs +276 -208
  12. package/dist/pwc.mjs.map +1 -1
  13. package/package.json +76 -66
  14. package/src/app.ts +612 -606
  15. package/src/asset.ts +159 -159
  16. package/src/async-element.ts +46 -46
  17. package/src/colors.ts +150 -150
  18. package/src/components/camera-component.ts +557 -557
  19. package/src/components/collision-component.ts +183 -183
  20. package/src/components/component.ts +97 -97
  21. package/src/components/element-component.ts +367 -367
  22. package/src/components/light-component.ts +570 -466
  23. package/src/components/listener-component.ts +30 -30
  24. package/src/components/particlesystem-component.ts +155 -155
  25. package/src/components/render-component.ts +147 -147
  26. package/src/components/rigidbody-component.ts +227 -227
  27. package/src/components/screen-component.ts +157 -157
  28. package/src/components/script-component.ts +270 -270
  29. package/src/components/script.ts +90 -90
  30. package/src/components/sound-component.ts +230 -230
  31. package/src/components/sound-slot.ts +288 -288
  32. package/src/components/splat-component.ts +102 -133
  33. package/src/entity.ts +360 -360
  34. package/src/index.ts +63 -63
  35. package/src/material.ts +141 -141
  36. package/src/model.ts +111 -111
  37. package/src/module.ts +43 -43
  38. package/src/scene.ts +217 -217
  39. package/src/sky.ts +293 -293
  40. package/src/utils.ts +71 -71
package/src/app.ts CHANGED
@@ -1,606 +1,612 @@
1
- import {
2
- AppBase,
3
- AppOptions,
4
- CameraComponent,
5
- createGraphicsDevice,
6
- FILLMODE_FILL_WINDOW,
7
- GraphNode,
8
- Keyboard,
9
- Mouse,
10
- Picker,
11
- RESOLUTION_AUTO,
12
- AnimComponentSystem,
13
- AnimationComponentSystem,
14
- AudioListenerComponentSystem,
15
- ButtonComponentSystem,
16
- CameraComponentSystem,
17
- CollisionComponentSystem,
18
- ElementComponentSystem,
19
- GSplatComponentSystem,
20
- JointComponentSystem,
21
- LayoutChildComponentSystem,
22
- LayoutGroupComponentSystem,
23
- LightComponentSystem,
24
- ModelComponentSystem,
25
- ParticleSystemComponentSystem,
26
- RenderComponentSystem,
27
- RigidBodyComponentSystem,
28
- ScriptComponentSystem,
29
- ScreenComponentSystem,
30
- ScrollbarComponentSystem,
31
- ScrollViewComponentSystem,
32
- SoundComponentSystem,
33
- SpriteComponentSystem,
34
- ZoneComponentSystem,
35
- RenderHandler,
36
- AnimationHandler,
37
- AnimClipHandler,
38
- AnimStateGraphHandler,
39
- AudioHandler,
40
- BinaryHandler,
41
- ContainerHandler,
42
- CssHandler,
43
- CubemapHandler,
44
- FolderHandler,
45
- FontHandler,
46
- GSplatHandler,
47
- HierarchyHandler,
48
- HtmlHandler,
49
- JsonHandler,
50
- MaterialHandler,
51
- ModelHandler,
52
- SceneHandler,
53
- ScriptHandler,
54
- ShaderHandler,
55
- SpriteHandler,
56
- TemplateHandler,
57
- TextHandler,
58
- TextureHandler,
59
- TextureAtlasHandler,
60
- BatchManager,
61
- SoundManager,
62
- Lightmapper,
63
- XrManager
64
- } from 'playcanvas';
65
-
66
- import { AssetElement } from './asset';
67
- import { AsyncElement } from './async-element';
68
- import { EntityElement } from './entity';
69
- import { MaterialElement } from './material';
70
- import { ModuleElement } from './module';
71
-
72
- /**
73
- * The AppElement interface provides properties and methods for manipulating
74
- * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-app/ | `<pc-app>`} elements.
75
- * The AppElement interface also inherits the properties and methods of the
76
- * {@link HTMLElement} interface.
77
- */
78
- class AppElement extends AsyncElement {
79
- /**
80
- * The canvas element.
81
- */
82
- private _canvas: HTMLCanvasElement | null = null;
83
-
84
- private _alpha = true;
85
-
86
- private _backend: 'webgpu' | 'webgl2' | 'null' = 'webgl2';
87
-
88
- private _antialias = true;
89
-
90
- private _depth = true;
91
-
92
- private _stencil = true;
93
-
94
- private _highResolution = true;
95
-
96
- private _hierarchyReady = false;
97
-
98
- private _picker: Picker | null = null;
99
-
100
- private _hasPointerListeners: { [key: string]: boolean } = {
101
- pointerenter: false,
102
- pointerleave: false,
103
- pointerdown: false,
104
- pointerup: false,
105
- pointermove: false
106
- };
107
-
108
- private _hoveredEntity: EntityElement | null = null;
109
-
110
- private _pointerHandlers: { [key: string]: EventListener | null } = {
111
- pointermove: null,
112
- pointerdown: null,
113
- pointerup: null
114
- };
115
-
116
- /**
117
- * The PlayCanvas application instance.
118
- */
119
- app: AppBase | null = null;
120
-
121
- /**
122
- * Creates a new AppElement instance.
123
- *
124
- * @ignore
125
- */
126
- constructor() {
127
- super();
128
-
129
- // Bind methods to maintain 'this' context
130
- this._onWindowResize = this._onWindowResize.bind(this);
131
- }
132
-
133
- async connectedCallback() {
134
- // Get all pc-module elements that are direct children of the pc-app element
135
- const moduleElements = this.querySelectorAll<ModuleElement>(':scope > pc-module');
136
-
137
- // Wait for all modules to load
138
- await Promise.all(Array.from(moduleElements).map(module => module.getLoadPromise()));
139
-
140
- // Create and append the canvas to the element
141
- this._canvas = document.createElement('canvas');
142
- this.appendChild(this._canvas);
143
-
144
- // Configure device types based on backend selection
145
- const backendToDeviceTypes: { [key: string]: string[] } = {
146
- webgpu: ['webgpu', 'webgl2'], // fallback to webgl2 if webgpu not available
147
- webgl2: ['webgl2'],
148
- null: ['null']
149
- };
150
- const deviceTypes = backendToDeviceTypes[this._backend] || [];
151
-
152
- const device = await createGraphicsDevice(this._canvas, {
153
- // @ts-ignore - alpha needs to be documented
154
- alpha: this._alpha,
155
- antialias: this._antialias,
156
- depth: this._depth,
157
- deviceTypes: deviceTypes,
158
- stencil: this._stencil
159
- });
160
- device.maxPixelRatio = this._highResolution ? window.devicePixelRatio : 1;
161
-
162
- const createOptions = new AppOptions();
163
- createOptions.graphicsDevice = device;
164
- createOptions.keyboard = new Keyboard(window);
165
- createOptions.mouse = new Mouse(this._canvas);
166
- createOptions.componentSystems = [
167
- AnimComponentSystem,
168
- AnimationComponentSystem,
169
- AudioListenerComponentSystem,
170
- ButtonComponentSystem,
171
- CameraComponentSystem,
172
- CollisionComponentSystem,
173
- ElementComponentSystem,
174
- GSplatComponentSystem,
175
- JointComponentSystem,
176
- LayoutChildComponentSystem,
177
- LayoutGroupComponentSystem,
178
- LightComponentSystem,
179
- ModelComponentSystem,
180
- ParticleSystemComponentSystem,
181
- RenderComponentSystem,
182
- RigidBodyComponentSystem,
183
- ScreenComponentSystem,
184
- ScriptComponentSystem,
185
- ScrollbarComponentSystem,
186
- ScrollViewComponentSystem,
187
- SoundComponentSystem,
188
- SpriteComponentSystem,
189
- ZoneComponentSystem
190
- ];
191
- createOptions.resourceHandlers = [
192
- AnimClipHandler,
193
- AnimationHandler,
194
- AnimStateGraphHandler,
195
- AudioHandler,
196
- BinaryHandler,
197
- CssHandler,
198
- ContainerHandler,
199
- CubemapHandler,
200
- FolderHandler,
201
- FontHandler,
202
- GSplatHandler,
203
- HierarchyHandler,
204
- HtmlHandler,
205
- JsonHandler,
206
- MaterialHandler,
207
- ModelHandler,
208
- RenderHandler,
209
- ScriptHandler,
210
- SceneHandler,
211
- ShaderHandler,
212
- SpriteHandler,
213
- TemplateHandler,
214
- TextHandler,
215
- TextureAtlasHandler,
216
- TextureHandler
217
- ];
218
- createOptions.soundManager = new SoundManager();
219
- createOptions.lightmapper = Lightmapper;
220
- createOptions.batchManager = BatchManager;
221
- createOptions.xr = XrManager;
222
-
223
- this.app = new AppBase(this._canvas);
224
- this.app.init(createOptions);
225
-
226
- this.app.setCanvasFillMode(FILLMODE_FILL_WINDOW);
227
- this.app.setCanvasResolution(RESOLUTION_AUTO);
228
-
229
- this._pickerCreate();
230
-
231
- // Get all pc-asset elements that are direct children of the pc-app element
232
- const assetElements = this.querySelectorAll<AssetElement>(':scope > pc-asset');
233
- Array.from(assetElements).forEach((assetElement) => {
234
- assetElement.createAsset();
235
- const asset = assetElement.asset;
236
- if (asset) {
237
- this.app!.assets.add(asset);
238
- }
239
- });
240
-
241
- // Get all pc-material elements that are direct children of the pc-app element
242
- const materialElements = this.querySelectorAll<MaterialElement>(':scope > pc-material');
243
- Array.from(materialElements).forEach((materialElement) => {
244
- materialElement.createMaterial();
245
- });
246
-
247
- // Create all entities
248
- const entityElements = this.querySelectorAll<EntityElement>('pc-entity');
249
- Array.from(entityElements).forEach((entityElement) => {
250
- entityElement.createEntity(this.app!);
251
- });
252
-
253
- // Build hierarchy
254
- entityElements.forEach((entityElement) => {
255
- entityElement.buildHierarchy(this.app!);
256
- });
257
-
258
- this._hierarchyReady = true;
259
-
260
- // Load assets before starting the application
261
- this.app.preload(() => {
262
- // Start the application
263
- this.app!.start();
264
-
265
- // Handle window resize to keep the canvas responsive
266
- window.addEventListener('resize', this._onWindowResize);
267
-
268
- this._onReady();
269
- });
270
- }
271
-
272
- disconnectedCallback() {
273
- this._pickerDestroy();
274
-
275
- // Clean up the application
276
- if (this.app) {
277
- this.app.destroy();
278
- this.app = null;
279
- }
280
-
281
- // Remove event listeners
282
- window.removeEventListener('resize', this._onWindowResize);
283
-
284
- // Remove the canvas
285
- if (this._canvas && this.contains(this._canvas)) {
286
- this.removeChild(this._canvas);
287
- this._canvas = null;
288
- }
289
- }
290
-
291
- _onWindowResize() {
292
- if (this.app) {
293
- this.app.resizeCanvas();
294
- }
295
- }
296
-
297
- _pickerCreate() {
298
- const { width, height } = this.app!.graphicsDevice;
299
- this._picker = new Picker(this.app!, width, height);
300
-
301
- // Create bound handlers but don't attach them yet
302
- this._pointerHandlers.pointermove = this._onPointerMove.bind(this) as EventListener;
303
- this._pointerHandlers.pointerdown = this._onPointerDown.bind(this) as EventListener;
304
- this._pointerHandlers.pointerup = this._onPointerUp.bind(this) as EventListener;
305
-
306
- // Listen for pointer listeners being added/removed
307
- ['pointermove', 'pointerdown', 'pointerup', 'pointerenter', 'pointerleave'].forEach((type) => {
308
- this.addEventListener(`${type}:connect`, () => this._onPointerListenerAdded(type));
309
- this.addEventListener(`${type}:disconnect`, () => this._onPointerListenerRemoved(type));
310
- });
311
- }
312
-
313
- _pickerDestroy() {
314
- if (this._canvas) {
315
- Object.entries(this._pointerHandlers).forEach(([type, handler]) => {
316
- if (handler) {
317
- this._canvas!.removeEventListener(type, handler);
318
- }
319
- });
320
- }
321
-
322
- this._picker = null;
323
- this._pointerHandlers = {
324
- pointermove: null,
325
- pointerdown: null,
326
- pointerup: null
327
- };
328
- }
329
-
330
- // New helper to convert CSS coordinates to canvas (picker) coordinates
331
- private _getPickerCoordinates(event: PointerEvent): { x: number, y: number } {
332
- // Get the canvas' bounding rectangle in CSS pixels.
333
- const canvasRect = this._canvas!.getBoundingClientRect();
334
- // Compute scale factors based on canvas actual resolution vs. its CSS display size.
335
- const scaleX = this._canvas!.width / canvasRect.width;
336
- const scaleY = this._canvas!.height / canvasRect.height;
337
- // Convert the client coordinates accordingly.
338
- const x = (event.clientX - canvasRect.left) * scaleX;
339
- const y = (event.clientY - canvasRect.top) * scaleY;
340
- return { x, y };
341
- }
342
-
343
- _onPointerMove(event: PointerEvent) {
344
- if (!this._picker || !this.app) return;
345
-
346
- const camera = this.app!.root.findComponent('camera') as CameraComponent;
347
- if (!camera) return;
348
-
349
- // Use the helper to convert event coordinates into canvas/picker coordinates.
350
- const { x, y } = this._getPickerCoordinates(event);
351
-
352
- this._picker.prepare(camera, this.app!.scene);
353
- const selection = this._picker.getSelection(x, y);
354
-
355
- // Get the currently hovered entity by walking up the hierarchy
356
- let newHoverEntity: EntityElement | null = null;
357
- if (selection.length > 0) {
358
- let currentNode: GraphNode | null = selection[0].node;
359
- while (currentNode !== null) {
360
- const entityElement = this.querySelector(`pc-entity[name="${currentNode.name}"]`) as EntityElement;
361
- if (entityElement) {
362
- newHoverEntity = entityElement;
363
- break;
364
- }
365
- currentNode = currentNode.parent;
366
- }
367
- }
368
-
369
- // Handle enter/leave events
370
- if (this._hoveredEntity !== newHoverEntity) {
371
- if (this._hoveredEntity && this._hoveredEntity.hasListeners('pointerleave')) {
372
- this._hoveredEntity.dispatchEvent(new PointerEvent('pointerleave', event));
373
- }
374
- if (newHoverEntity && newHoverEntity.hasListeners('pointerenter')) {
375
- newHoverEntity.dispatchEvent(new PointerEvent('pointerenter', event));
376
- }
377
- }
378
-
379
- // Update hover state
380
- this._hoveredEntity = newHoverEntity;
381
-
382
- // Handle pointermove event
383
- if (newHoverEntity && newHoverEntity.hasListeners('pointermove')) {
384
- newHoverEntity.dispatchEvent(new PointerEvent('pointermove', event));
385
- }
386
- }
387
-
388
- _onPointerDown(event: PointerEvent) {
389
- if (!this._picker || !this.app) return;
390
-
391
- const camera = this.app!.root.findComponent('camera') as CameraComponent;
392
- if (!camera) return;
393
-
394
- // Convert the event's pointer coordinates
395
- const { x, y } = this._getPickerCoordinates(event);
396
-
397
- this._picker.prepare(camera, this.app!.scene);
398
- const selection = this._picker.getSelection(x, y);
399
-
400
- if (selection.length > 0) {
401
- let currentNode: GraphNode | null = selection[0].node;
402
- while (currentNode !== null) {
403
- const entityElement = this.querySelector(`pc-entity[name="${currentNode.name}"]`) as EntityElement;
404
- if (entityElement && entityElement.hasListeners('pointerdown')) {
405
- entityElement.dispatchEvent(new PointerEvent('pointerdown', event));
406
- break;
407
- }
408
- currentNode = currentNode.parent;
409
- }
410
- }
411
- }
412
-
413
- _onPointerUp(event: PointerEvent) {
414
- if (!this._picker || !this.app) return;
415
-
416
- const camera = this.app!.root.findComponent('camera') as CameraComponent;
417
- if (!camera) return;
418
-
419
- // Convert CSS coordinates to picker coordinates
420
- const { x, y } = this._getPickerCoordinates(event);
421
-
422
- this._picker.prepare(camera, this.app!.scene);
423
- const selection = this._picker.getSelection(x, y);
424
-
425
- if (selection.length > 0) {
426
- const entityElement = this.querySelector(`pc-entity[name="${selection[0].node.name}"]`) as EntityElement;
427
- if (entityElement && entityElement.hasListeners('pointerup')) {
428
- entityElement.dispatchEvent(new PointerEvent('pointerup', event));
429
- }
430
- }
431
- }
432
-
433
- _onPointerListenerAdded(type: string) {
434
- if (!this._hasPointerListeners[type] && this._canvas) {
435
- this._hasPointerListeners[type] = true;
436
-
437
- // For enter/leave events, we need the move handler
438
- const handler = (type === 'pointerenter' || type === 'pointerleave') ?
439
- this._pointerHandlers.pointermove :
440
- this._pointerHandlers[type];
441
-
442
- if (handler) {
443
- this._canvas.addEventListener(type === 'pointerenter' || type === 'pointerleave' ? 'pointermove' : type, handler);
444
- }
445
- }
446
- }
447
-
448
- _onPointerListenerRemoved(type: string) {
449
- const hasListeners = Array.from(this.querySelectorAll<EntityElement>('pc-entity'))
450
- .some(entity => entity.hasListeners(type));
451
-
452
- if (!hasListeners && this._canvas) {
453
- this._hasPointerListeners[type] = false;
454
-
455
- const handler = (type === 'pointerenter' || type === 'pointerleave') ?
456
- this._pointerHandlers.pointermove :
457
- this._pointerHandlers[type];
458
-
459
- if (handler) {
460
- this._canvas.removeEventListener(type === 'pointerenter' || type === 'pointerleave' ? 'pointermove' : type, handler);
461
- }
462
- }
463
- }
464
-
465
- /**
466
- * Sets the alpha flag.
467
- * @param value - The alpha flag.
468
- */
469
- set alpha(value: boolean) {
470
- this._alpha = value;
471
- }
472
-
473
- /**
474
- * Gets the alpha flag.
475
- * @returns The alpha flag.
476
- */
477
- get alpha() {
478
- return this._alpha;
479
- }
480
-
481
- /**
482
- * Sets the antialias flag.
483
- * @param value - The antialias flag.
484
- */
485
- set antialias(value: boolean) {
486
- this._antialias = value;
487
- }
488
-
489
- /**
490
- * Gets the antialias flag.
491
- * @returns The antialias flag.
492
- */
493
- get antialias() {
494
- return this._antialias;
495
- }
496
-
497
- /**
498
- * Sets the graphics backend.
499
- * @param value - The graphics backend ('webgpu', 'webgl2', or 'null').
500
- */
501
- set backend(value: 'webgpu' | 'webgl2' | 'null') {
502
- this._backend = value;
503
- }
504
-
505
- /**
506
- * Gets the graphics backend.
507
- * @returns The graphics backend.
508
- */
509
- get backend() {
510
- return this._backend;
511
- }
512
-
513
- /**
514
- * Sets the depth flag.
515
- * @param value - The depth flag.
516
- */
517
- set depth(value: boolean) {
518
- this._depth = value;
519
- }
520
-
521
- /**
522
- * Gets the depth flag.
523
- * @returns The depth flag.
524
- */
525
- get depth() {
526
- return this._depth;
527
- }
528
-
529
- /**
530
- * Gets the hierarchy ready flag.
531
- * @returns The hierarchy ready flag.
532
- * @ignore
533
- */
534
- get hierarchyReady() {
535
- return this._hierarchyReady;
536
- }
537
-
538
- /**
539
- * Sets the high resolution flag. When true, the application will render at the device's
540
- * physical resolution. When false, the application will render at CSS resolution.
541
- * @param value - The high resolution flag.
542
- */
543
- set highResolution(value: boolean) {
544
- this._highResolution = value;
545
- if (this.app) {
546
- this.app.graphicsDevice.maxPixelRatio = value ? window.devicePixelRatio : 1;
547
- }
548
- }
549
-
550
- /**
551
- * Gets the high resolution flag.
552
- * @returns The high resolution flag.
553
- */
554
- get highResolution() {
555
- return this._highResolution;
556
- }
557
-
558
- /**
559
- * Sets the stencil flag.
560
- * @param value - The stencil flag.
561
- */
562
- set stencil(value: boolean) {
563
- this._stencil = value;
564
- }
565
-
566
- /**
567
- * Gets the stencil flag.
568
- * @returns The stencil flag.
569
- */
570
- get stencil() {
571
- return this._stencil;
572
- }
573
-
574
- static get observedAttributes() {
575
- return ['alpha', 'antialias', 'backend', 'depth', 'stencil', 'high-resolution'];
576
- }
577
-
578
- attributeChangedCallback(name: string, _oldValue: string, newValue: string) {
579
- switch (name) {
580
- case 'alpha':
581
- this.alpha = newValue !== 'false';
582
- break;
583
- case 'antialias':
584
- this.antialias = newValue !== 'false';
585
- break;
586
- case 'backend':
587
- if (newValue === 'webgpu' || newValue === 'webgl2' || newValue === 'null') {
588
- this.backend = newValue;
589
- }
590
- break;
591
- case 'depth':
592
- this.depth = newValue !== 'false';
593
- break;
594
- case 'high-resolution':
595
- this.highResolution = newValue !== 'false';
596
- break;
597
- case 'stencil':
598
- this.stencil = newValue !== 'false';
599
- break;
600
- }
601
- }
602
- }
603
-
604
- customElements.define('pc-app', AppElement);
605
-
606
- export { AppElement };
1
+ import {
2
+ AppBase,
3
+ AppOptions,
4
+ CameraComponent,
5
+ createGraphicsDevice,
6
+ FILLMODE_FILL_WINDOW,
7
+ GraphNode,
8
+ Keyboard,
9
+ Mouse,
10
+ Picker,
11
+ RESOLUTION_AUTO,
12
+ AnimComponentSystem,
13
+ AnimationComponentSystem,
14
+ AudioListenerComponentSystem,
15
+ ButtonComponentSystem,
16
+ CameraComponentSystem,
17
+ CollisionComponentSystem,
18
+ ElementComponentSystem,
19
+ GSplatComponentSystem,
20
+ JointComponentSystem,
21
+ LayoutChildComponentSystem,
22
+ LayoutGroupComponentSystem,
23
+ LightComponentSystem,
24
+ ModelComponentSystem,
25
+ ParticleSystemComponentSystem,
26
+ RenderComponentSystem,
27
+ RigidBodyComponentSystem,
28
+ ScriptComponentSystem,
29
+ ScreenComponentSystem,
30
+ ScrollbarComponentSystem,
31
+ ScrollViewComponentSystem,
32
+ SoundComponentSystem,
33
+ SpriteComponentSystem,
34
+ ZoneComponentSystem,
35
+ RenderHandler,
36
+ AnimationHandler,
37
+ AnimClipHandler,
38
+ AnimStateGraphHandler,
39
+ AudioHandler,
40
+ BinaryHandler,
41
+ ContainerHandler,
42
+ CssHandler,
43
+ CubemapHandler,
44
+ FolderHandler,
45
+ FontHandler,
46
+ GSplatHandler,
47
+ HierarchyHandler,
48
+ HtmlHandler,
49
+ JsonHandler,
50
+ MaterialHandler,
51
+ ModelHandler,
52
+ SceneHandler,
53
+ ScriptHandler,
54
+ ShaderHandler,
55
+ SpriteHandler,
56
+ TemplateHandler,
57
+ TextHandler,
58
+ TextureHandler,
59
+ TextureAtlasHandler,
60
+ BatchManager,
61
+ SoundManager,
62
+ Lightmapper,
63
+ XrManager,
64
+ MeshInstance,
65
+ GSplatComponent
66
+ } from 'playcanvas';
67
+
68
+ import { AssetElement } from './asset';
69
+ import { AsyncElement } from './async-element';
70
+ import { EntityElement } from './entity';
71
+ import { MaterialElement } from './material';
72
+ import { ModuleElement } from './module';
73
+
74
+ /**
75
+ * The AppElement interface provides properties and methods for manipulating
76
+ * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-app/ | `<pc-app>`} elements.
77
+ * The AppElement interface also inherits the properties and methods of the
78
+ * {@link HTMLElement} interface.
79
+ */
80
+ class AppElement extends AsyncElement {
81
+ /**
82
+ * The canvas element.
83
+ */
84
+ private _canvas: HTMLCanvasElement | null = null;
85
+
86
+ private _alpha = true;
87
+
88
+ private _backend: 'webgpu' | 'webgl2' | 'null' = 'webgl2';
89
+
90
+ private _antialias = true;
91
+
92
+ private _depth = true;
93
+
94
+ private _stencil = true;
95
+
96
+ private _highResolution = true;
97
+
98
+ private _hierarchyReady = false;
99
+
100
+ private _picker: Picker | null = null;
101
+
102
+ private _hasPointerListeners: { [key: string]: boolean } = {
103
+ pointerenter: false,
104
+ pointerleave: false,
105
+ pointerdown: false,
106
+ pointerup: false,
107
+ pointermove: false
108
+ };
109
+
110
+ private _hoveredEntity: EntityElement | null = null;
111
+
112
+ private _pointerHandlers: { [key: string]: EventListener | null } = {
113
+ pointermove: null,
114
+ pointerdown: null,
115
+ pointerup: null
116
+ };
117
+
118
+ /**
119
+ * The PlayCanvas application instance.
120
+ */
121
+ app: AppBase | null = null;
122
+
123
+ /**
124
+ * Creates a new AppElement instance.
125
+ *
126
+ * @ignore
127
+ */
128
+ constructor() {
129
+ super();
130
+
131
+ // Bind methods to maintain 'this' context
132
+ this._onWindowResize = this._onWindowResize.bind(this);
133
+ }
134
+
135
+ async connectedCallback() {
136
+ // Get all pc-module elements that are direct children of the pc-app element
137
+ const moduleElements = this.querySelectorAll<ModuleElement>(':scope > pc-module');
138
+
139
+ // Wait for all modules to load
140
+ await Promise.all(Array.from(moduleElements).map(module => module.getLoadPromise()));
141
+
142
+ // Create and append the canvas to the element
143
+ this._canvas = document.createElement('canvas');
144
+ this.appendChild(this._canvas);
145
+
146
+ // Configure device types based on backend selection
147
+ const backendToDeviceTypes: { [key: string]: string[] } = {
148
+ webgpu: ['webgpu', 'webgl2'], // fallback to webgl2 if webgpu not available
149
+ webgl2: ['webgl2'],
150
+ null: ['null']
151
+ };
152
+ const deviceTypes = backendToDeviceTypes[this._backend] || [];
153
+
154
+ const device = await createGraphicsDevice(this._canvas, {
155
+ // @ts-ignore - alpha needs to be documented
156
+ alpha: this._alpha,
157
+ antialias: this._antialias,
158
+ depth: this._depth,
159
+ deviceTypes: deviceTypes,
160
+ stencil: this._stencil
161
+ });
162
+ device.maxPixelRatio = this._highResolution ? window.devicePixelRatio : 1;
163
+
164
+ const createOptions = new AppOptions();
165
+ createOptions.graphicsDevice = device;
166
+ createOptions.keyboard = new Keyboard(window);
167
+ createOptions.mouse = new Mouse(this._canvas);
168
+ createOptions.componentSystems = [
169
+ AnimComponentSystem,
170
+ AnimationComponentSystem,
171
+ AudioListenerComponentSystem,
172
+ ButtonComponentSystem,
173
+ CameraComponentSystem,
174
+ CollisionComponentSystem,
175
+ ElementComponentSystem,
176
+ GSplatComponentSystem,
177
+ JointComponentSystem,
178
+ LayoutChildComponentSystem,
179
+ LayoutGroupComponentSystem,
180
+ LightComponentSystem,
181
+ ModelComponentSystem,
182
+ ParticleSystemComponentSystem,
183
+ RenderComponentSystem,
184
+ RigidBodyComponentSystem,
185
+ ScreenComponentSystem,
186
+ ScriptComponentSystem,
187
+ ScrollbarComponentSystem,
188
+ ScrollViewComponentSystem,
189
+ SoundComponentSystem,
190
+ SpriteComponentSystem,
191
+ ZoneComponentSystem
192
+ ];
193
+ createOptions.resourceHandlers = [
194
+ AnimClipHandler,
195
+ AnimationHandler,
196
+ AnimStateGraphHandler,
197
+ AudioHandler,
198
+ BinaryHandler,
199
+ CssHandler,
200
+ ContainerHandler,
201
+ CubemapHandler,
202
+ FolderHandler,
203
+ FontHandler,
204
+ GSplatHandler,
205
+ HierarchyHandler,
206
+ HtmlHandler,
207
+ JsonHandler,
208
+ MaterialHandler,
209
+ ModelHandler,
210
+ RenderHandler,
211
+ ScriptHandler,
212
+ SceneHandler,
213
+ ShaderHandler,
214
+ SpriteHandler,
215
+ TemplateHandler,
216
+ TextHandler,
217
+ TextureAtlasHandler,
218
+ TextureHandler
219
+ ];
220
+ createOptions.soundManager = new SoundManager();
221
+ createOptions.lightmapper = Lightmapper;
222
+ createOptions.batchManager = BatchManager;
223
+ createOptions.xr = XrManager;
224
+
225
+ this.app = new AppBase(this._canvas);
226
+ this.app.init(createOptions);
227
+
228
+ this.app.setCanvasFillMode(FILLMODE_FILL_WINDOW);
229
+ this.app.setCanvasResolution(RESOLUTION_AUTO);
230
+
231
+ this._pickerCreate();
232
+
233
+ // Get all pc-asset elements that are direct children of the pc-app element
234
+ const assetElements = this.querySelectorAll<AssetElement>(':scope > pc-asset');
235
+ Array.from(assetElements).forEach((assetElement) => {
236
+ assetElement.createAsset();
237
+ const asset = assetElement.asset;
238
+ if (asset) {
239
+ this.app!.assets.add(asset);
240
+ }
241
+ });
242
+
243
+ // Get all pc-material elements that are direct children of the pc-app element
244
+ const materialElements = this.querySelectorAll<MaterialElement>(':scope > pc-material');
245
+ Array.from(materialElements).forEach((materialElement) => {
246
+ materialElement.createMaterial();
247
+ });
248
+
249
+ // Create all entities
250
+ const entityElements = this.querySelectorAll<EntityElement>('pc-entity');
251
+ Array.from(entityElements).forEach((entityElement) => {
252
+ entityElement.createEntity(this.app!);
253
+ });
254
+
255
+ // Build hierarchy
256
+ entityElements.forEach((entityElement) => {
257
+ entityElement.buildHierarchy(this.app!);
258
+ });
259
+
260
+ this._hierarchyReady = true;
261
+
262
+ // Load assets before starting the application
263
+ this.app.preload(() => {
264
+ // Start the application
265
+ this.app!.start();
266
+
267
+ // Handle window resize to keep the canvas responsive
268
+ window.addEventListener('resize', this._onWindowResize);
269
+
270
+ this._onReady();
271
+ });
272
+ }
273
+
274
+ disconnectedCallback() {
275
+ this._pickerDestroy();
276
+
277
+ // Clean up the application
278
+ if (this.app) {
279
+ this.app.destroy();
280
+ this.app = null;
281
+ }
282
+
283
+ // Remove event listeners
284
+ window.removeEventListener('resize', this._onWindowResize);
285
+
286
+ // Remove the canvas
287
+ if (this._canvas && this.contains(this._canvas)) {
288
+ this.removeChild(this._canvas);
289
+ this._canvas = null;
290
+ }
291
+ }
292
+
293
+ _onWindowResize() {
294
+ if (this.app) {
295
+ this.app.resizeCanvas();
296
+ }
297
+ }
298
+
299
+ _pickerCreate() {
300
+ const { width, height } = this.app!.graphicsDevice;
301
+ this._picker = new Picker(this.app!, width, height);
302
+
303
+ // Create bound handlers but don't attach them yet
304
+ this._pointerHandlers.pointermove = this._onPointerMove.bind(this) as EventListener;
305
+ this._pointerHandlers.pointerdown = this._onPointerDown.bind(this) as EventListener;
306
+ this._pointerHandlers.pointerup = this._onPointerUp.bind(this) as EventListener;
307
+
308
+ // Listen for pointer listeners being added/removed
309
+ ['pointermove', 'pointerdown', 'pointerup', 'pointerenter', 'pointerleave'].forEach((type) => {
310
+ this.addEventListener(`${type}:connect`, () => this._onPointerListenerAdded(type));
311
+ this.addEventListener(`${type}:disconnect`, () => this._onPointerListenerRemoved(type));
312
+ });
313
+ }
314
+
315
+ _pickerDestroy() {
316
+ if (this._canvas) {
317
+ Object.entries(this._pointerHandlers).forEach(([type, handler]) => {
318
+ if (handler) {
319
+ this._canvas!.removeEventListener(type, handler);
320
+ }
321
+ });
322
+ }
323
+
324
+ this._picker = null;
325
+ this._pointerHandlers = {
326
+ pointermove: null,
327
+ pointerdown: null,
328
+ pointerup: null
329
+ };
330
+ }
331
+
332
+ // New helper to convert CSS coordinates to canvas (picker) coordinates
333
+ private _getPickerCoordinates(event: PointerEvent): { x: number, y: number } {
334
+ // Get the canvas' bounding rectangle in CSS pixels.
335
+ const canvasRect = this._canvas!.getBoundingClientRect();
336
+ // Compute scale factors based on canvas actual resolution vs. its CSS display size.
337
+ const scaleX = this._canvas!.width / canvasRect.width;
338
+ const scaleY = this._canvas!.height / canvasRect.height;
339
+ // Convert the client coordinates accordingly.
340
+ const x = (event.clientX - canvasRect.left) * scaleX;
341
+ const y = (event.clientY - canvasRect.top) * scaleY;
342
+ return { x, y };
343
+ }
344
+
345
+ _onPointerMove(event: PointerEvent) {
346
+ if (!this._picker || !this.app) return;
347
+
348
+ const camera = this.app!.root.findComponent('camera') as CameraComponent;
349
+ if (!camera) return;
350
+
351
+ // Use the helper to convert event coordinates into canvas/picker coordinates.
352
+ const { x, y } = this._getPickerCoordinates(event);
353
+
354
+ this._picker.prepare(camera, this.app!.scene);
355
+ const selection = this._picker.getSelection(x, y);
356
+
357
+ // Get the currently hovered entity by walking up the hierarchy
358
+ let newHoverEntity: EntityElement | null = null;
359
+ if (selection.length > 0) {
360
+ const item = selection[0];
361
+ let currentNode: GraphNode | null = item instanceof MeshInstance ? item.node : (item as GSplatComponent).entity;
362
+ while (currentNode !== null) {
363
+ const entityElement = this.querySelector(`pc-entity[name="${currentNode.name}"]`) as EntityElement;
364
+ if (entityElement) {
365
+ newHoverEntity = entityElement;
366
+ break;
367
+ }
368
+ currentNode = currentNode.parent;
369
+ }
370
+ }
371
+
372
+ // Handle enter/leave events
373
+ if (this._hoveredEntity !== newHoverEntity) {
374
+ if (this._hoveredEntity && this._hoveredEntity.hasListeners('pointerleave')) {
375
+ this._hoveredEntity.dispatchEvent(new PointerEvent('pointerleave', event));
376
+ }
377
+ if (newHoverEntity && newHoverEntity.hasListeners('pointerenter')) {
378
+ newHoverEntity.dispatchEvent(new PointerEvent('pointerenter', event));
379
+ }
380
+ }
381
+
382
+ // Update hover state
383
+ this._hoveredEntity = newHoverEntity;
384
+
385
+ // Handle pointermove event
386
+ if (newHoverEntity && newHoverEntity.hasListeners('pointermove')) {
387
+ newHoverEntity.dispatchEvent(new PointerEvent('pointermove', event));
388
+ }
389
+ }
390
+
391
+ _onPointerDown(event: PointerEvent) {
392
+ if (!this._picker || !this.app) return;
393
+
394
+ const camera = this.app!.root.findComponent('camera') as CameraComponent;
395
+ if (!camera) return;
396
+
397
+ // Convert the event's pointer coordinates
398
+ const { x, y } = this._getPickerCoordinates(event);
399
+
400
+ this._picker.prepare(camera, this.app!.scene);
401
+ const selection = this._picker.getSelection(x, y);
402
+
403
+ if (selection.length > 0) {
404
+ const item = selection[0];
405
+ let currentNode: GraphNode | null = item instanceof MeshInstance ? item.node : (item as GSplatComponent).entity;
406
+ while (currentNode !== null) {
407
+ const entityElement = this.querySelector(`pc-entity[name="${currentNode.name}"]`) as EntityElement;
408
+ if (entityElement && entityElement.hasListeners('pointerdown')) {
409
+ entityElement.dispatchEvent(new PointerEvent('pointerdown', event));
410
+ break;
411
+ }
412
+ currentNode = currentNode.parent;
413
+ }
414
+ }
415
+ }
416
+
417
+ _onPointerUp(event: PointerEvent) {
418
+ if (!this._picker || !this.app) return;
419
+
420
+ const camera = this.app!.root.findComponent('camera') as CameraComponent;
421
+ if (!camera) return;
422
+
423
+ // Convert CSS coordinates to picker coordinates
424
+ const { x, y } = this._getPickerCoordinates(event);
425
+
426
+ this._picker.prepare(camera, this.app!.scene);
427
+ const selection = this._picker.getSelection(x, y);
428
+
429
+ if (selection.length > 0) {
430
+ const item = selection[0];
431
+ const node = item instanceof MeshInstance ? item.node : (item as GSplatComponent).entity;
432
+ const entityElement = this.querySelector(`pc-entity[name="${node.name}"]`) as EntityElement;
433
+ if (entityElement && entityElement.hasListeners('pointerup')) {
434
+ entityElement.dispatchEvent(new PointerEvent('pointerup', event));
435
+ }
436
+ }
437
+ }
438
+
439
+ _onPointerListenerAdded(type: string) {
440
+ if (!this._hasPointerListeners[type] && this._canvas) {
441
+ this._hasPointerListeners[type] = true;
442
+
443
+ // For enter/leave events, we need the move handler
444
+ const handler = (type === 'pointerenter' || type === 'pointerleave') ?
445
+ this._pointerHandlers.pointermove :
446
+ this._pointerHandlers[type];
447
+
448
+ if (handler) {
449
+ this._canvas.addEventListener(type === 'pointerenter' || type === 'pointerleave' ? 'pointermove' : type, handler);
450
+ }
451
+ }
452
+ }
453
+
454
+ _onPointerListenerRemoved(type: string) {
455
+ const hasListeners = Array.from(this.querySelectorAll<EntityElement>('pc-entity'))
456
+ .some(entity => entity.hasListeners(type));
457
+
458
+ if (!hasListeners && this._canvas) {
459
+ this._hasPointerListeners[type] = false;
460
+
461
+ const handler = (type === 'pointerenter' || type === 'pointerleave') ?
462
+ this._pointerHandlers.pointermove :
463
+ this._pointerHandlers[type];
464
+
465
+ if (handler) {
466
+ this._canvas.removeEventListener(type === 'pointerenter' || type === 'pointerleave' ? 'pointermove' : type, handler);
467
+ }
468
+ }
469
+ }
470
+
471
+ /**
472
+ * Sets the alpha flag.
473
+ * @param value - The alpha flag.
474
+ */
475
+ set alpha(value: boolean) {
476
+ this._alpha = value;
477
+ }
478
+
479
+ /**
480
+ * Gets the alpha flag.
481
+ * @returns The alpha flag.
482
+ */
483
+ get alpha() {
484
+ return this._alpha;
485
+ }
486
+
487
+ /**
488
+ * Sets the antialias flag.
489
+ * @param value - The antialias flag.
490
+ */
491
+ set antialias(value: boolean) {
492
+ this._antialias = value;
493
+ }
494
+
495
+ /**
496
+ * Gets the antialias flag.
497
+ * @returns The antialias flag.
498
+ */
499
+ get antialias() {
500
+ return this._antialias;
501
+ }
502
+
503
+ /**
504
+ * Sets the graphics backend.
505
+ * @param value - The graphics backend ('webgpu', 'webgl2', or 'null').
506
+ */
507
+ set backend(value: 'webgpu' | 'webgl2' | 'null') {
508
+ this._backend = value;
509
+ }
510
+
511
+ /**
512
+ * Gets the graphics backend.
513
+ * @returns The graphics backend.
514
+ */
515
+ get backend() {
516
+ return this._backend;
517
+ }
518
+
519
+ /**
520
+ * Sets the depth flag.
521
+ * @param value - The depth flag.
522
+ */
523
+ set depth(value: boolean) {
524
+ this._depth = value;
525
+ }
526
+
527
+ /**
528
+ * Gets the depth flag.
529
+ * @returns The depth flag.
530
+ */
531
+ get depth() {
532
+ return this._depth;
533
+ }
534
+
535
+ /**
536
+ * Gets the hierarchy ready flag.
537
+ * @returns The hierarchy ready flag.
538
+ * @ignore
539
+ */
540
+ get hierarchyReady() {
541
+ return this._hierarchyReady;
542
+ }
543
+
544
+ /**
545
+ * Sets the high resolution flag. When true, the application will render at the device's
546
+ * physical resolution. When false, the application will render at CSS resolution.
547
+ * @param value - The high resolution flag.
548
+ */
549
+ set highResolution(value: boolean) {
550
+ this._highResolution = value;
551
+ if (this.app) {
552
+ this.app.graphicsDevice.maxPixelRatio = value ? window.devicePixelRatio : 1;
553
+ }
554
+ }
555
+
556
+ /**
557
+ * Gets the high resolution flag.
558
+ * @returns The high resolution flag.
559
+ */
560
+ get highResolution() {
561
+ return this._highResolution;
562
+ }
563
+
564
+ /**
565
+ * Sets the stencil flag.
566
+ * @param value - The stencil flag.
567
+ */
568
+ set stencil(value: boolean) {
569
+ this._stencil = value;
570
+ }
571
+
572
+ /**
573
+ * Gets the stencil flag.
574
+ * @returns The stencil flag.
575
+ */
576
+ get stencil() {
577
+ return this._stencil;
578
+ }
579
+
580
+ static get observedAttributes() {
581
+ return ['alpha', 'antialias', 'backend', 'depth', 'stencil', 'high-resolution'];
582
+ }
583
+
584
+ attributeChangedCallback(name: string, _oldValue: string, newValue: string) {
585
+ switch (name) {
586
+ case 'alpha':
587
+ this.alpha = newValue !== 'false';
588
+ break;
589
+ case 'antialias':
590
+ this.antialias = newValue !== 'false';
591
+ break;
592
+ case 'backend':
593
+ if (newValue === 'webgpu' || newValue === 'webgl2' || newValue === 'null') {
594
+ this.backend = newValue;
595
+ }
596
+ break;
597
+ case 'depth':
598
+ this.depth = newValue !== 'false';
599
+ break;
600
+ case 'high-resolution':
601
+ this.highResolution = newValue !== 'false';
602
+ break;
603
+ case 'stencil':
604
+ this.stencil = newValue !== 'false';
605
+ break;
606
+ }
607
+ }
608
+ }
609
+
610
+ customElements.define('pc-app', AppElement);
611
+
612
+ export { AppElement };