@playcanvas/web-components 0.12.0 → 0.13.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 (85) hide show
  1. package/README.md +1 -1
  2. package/dist/app.d.cts +246 -0
  3. package/dist/app.d.ts +26 -3
  4. package/dist/asset.d.cts +218 -0
  5. package/dist/asset.d.ts +1 -1
  6. package/dist/async-element.d.cts +103 -0
  7. package/dist/async-element.d.ts +6 -4
  8. package/dist/colors.d.cts +1 -0
  9. package/dist/components/button-component.d.cts +186 -0
  10. package/dist/components/button-component.d.ts +1 -1
  11. package/dist/components/camera-component.d.cts +253 -0
  12. package/dist/components/camera-component.d.ts +1 -1
  13. package/dist/components/collision-component.d.cts +73 -0
  14. package/dist/components/collision-component.d.ts +1 -1
  15. package/dist/components/component.d.cts +82 -0
  16. package/dist/components/component.d.ts +1 -1
  17. package/dist/components/element-component.d.cts +316 -0
  18. package/dist/components/element-component.d.ts +1 -1
  19. package/dist/components/gsplat-component.d.cts +108 -0
  20. package/dist/components/gsplat-component.d.ts +1 -1
  21. package/dist/components/layoutchild-component.d.cts +110 -0
  22. package/dist/components/layoutchild-component.d.ts +1 -1
  23. package/dist/components/layoutgroup-component.d.cts +136 -0
  24. package/dist/components/layoutgroup-component.d.ts +1 -1
  25. package/dist/components/light-component.d.cts +264 -0
  26. package/dist/components/light-component.d.ts +1 -1
  27. package/dist/components/listener-component.d.cts +20 -0
  28. package/dist/components/listener-component.d.ts +1 -1
  29. package/dist/components/particlesystem-component.d.cts +52 -0
  30. package/dist/components/particlesystem-component.d.ts +1 -1
  31. package/dist/components/render-component.d.cts +76 -0
  32. package/dist/components/render-component.d.ts +1 -1
  33. package/dist/components/rigidbody-component.d.cts +88 -0
  34. package/dist/components/rigidbody-component.d.ts +1 -1
  35. package/dist/components/screen-component.d.cts +70 -0
  36. package/dist/components/screen-component.d.ts +1 -1
  37. package/dist/components/script-component.d.cts +163 -0
  38. package/dist/components/script-component.d.ts +1 -1
  39. package/dist/components/script.d.cts +94 -0
  40. package/dist/components/script.d.ts +1 -1
  41. package/dist/components/scrollbar-component.d.cts +69 -0
  42. package/dist/components/scrollbar-component.d.ts +1 -1
  43. package/dist/components/scrollview-component.d.cts +178 -0
  44. package/dist/components/scrollview-component.d.ts +1 -1
  45. package/dist/components/sound-component.d.cts +108 -0
  46. package/dist/components/sound-component.d.ts +1 -1
  47. package/dist/components/sound-slot.d.cts +134 -0
  48. package/dist/components/sound-slot.d.ts +2 -2
  49. package/dist/custom-elements.json +79 -21
  50. package/dist/entity-base.d.cts +67 -0
  51. package/dist/entity-base.d.ts +2 -2
  52. package/dist/entity.d.cts +131 -0
  53. package/dist/entity.d.ts +1 -1
  54. package/dist/index.d.cts +79 -0
  55. package/dist/index.d.ts +33 -33
  56. package/dist/loading-bar.d.cts +35 -0
  57. package/dist/material.d.cts +1011 -0
  58. package/dist/model.d.cts +72 -0
  59. package/dist/model.d.ts +1 -1
  60. package/dist/module.d.cts +29 -0
  61. package/dist/module.d.ts +16 -10
  62. package/dist/node.d.cts +253 -0
  63. package/dist/node.d.ts +1 -1
  64. package/dist/parse.d.cts +147 -0
  65. package/dist/pwc.cjs +141 -41
  66. package/dist/pwc.cjs.map +1 -1
  67. package/dist/pwc.js +142 -42
  68. package/dist/pwc.js.map +1 -1
  69. package/dist/pwc.min.js +1 -1
  70. package/dist/pwc.min.js.map +1 -1
  71. package/dist/pwc.min.mjs +1 -1
  72. package/dist/pwc.min.mjs.map +1 -1
  73. package/dist/pwc.mjs +142 -42
  74. package/dist/pwc.mjs.map +1 -1
  75. package/dist/scene.d.cts +117 -0
  76. package/dist/scene.d.ts +1 -1
  77. package/dist/sky.d.cts +121 -0
  78. package/dist/sky.d.ts +1 -1
  79. package/dist/vscode.html-custom-data.json +2 -2
  80. package/dist/web-types.json +41 -5
  81. package/package.json +16 -7
  82. package/src/app.ts +119 -30
  83. package/src/async-element.ts +4 -2
  84. package/src/loading-bar.ts +2 -2
  85. package/src/module.ts +39 -20
package/src/app.ts CHANGED
@@ -1,10 +1,10 @@
1
- import type { CameraComponent, GraphNode, GSplatComponent, Entity } from 'playcanvas';
1
+ import type { CameraComponent, GraphicsDevice, GraphNode, GSplatComponent, Entity } from 'playcanvas';
2
2
  import {
3
3
  AppBase,
4
4
  AppOptions,
5
5
  createGraphicsDevice,
6
6
  ElementInput,
7
- FILLMODE_FILL_WINDOW,
7
+ FILLMODE_NONE,
8
8
  Keyboard,
9
9
  Mouse,
10
10
  Picker,
@@ -76,16 +76,45 @@ import { parseBool, parseEnum, parseNumber } from './parse';
76
76
  /** The pointer event types the application synthesizes on `<pc-entity>` elements via picking. */
77
77
  const pointerEventTypes = ['pointermove', 'pointerdown', 'pointerup', 'pointerenter', 'pointerleave'] as const;
78
78
 
79
+ /**
80
+ * Gives `pc-app` the sizing contract of a replaced element (`<video>`, `<img>`): a block-level
81
+ * box that the page's CSS sizes, defaulting to the canvas's own 300x150 intrinsic size, with the
82
+ * canvas and loading bar anchored to it. `:where()` keeps every declaration at zero specificity,
83
+ * so any page rule - however plain - overrides these defaults.
84
+ */
85
+ const ensureBaseStyles = () => {
86
+ const id = 'pc-app-styles';
87
+ if (document.getElementById(id)) {
88
+ return;
89
+ }
90
+ const style = document.createElement('style');
91
+ style.id = id;
92
+ style.textContent = ':where(pc-app) { display: block; position: relative; width: 300px; height: 150px; }';
93
+ document.head.appendChild(style);
94
+ };
95
+
79
96
  /**
80
97
  * The AppElement interface provides properties and methods for manipulating
81
98
  * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-app/ | `<pc-app>`} elements.
82
99
  * The AppElement interface also inherits the properties and methods of the
83
100
  * {@link HTMLElement} interface.
84
101
  *
102
+ * The element is sized like a replaced element such as `<video>`: a block-level box that the
103
+ * page's CSS controls, 300x150 by default. The application's canvas always fills the element,
104
+ * and the drawing buffer resolution follows the element's size (capped by `max-pixel-ratio`),
105
+ * tracked live via a ResizeObserver — so the element can be embedded at any size, resized by
106
+ * its container, or made fullscreen with ordinary CSS such as `width: 100vw; height: 100dvh`.
107
+ *
85
108
  * @fires {ProgressEvent} progress - Fired while the application preloads its assets. `loaded` and
86
109
  * `total` are asset counts, not bytes, and an asset that fails to load still counts as loaded.
87
110
  * Fired at least once per boot, and the final event always has `loaded` equal to `total`. Does
88
111
  * not bubble.
112
+ *
113
+ * @fires {ErrorEvent} error - Fired when the application cannot boot because no graphics device
114
+ * could be created (for example, a browser with WebGL disabled). `message` names the requested
115
+ * backends and `error` holds the underlying failure. The element never becomes ready
116
+ * and `app` stays `null` — listen for this event to show a fallback UI. Removing the element and
117
+ * re-inserting it retries the boot with its current attributes. Does not bubble.
89
118
  */
90
119
  class AppElement extends AsyncElement {
91
120
  /**
@@ -165,6 +194,13 @@ class AppElement extends AsyncElement {
165
194
 
166
195
  private _loadProgress = 0;
167
196
 
197
+ /**
198
+ * Tracks the element's box so the drawing buffer and picker follow it. Created per boot once
199
+ * the application exists, and disconnected on teardown. `null` where ResizeObserver is
200
+ * unavailable (jsdom), where the boot-time resolution set is the only sizing that happens.
201
+ */
202
+ private _resizeObserver: ResizeObserver | null = null;
203
+
168
204
  /**
169
205
  * The PlayCanvas application instance. `null` until the element is ready, and again once it
170
206
  * has been removed from the document — await {@link whenReady} or the element's `ready()`
@@ -194,9 +230,6 @@ class AppElement extends AsyncElement {
194
230
  constructor() {
195
231
  super();
196
232
 
197
- // Bind methods to maintain 'this' context
198
- this._onWindowResize = this._onWindowResize.bind(this);
199
-
200
233
  // Track pointer listeners being added to and removed from descendant entities.
201
234
  // Registered once here rather than on every boot - the handlers no-op while there is no
202
235
  // canvas, and a re-booted element must not stack a second set.
@@ -209,6 +242,10 @@ class AppElement extends AsyncElement {
209
242
  async connectedCallback() {
210
243
  const generation = ++this._bootGeneration;
211
244
 
245
+ // Installed before the loading bar is created: the bar anchors to this element, which
246
+ // these styles make a positioned block box
247
+ ensureBaseStyles();
248
+
212
249
  // Created before the first await, so the bar is visible while modules and the graphics
213
250
  // device are created, and exists before any disconnect could need to clean it up
214
251
  if (this._loadingBar && !this._bar) {
@@ -227,8 +264,11 @@ class AppElement extends AsyncElement {
227
264
  return;
228
265
  }
229
266
 
230
- // Create and append the canvas to the element
267
+ // Create and append the canvas, filling the element's content box - the page sizes the
268
+ // element, and everything else follows. touch-action: none keeps touch drags driving the
269
+ // engine's input handlers instead of scrolling the page.
231
270
  this._canvas = document.createElement('canvas');
271
+ this._canvas.style.cssText = 'display: block; width: 100%; height: 100%; touch-action: none;';
232
272
  this.appendChild(this._canvas);
233
273
 
234
274
  // Configure device types based on backend selection
@@ -241,14 +281,46 @@ class AppElement extends AsyncElement {
241
281
 
242
282
  this._optionsLocked = true;
243
283
 
244
- const device = await createGraphicsDevice(this._canvas, {
245
- // @ts-ignore - alpha needs to be documented
246
- alpha: this._alpha,
247
- antialias: this._antialias,
248
- depth: this._depthBuffer,
249
- deviceTypes: deviceTypes,
250
- stencil: this._stencilBuffer
251
- });
284
+ // createGraphicsDevice appends its final null-device fallback to the array in place, so
285
+ // the requested list is captured now for the failure message.
286
+ const requested = deviceTypes.join(', ');
287
+
288
+ let device: GraphicsDevice;
289
+ try {
290
+ device = await createGraphicsDevice(this._canvas, {
291
+ // @ts-ignore - alpha needs to be documented
292
+ alpha: this._alpha,
293
+ antialias: this._antialias,
294
+ depth: this._depthBuffer,
295
+ deviceTypes: deviceTypes,
296
+ stencil: this._stencilBuffer
297
+ });
298
+ } catch (error) {
299
+ // The element may have been removed while device creation was failing. The teardown
300
+ // has already cleaned up, and the failure belongs to a boot that no longer owns the
301
+ // element.
302
+ if (generation !== this._bootGeneration) {
303
+ return;
304
+ }
305
+
306
+ // Return the element to its pre-boot state - no dead canvas, no loading bar stuck at
307
+ // zero - before announcing the failure. Readiness deliberately stays pending: nothing
308
+ // it would announce (the app, the entity hierarchy) exists, so a device-less element
309
+ // joins the documented never-ready cases and the failure surfaces through the error
310
+ // event instead.
311
+ if (this._canvas && this.contains(this._canvas)) {
312
+ this.removeChild(this._canvas);
313
+ }
314
+ this._canvas = null;
315
+ this._bar?.destroy();
316
+ this._bar = null;
317
+
318
+ const reason = error instanceof Error ? error.message : String(error);
319
+ const message = `pc-app failed to create a graphics device (${requested}) - ${reason}`;
320
+ console.error(message, error);
321
+ this.dispatchEvent(new ErrorEvent('error', { message, error }));
322
+ return;
323
+ }
252
324
 
253
325
  // The element may have been removed while the device was created. disconnectedCallback
254
326
  // has already cleaned up the canvas; the device was created inside the await, so it is
@@ -332,11 +404,22 @@ class AppElement extends AsyncElement {
332
404
  this._app = app;
333
405
  app.init(createOptions);
334
406
 
335
- app.setCanvasFillMode(FILLMODE_FILL_WINDOW);
407
+ // FILLMODE_NONE leaves the canvas's CSS sizing alone (the engine's other fill modes
408
+ // stamp window-derived pixel sizes onto it); RESOLUTION_AUTO sizes the drawing buffer
409
+ // from the canvas's client size
410
+ app.setCanvasFillMode(FILLMODE_NONE);
336
411
  app.setCanvasResolution(RESOLUTION_AUTO);
337
412
 
338
413
  this._pickerCreate();
339
414
 
415
+ // Track the element's box rather than the window: containers resize without any window
416
+ // event (splitter drags, flex reflow, animations). Guarded because jsdom has no
417
+ // ResizeObserver - there, the resolution set above is the only sizing that happens.
418
+ if (typeof ResizeObserver !== 'undefined') {
419
+ this._resizeObserver = new ResizeObserver(() => this._syncCanvasSize());
420
+ this._resizeObserver.observe(this);
421
+ }
422
+
340
423
  // Get all pc-asset elements that are direct children of the pc-app element
341
424
  const assetElements = this.querySelectorAll<AssetElement>(':scope > pc-asset');
342
425
  for (const assetElement of Array.from(assetElements)) {
@@ -426,9 +509,6 @@ class AppElement extends AsyncElement {
426
509
  // first rAF tick
427
510
  app.once('frameend', () => this._bar?.complete());
428
511
 
429
- // Handle window resize to keep the canvas responsive
430
- window.addEventListener('resize', this._onWindowResize);
431
-
432
512
  this._onReady();
433
513
  });
434
514
  }
@@ -458,8 +538,9 @@ class AppElement extends AsyncElement {
458
538
  this._hierarchyReady = false;
459
539
  this._resetReady();
460
540
 
461
- // Remove event listeners
462
- window.removeEventListener('resize', this._onWindowResize);
541
+ // Stop tracking the element's size
542
+ this._resizeObserver?.disconnect();
543
+ this._resizeObserver = null;
463
544
 
464
545
  // Remove the canvas
465
546
  if (this._canvas && this.contains(this._canvas)) {
@@ -468,10 +549,18 @@ class AppElement extends AsyncElement {
468
549
  }
469
550
  }
470
551
 
471
- private _onWindowResize() {
472
- if (this.app) {
473
- this.app.resizeCanvas();
552
+ /**
553
+ * Syncs the drawing buffer and the picker to the canvas's current CSS size. The picker must
554
+ * track the buffer, or picks would land at stale coordinates after a resize. Skipped while
555
+ * an XR session presents - the session owns the buffer size.
556
+ */
557
+ private _syncCanvasSize() {
558
+ if (!this.app || this.app.xr?.active) {
559
+ return;
474
560
  }
561
+ this.app.updateCanvasSize();
562
+ const { width, height } = this.app.graphicsDevice;
563
+ this._picker?.resize(width, height);
475
564
  }
476
565
 
477
566
  private _pickerCreate() {
@@ -495,9 +584,9 @@ class AppElement extends AsyncElement {
495
584
  // created from onpointer* attributes when their elements were first upgraded, or
496
585
  // listeners carried over from before a re-boot)
497
586
  pointerEventTypes.forEach((type) => {
498
- const anyListeners = Array.from(
499
- this.querySelectorAll<EntityBaseElement>('pc-entity, pc-node')
500
- ).some((entity) => entity._hasListeners(type));
587
+ const anyListeners = Array.from(this.querySelectorAll<EntityBaseElement>('pc-entity, pc-node')).some(
588
+ (entity) => entity._hasListeners(type)
589
+ );
501
590
  if (anyListeners) {
502
591
  this._onPointerListenerAdded(type);
503
592
  }
@@ -718,9 +807,9 @@ class AppElement extends AsyncElement {
718
807
  }
719
808
 
720
809
  private _onPointerListenerRemoved(type: string) {
721
- const hasListeners = Array.from(
722
- this.querySelectorAll<EntityBaseElement>('pc-entity, pc-node')
723
- ).some((entity) => entity._hasListeners(type));
810
+ const hasListeners = Array.from(this.querySelectorAll<EntityBaseElement>('pc-entity, pc-node')).some((entity) =>
811
+ entity._hasListeners(type)
812
+ );
724
813
 
725
814
  if (!hasListeners && this._canvas) {
726
815
  this._hasPointerListeners[type] = false;
@@ -862,7 +951,7 @@ class AppElement extends AsyncElement {
862
951
  this._maxPixelRatio = value;
863
952
  if (this.app) {
864
953
  this.app.graphicsDevice.maxPixelRatio = value;
865
- this.app.resizeCanvas();
954
+ this._syncCanvasSize();
866
955
  }
867
956
  }
868
957
 
@@ -96,7 +96,8 @@ type AsyncElementTagName = {
96
96
  /**
97
97
  * Waits for the first element matching the given tag name to be fully initialized. Note that the
98
98
  * promise never settles if the element cannot finish initializing (for example, a `<pc-script>`
99
- * that is not a direct child of `<pc-scripts>`). A component element outside a `<pc-entity>` is
99
+ * that is not a direct child of `<pc-scripts>`, or a `<pc-app>` that could not create a graphics
100
+ * device — listen for its `error` event instead). A component element outside a `<pc-entity>` is
100
101
  * the exception: it still becomes ready, but its `component` is `null`. Either way, a misplaced
101
102
  * element logs a warning naming the parent it requires.
102
103
  * @param target - The tag name of the element to wait for (e.g. `'pc-app'`).
@@ -120,7 +121,8 @@ function whenReady<T extends AsyncElement>(target: T): Promise<T>;
120
121
  /**
121
122
  * Waits for the first element matching the given CSS selector to be fully initialized. Note that
122
123
  * the promise never settles if the element cannot finish initializing (for example, a `<pc-script>`
123
- * that is not a direct child of `<pc-scripts>`). A component element outside a `<pc-entity>` is
124
+ * that is not a direct child of `<pc-scripts>`, or a `<pc-app>` that could not create a graphics
125
+ * device — listen for its `error` event instead). A component element outside a `<pc-entity>` is
124
126
  * the exception: it still becomes ready, but its `component` is `null`. Either way, a misplaced
125
127
  * element logs a warning naming the parent it requires.
126
128
  * @param target - A CSS selector matching the element to wait for (e.g. `'#my-app'`).
@@ -28,9 +28,9 @@ class LoadingBar {
28
28
  this._track.setAttribute('aria-label', 'Loading');
29
29
  this._track.setAttribute('aria-valuemin', '0');
30
30
  this._track.setAttribute('aria-valuemax', '100');
31
- // Fixed positioning matches the canvas, which always fills the window (FILLMODE_FILL_WINDOW)
31
+ // Anchored to the pc-app element, which the library's base styles make a positioned box
32
32
  this._track.style.cssText = [
33
- 'position: fixed',
33
+ 'position: absolute',
34
34
  'top: 0',
35
35
  'left: 0',
36
36
  'width: 100%',
package/src/module.ts CHANGED
@@ -1,13 +1,21 @@
1
1
  import { basisInitialize, WasmModule } from 'playcanvas';
2
2
 
3
+ import { AsyncElement } from './async-element';
4
+
3
5
  /**
4
6
  * The ModuleElement interface provides properties and methods for manipulating
5
- * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-module/ | `<pc-module>`} elements.
6
- * The ModuleElement interface also inherits the properties and methods of the
7
- * {@link HTMLElement} interface.
7
+ * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-module/ | `<pc-module>`}
8
+ * elements. The ModuleElement interface also inherits the properties and methods of the
9
+ * {@link AsyncElement} interface.
10
+ *
11
+ * The attributes are read once, when the module starts loading - on the element's first
12
+ * connection, or earlier if a containing `<pc-app>` boots first and collects it - so changing
13
+ * them later has no effect. The element becomes ready once the module has loaded. WebAssembly
14
+ * modules configure engine-global state that never unloads, so readiness is not re-armed by
15
+ * removing the element, and a re-inserted element does not load again.
8
16
  *
9
- * Note that these attributes are read once when the element is created, so changing them later
10
- * has no effect.
17
+ * A `<pc-module>` without a `name` warns and never becomes ready; a containing `<pc-app>` still
18
+ * boots.
11
19
  *
12
20
  * @attribute {string} name - The name of the WebAssembly module to configure, e.g. `Basis` or
13
21
  * `Ammo`.
@@ -16,21 +24,25 @@ import { basisInitialize, WasmModule } from 'playcanvas';
16
24
  * @attribute {string} fallback - The URL of the module's asm.js fallback script, used when
17
25
  * WebAssembly is unavailable.
18
26
  */
19
- class ModuleElement extends HTMLElement {
20
- private loadPromise: Promise<void>;
27
+ class ModuleElement extends AsyncElement {
28
+ private _loadPromise: Promise<void> | null = null;
21
29
 
22
- /** @ignore */
23
- constructor() {
24
- super();
25
- this.loadPromise = this.loadModule();
30
+ connectedCallback() {
31
+ this._getLoadPromise();
26
32
  }
27
33
 
28
- private async loadModule(): Promise<void> {
29
- const name = this.getAttribute('name')!;
30
- const glueUrl = this.getAttribute('glue')!;
31
- const wasmUrl = this.getAttribute('wasm')!;
32
- const fallbackUrl = this.getAttribute('fallback')!;
33
- const config = { glueUrl, wasmUrl, fallbackUrl };
34
+ private async _loadModule(): Promise<void> {
35
+ const name = this.getAttribute('name');
36
+ if (!name) {
37
+ console.warn("pc-module requires a 'name' attribute - no module was configured");
38
+ return;
39
+ }
40
+
41
+ const config = {
42
+ glueUrl: this.getAttribute('glue') ?? undefined,
43
+ wasmUrl: this.getAttribute('wasm') ?? undefined,
44
+ fallbackUrl: this.getAttribute('fallback') ?? undefined
45
+ };
34
46
 
35
47
  if (name === 'Basis') {
36
48
  basisInitialize(config);
@@ -41,17 +53,24 @@ class ModuleElement extends HTMLElement {
41
53
  WasmModule.getInstance(name, () => resolve());
42
54
  });
43
55
  }
56
+
57
+ this._onReady();
44
58
  }
45
59
 
46
60
  /**
47
- * Returns the promise that settles when the module has loaded. Awaited by the containing
48
- * `<pc-app>` element before it creates its graphics device.
61
+ * Returns the promise that settles when the module has loaded, starting the load if it has
62
+ * not already started - a containing `<pc-app>` boots in document order, so it may collect
63
+ * this element before the element's own connectedCallback has run. A missing `name` resolves
64
+ * the promise without configuring anything, so a misconfigured module never blocks the app.
49
65
  *
50
66
  * @returns The load promise.
51
67
  * @internal
52
68
  */
53
69
  _getLoadPromise(): Promise<void> {
54
- return this.loadPromise;
70
+ if (!this._loadPromise) {
71
+ this._loadPromise = this._loadModule();
72
+ }
73
+ return this._loadPromise;
55
74
  }
56
75
  }
57
76