@playcanvas/web-components 0.8.1 → 0.9.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 (71) hide show
  1. package/dist/app.d.ts +10 -2
  2. package/dist/asset.d.ts +18 -3
  3. package/dist/async-element.d.ts +51 -4
  4. package/dist/components/button-component.d.ts +10 -4
  5. package/dist/components/camera-component.d.ts +9 -4
  6. package/dist/components/collision-component.d.ts +9 -4
  7. package/dist/components/component.d.ts +7 -1
  8. package/dist/components/element-component.d.ts +17 -12
  9. package/dist/components/gsplat-component.d.ts +6 -1
  10. package/dist/components/layoutchild-component.d.ts +6 -1
  11. package/dist/components/layoutgroup-component.d.ts +21 -15
  12. package/dist/components/light-component.d.ts +12 -6
  13. package/dist/components/listener-component.d.ts +6 -1
  14. package/dist/components/particlesystem-component.d.ts +6 -1
  15. package/dist/components/render-component.d.ts +13 -4
  16. package/dist/components/rigidbody-component.d.ts +9 -4
  17. package/dist/components/screen-component.d.ts +6 -1
  18. package/dist/components/script-component.d.ts +116 -16
  19. package/dist/components/script.d.ts +72 -8
  20. package/dist/components/scrollbar-component.d.ts +10 -4
  21. package/dist/components/scrollview-component.d.ts +17 -10
  22. package/dist/components/sound-component.d.ts +6 -1
  23. package/dist/components/sound-slot.d.ts +8 -3
  24. package/dist/entity.d.ts +26 -2
  25. package/dist/index.d.ts +3 -2
  26. package/dist/material.d.ts +10 -0
  27. package/dist/model.d.ts +5 -0
  28. package/dist/module.d.ts +5 -0
  29. package/dist/pwc.cjs +1574 -853
  30. package/dist/pwc.cjs.map +1 -1
  31. package/dist/pwc.js +1574 -853
  32. package/dist/pwc.js.map +1 -1
  33. package/dist/pwc.min.js +1 -1
  34. package/dist/pwc.min.js.map +1 -1
  35. package/dist/pwc.mjs +1575 -855
  36. package/dist/pwc.mjs.map +1 -1
  37. package/dist/scene.d.ts +14 -5
  38. package/dist/sky.d.ts +6 -0
  39. package/dist/utils.d.ts +81 -23
  40. package/package.json +23 -13
  41. package/src/app.ts +38 -12
  42. package/src/asset.ts +61 -8
  43. package/src/async-element.ts +86 -5
  44. package/src/components/button-component.ts +26 -19
  45. package/src/components/camera-component.ts +29 -23
  46. package/src/components/collision-component.ts +19 -13
  47. package/src/components/component.ts +32 -13
  48. package/src/components/element-component.ts +58 -52
  49. package/src/components/gsplat-component.ts +14 -7
  50. package/src/components/layoutchild-component.ts +16 -9
  51. package/src/components/layoutgroup-component.ts +38 -31
  52. package/src/components/light-component.ts +33 -31
  53. package/src/components/listener-component.ts +8 -2
  54. package/src/components/particlesystem-component.ts +8 -2
  55. package/src/components/render-component.ts +19 -8
  56. package/src/components/rigidbody-component.ts +21 -15
  57. package/src/components/screen-component.ts +15 -9
  58. package/src/components/script-component.ts +512 -125
  59. package/src/components/script.ts +116 -16
  60. package/src/components/scrollbar-component.ts +19 -12
  61. package/src/components/scrollview-component.ts +37 -29
  62. package/src/components/sound-component.ts +16 -9
  63. package/src/components/sound-slot.ts +23 -14
  64. package/src/entity.ts +61 -71
  65. package/src/index.ts +5 -2
  66. package/src/material.ts +34 -1
  67. package/src/model.ts +6 -0
  68. package/src/module.ts +6 -0
  69. package/src/scene.ts +25 -12
  70. package/src/sky.ts +34 -16
  71. package/src/utils.ts +180 -40
package/dist/scene.d.ts CHANGED
@@ -31,22 +31,26 @@ declare class SceneElement extends AsyncElement {
31
31
  * The gravity of the scene.
32
32
  */
33
33
  private _gravity;
34
+ private _scene;
34
35
  /**
35
- * The PlayCanvas scene instance.
36
+ * The PlayCanvas scene instance. Available once the element is ready — await
37
+ * {@link whenReady} or the element's `ready()` promise before accessing it.
38
+ * @returns The scene instance.
36
39
  */
37
- scene: Scene | null;
40
+ get scene(): Scene;
38
41
  connectedCallback(): Promise<void>;
39
42
  updateSceneSettings(): void;
40
43
  /**
41
- * Sets the fog type of the scene.
44
+ * Sets the fog type of the scene. Can be `none`, `linear`, `exp` or `exp2`. Defaults to
45
+ * `none`.
42
46
  * @param value - The fog type.
43
47
  */
44
- set fog(value: string);
48
+ set fog(value: "linear" | "none" | "exp" | "exp2");
45
49
  /**
46
50
  * Gets the fog type of the scene.
47
51
  * @returns The fog type.
48
52
  */
49
- get fog(): string;
53
+ get fog(): "linear" | "none" | "exp" | "exp2";
50
54
  /**
51
55
  * Sets the fog color of the scene.
52
56
  * @param value - The fog color.
@@ -100,4 +104,9 @@ declare class SceneElement extends AsyncElement {
100
104
  static get observedAttributes(): string[];
101
105
  attributeChangedCallback(name: string, _oldValue: string, newValue: string): void;
102
106
  }
107
+ declare global {
108
+ interface HTMLElementTagNameMap {
109
+ 'pc-scene': SceneElement;
110
+ }
111
+ }
103
112
  export { SceneElement };
package/dist/sky.d.ts CHANGED
@@ -15,6 +15,7 @@ declare class SkyElement extends AsyncElement {
15
15
  private _scale;
16
16
  private _type;
17
17
  private _scene;
18
+ private _appElement;
18
19
  connectedCallback(): void;
19
20
  disconnectedCallback(): void;
20
21
  private _generateSkybox;
@@ -103,4 +104,9 @@ declare class SkyElement extends AsyncElement {
103
104
  static get observedAttributes(): string[];
104
105
  attributeChangedCallback(name: string, _oldValue: string, newValue: string): void;
105
106
  }
107
+ declare global {
108
+ interface HTMLElementTagNameMap {
109
+ 'pc-sky': SkyElement;
110
+ }
111
+ }
106
112
  export { SkyElement };
package/dist/utils.d.ts CHANGED
@@ -1,50 +1,108 @@
1
1
  import { Color, Entity, Quat, Vec2, Vec3, Vec4 } from 'playcanvas';
2
2
  /**
3
- * Parse a color string into a Color object. String can be in the format of '#rgb', '#rgba',
4
- * '#rrggbb', '#rrggbbaa', or a string of 3 or 4 comma-delimited numbers.
3
+ * Parse a boolean attribute value. The same rules apply to every boolean attribute:
5
4
  *
6
- * @param value - The color string to parse.
5
+ * - Attribute absent (or removed): the supplied default is used.
6
+ * - Attribute set to the string 'false': `false`.
7
+ * - Attribute present with any other value, including the empty string of a bare boolean
8
+ * attribute (e.g. `<pc-light cast-shadows>`): `true`.
9
+ *
10
+ * @param value - The attribute value to parse (`null` when the attribute is absent).
11
+ * @param defaultValue - The value to use when the attribute is absent or removed.
12
+ * @returns The parsed boolean.
13
+ */
14
+ export declare const parseBool: (value: string | null, defaultValue: boolean) => boolean;
15
+ /**
16
+ * Splits an attribute value into exactly `count` numeric components. Returns `null` when the
17
+ * value does not consist of exactly `count` whitespace-separated finite numbers.
18
+ *
19
+ * @param value - The value to split.
20
+ * @param count - The required number of components.
21
+ * @returns The parsed components, or `null`.
22
+ * @ignore
23
+ */
24
+ export declare const parseComponents: (value: string, count: number) => number[] | null;
25
+ /**
26
+ * Parse a color attribute value. The expected format is a CSS color name (e.g. 'rebeccapurple'),
27
+ * a hex color (e.g. '#ff0000' or '#f00'), or 3 or 4 space-separated numbers in the range 0 to 1
28
+ * (e.g. '1 0.5 0.5' or '1 0.5 0.5 0.5'). Returns `defaultValue` (cloned, when it is a color)
29
+ * when the attribute is absent (`null`), or when the value is malformed — the latter also logs
30
+ * a warning.
31
+ *
32
+ * @param value - The attribute value to parse (`null` when the attribute is absent).
33
+ * @param defaultValue - The value to use when the attribute is absent or invalid.
34
+ * @param attribute - The attribute name, used in the warning message.
7
35
  * @returns The parsed Color object.
8
36
  */
9
- export declare const parseColor: (value: string) => Color;
37
+ export declare const parseColor: <T extends Color | null>(value: string | null, defaultValue: T, attribute: string) => Color | T;
10
38
  /**
11
- * Parse an Euler angles string into a Quat object. String can be in the format of 'x,y,z'.
39
+ * Parse an Euler-angles attribute value into a quaternion. The expected format is 3
40
+ * space-separated angles in degrees (e.g. '0 90 0'). Returns `defaultValue` (cloned, when it is
41
+ * a quaternion) when the attribute is absent (`null`), or when the value is malformed — the
42
+ * latter also logs a warning.
12
43
  *
13
- * @param value - The Euler angles string to parse.
44
+ * @param value - The attribute value to parse (`null` when the attribute is absent).
45
+ * @param defaultValue - The value to use when the attribute is absent or invalid.
46
+ * @param attribute - The attribute name, used in the warning message.
14
47
  * @returns The parsed Quat object.
15
48
  */
16
- export declare const parseQuat: (value: string) => Quat;
49
+ export declare const parseQuat: <T extends Quat | null>(value: string | null, defaultValue: T, attribute: string) => Quat | T;
17
50
  /**
18
- * Parse a Vec2 string into a Vec2 object. String can be in the format of 'x,y'.
51
+ * Parse a Vec2 attribute value. The expected format is 2 space-separated numbers (e.g. '1 2').
52
+ * Returns `defaultValue` (cloned, when it is a vector) when the attribute is absent (`null`),
53
+ * or when the value is malformed — the latter also logs a warning.
19
54
  *
20
- * @param value - The Vec2 string to parse.
55
+ * @param value - The attribute value to parse (`null` when the attribute is absent).
56
+ * @param defaultValue - The value to use when the attribute is absent or invalid.
57
+ * @param attribute - The attribute name, used in the warning message.
21
58
  * @returns The parsed Vec2 object.
22
59
  */
23
- export declare const parseVec2: (value: string) => Vec2;
60
+ export declare const parseVec2: <T extends Vec2 | null>(value: string | null, defaultValue: T, attribute: string) => Vec2 | T;
24
61
  /**
25
- * Parse a Vec3 string into a Vec3 object. String can be in the format of 'x,y,z'.
62
+ * Parse a Vec3 attribute value. The expected format is 3 space-separated numbers (e.g. '1 2 3').
63
+ * Returns `defaultValue` (cloned, when it is a vector) when the attribute is absent (`null`),
64
+ * or when the value is malformed — the latter also logs a warning.
26
65
  *
27
- * @param value - The Vec3 string to parse.
66
+ * @param value - The attribute value to parse (`null` when the attribute is absent).
67
+ * @param defaultValue - The value to use when the attribute is absent or invalid.
68
+ * @param attribute - The attribute name, used in the warning message.
28
69
  * @returns The parsed Vec3 object.
29
70
  */
30
- export declare const parseVec3: (value: string) => Vec3;
71
+ export declare const parseVec3: <T extends Vec3 | null>(value: string | null, defaultValue: T, attribute: string) => Vec3 | T;
31
72
  /**
32
- * Parse a Vec4 string into a Vec4 object. String can be in the format of 'x,y,z,w'.
73
+ * Parse a Vec4 attribute value. The expected format is 4 space-separated numbers
74
+ * (e.g. '1 2 3 4'). Returns `defaultValue` (cloned, when it is a vector) when the attribute is
75
+ * absent (`null`), or when the value is malformed — the latter also logs a warning.
33
76
  *
34
- * @param value - The Vec4 string to parse.
77
+ * @param value - The attribute value to parse (`null` when the attribute is absent).
78
+ * @param defaultValue - The value to use when the attribute is absent or invalid.
79
+ * @param attribute - The attribute name, used in the warning message.
35
80
  * @returns The parsed Vec4 object.
36
81
  */
37
- export declare const parseVec4: (value: string) => Vec4;
82
+ export declare const parseVec4: <T extends Vec4 | null>(value: string | null, defaultValue: T, attribute: string) => Vec4 | T;
83
+ /**
84
+ * Resolves an enum attribute value against its set of valid names. Returns the value when it is
85
+ * one of the valid names. Returns `defaultValue` when the attribute is absent (`null`), or when
86
+ * the value is invalid — the latter also logs a warning listing the valid names.
87
+ *
88
+ * @param value - The attribute value to parse (`null` when the attribute is absent).
89
+ * @param valid - The valid names: an array, or a map whose keys are the valid names.
90
+ * @param defaultValue - The value to use when the attribute is absent or invalid.
91
+ * @param attribute - The attribute name, used in the warning message.
92
+ * @returns The resolved enum name.
93
+ */
94
+ export declare const parseEnum: <T extends string>(value: string | null, valid: readonly T[] | ReadonlyMap<T, number>, defaultValue: T, attribute: string) => T;
38
95
  /**
39
- * Resolves an enum value supplied as either a named string (looked up in `map`) or a numeric
40
- * string. Falls back to `defaultValue` when the value is neither a known name nor a finite number.
96
+ * Parses a number attribute value. Returns the parsed number when the value is a finite number.
97
+ * Returns `defaultValue` when the attribute is absent (`null`), or when the value is not a
98
+ * finite number — the latter also logs a warning.
41
99
  *
42
- * @param value - The attribute value to parse.
43
- * @param map - A map of named values to their numeric enum equivalents.
44
- * @param defaultValue - The value to return when parsing fails.
45
- * @returns The resolved numeric enum value.
100
+ * @param value - The attribute value to parse (`null` when the attribute is absent).
101
+ * @param defaultValue - The value to use when the attribute is absent or invalid.
102
+ * @param attribute - The attribute name, used in the warning message.
103
+ * @returns The parsed number.
46
104
  */
47
- export declare const parseEnum: (value: string, map: Map<string, number>, defaultValue: number) => number;
105
+ export declare const parseNumber: <T extends number | null>(value: string | null, defaultValue: T, attribute: string) => number | T;
48
106
  /**
49
107
  * Resolves a reference string to the {@link Entity} backing a `<pc-entity>` element. The reference
50
108
  * can be a CSS selector (e.g. `#my-id`, `pc-entity[name="Foo"]`), a bare element id, or a bare
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcanvas/web-components",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "author": "PlayCanvas <support@playcanvas.com>",
5
5
  "homepage": "https://playcanvas.com",
6
6
  "description": "Web Components for the PlayCanvas Engine",
@@ -27,12 +27,22 @@
27
27
  "module": "dist/pwc.mjs",
28
28
  "browser": "dist/pwc.js",
29
29
  "types": "dist/index.d.ts",
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/index.d.ts",
33
+ "import": "./dist/pwc.mjs",
34
+ "require": "./dist/pwc.cjs"
35
+ },
36
+ "./dist/*": "./dist/*",
37
+ "./package.json": "./package.json"
38
+ },
30
39
  "type": "module",
31
40
  "files": [
32
41
  "dist",
33
42
  "src"
34
43
  ],
35
44
  "scripts": {
45
+ "prebuild": "node -e \"fs.rmSync('dist', { recursive: true, force: true })\"",
36
46
  "build": "rollup -c",
37
47
  "dev": "concurrently \"npm run watch\" \"npm run serve\"",
38
48
  "docs": "typedoc",
@@ -48,28 +58,28 @@
48
58
  "playcanvas": "^2.20.1"
49
59
  },
50
60
  "devDependencies": {
51
- "@mediapipe/tasks-vision": "0.10.35",
61
+ "@mediapipe/tasks-vision": "1.0.0",
52
62
  "@playcanvas/eslint-config": "2.1.0",
53
63
  "@rollup/plugin-commonjs": "29.0.3",
54
64
  "@rollup/plugin-node-resolve": "16.0.3",
55
65
  "@rollup/plugin-terser": "1.0.0",
56
66
  "@rollup/plugin-typescript": "12.3.0",
57
67
  "@tweenjs/tween.js": "25.0.0",
58
- "@typescript-eslint/eslint-plugin": "8.62.1",
59
- "@typescript-eslint/parser": "8.62.1",
60
- "concurrently": "10.0.3",
61
- "earcut": "3.2.2",
62
- "eslint": "9.39.4",
68
+ "@typescript-eslint/eslint-plugin": "8.65.0",
69
+ "@typescript-eslint/parser": "8.65.0",
70
+ "concurrently": "10.0.4",
71
+ "earcut": "3.2.3",
72
+ "eslint": "9.39.5",
63
73
  "eslint-import-resolver-typescript": "4.4.5",
64
- "globals": "17.7.0",
65
- "mediabunny": "1.50.3",
74
+ "globals": "17.8.0",
75
+ "mediabunny": "1.51.0",
66
76
  "opentype.js": "2.0.0",
67
- "playcanvas": "2.20.4",
68
- "publint": "0.3.21",
69
- "rollup": "4.62.2",
77
+ "playcanvas": "2.21.3",
78
+ "publint": "0.3.22",
79
+ "rollup": "4.62.3",
70
80
  "serve": "14.2.6",
71
81
  "tslib": "2.8.1",
72
- "typedoc": "0.28.19",
82
+ "typedoc": "0.28.20",
73
83
  "typedoc-plugin-mdn-links": "5.1.1",
74
84
  "typescript": "6.0.3"
75
85
  }
package/src/app.ts CHANGED
@@ -71,6 +71,7 @@ import { AsyncElement } from './async-element';
71
71
  import { EntityElement } from './entity';
72
72
  import { MaterialElement } from './material';
73
73
  import { ModuleElement } from './module';
74
+ import { parseBool, parseEnum } from './utils';
74
75
 
75
76
  /**
76
77
  * The AppElement interface provides properties and methods for manipulating
@@ -116,10 +117,16 @@ class AppElement extends AsyncElement {
116
117
  pointerup: null
117
118
  };
118
119
 
120
+ private _app: AppBase | null = null;
121
+
119
122
  /**
120
- * The PlayCanvas application instance.
123
+ * The PlayCanvas application instance. Available once the element is ready — await
124
+ * {@link whenReady} or the element's `ready()` promise before accessing it.
125
+ * @returns The application instance.
121
126
  */
122
- app: AppBase | null = null;
127
+ get app(): AppBase {
128
+ return this._app!;
129
+ }
123
130
 
124
131
  /**
125
132
  * Creates a new AppElement instance.
@@ -227,7 +234,7 @@ class AppElement extends AsyncElement {
227
234
  createOptions.batchManager = BatchManager;
228
235
  createOptions.xr = XrManager;
229
236
 
230
- this.app = new AppBase(this._canvas);
237
+ this._app = new AppBase(this._canvas);
231
238
  this.app.init(createOptions);
232
239
 
233
240
  this.app.setCanvasFillMode(FILLMODE_FILL_WINDOW);
@@ -282,7 +289,7 @@ class AppElement extends AsyncElement {
282
289
  // Clean up the application
283
290
  if (this.app) {
284
291
  this.app.destroy();
285
- this.app = null;
292
+ this._app = null;
286
293
  }
287
294
 
288
295
  // Remove event listeners
@@ -314,6 +321,14 @@ class AppElement extends AsyncElement {
314
321
  ['pointermove', 'pointerdown', 'pointerup', 'pointerenter', 'pointerleave'].forEach((type) => {
315
322
  this.addEventListener(`${type}:connect`, () => this._onPointerListenerAdded(type));
316
323
  this.addEventListener(`${type}:disconnect`, () => this._onPointerListenerRemoved(type));
324
+
325
+ // Attach canvas handlers for listeners registered before this point (e.g. handlers
326
+ // created from onpointer* attributes when their elements were first upgraded)
327
+ const anyListeners = Array.from(this.querySelectorAll<EntityElement>('pc-entity'))
328
+ .some(entity => entity.hasListeners(type));
329
+ if (anyListeners) {
330
+ this._onPointerListenerAdded(type);
331
+ }
317
332
  });
318
333
  }
319
334
 
@@ -332,6 +347,13 @@ class AppElement extends AsyncElement {
332
347
  pointerdown: null,
333
348
  pointerup: null
334
349
  };
350
+ this._hasPointerListeners = {
351
+ pointerenter: false,
352
+ pointerleave: false,
353
+ pointerdown: false,
354
+ pointerup: false,
355
+ pointermove: false
356
+ };
335
357
  }
336
358
 
337
359
  // New helper to convert CSS coordinates to canvas (picker) coordinates
@@ -589,24 +611,22 @@ class AppElement extends AsyncElement {
589
611
  attributeChangedCallback(name: string, _oldValue: string, newValue: string) {
590
612
  switch (name) {
591
613
  case 'alpha':
592
- this.alpha = newValue !== 'false';
614
+ this.alpha = parseBool(newValue, true);
593
615
  break;
594
616
  case 'antialias':
595
- this.antialias = newValue !== 'false';
617
+ this.antialias = parseBool(newValue, true);
596
618
  break;
597
619
  case 'backend':
598
- if (newValue === 'webgpu' || newValue === 'webgl2' || newValue === 'null') {
599
- this.backend = newValue;
600
- }
620
+ this.backend = parseEnum(newValue, ['webgpu', 'webgl2', 'null'], 'webgl2', name);
601
621
  break;
602
622
  case 'depth':
603
- this.depth = newValue !== 'false';
623
+ this.depth = parseBool(newValue, true);
604
624
  break;
605
625
  case 'high-resolution':
606
- this.highResolution = newValue !== 'false';
626
+ this.highResolution = parseBool(newValue, true);
607
627
  break;
608
628
  case 'stencil':
609
- this.stencil = newValue !== 'false';
629
+ this.stencil = parseBool(newValue, true);
610
630
  break;
611
631
  }
612
632
  }
@@ -614,4 +634,10 @@ class AppElement extends AsyncElement {
614
634
 
615
635
  customElements.define('pc-app', AppElement);
616
636
 
637
+ declare global {
638
+ interface HTMLElementTagNameMap {
639
+ 'pc-app': AppElement;
640
+ }
641
+ }
642
+
617
643
  export { AppElement };
package/src/asset.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import { Asset, SPRITE_RENDERMODE_SIMPLE, SPRITE_RENDERMODE_SLICED, SPRITE_RENDERMODE_TILED } from 'playcanvas';
2
2
 
3
- import { parseEnum } from './utils';
3
+ import { AsyncElement } from './async-element';
4
+ import { parseBool, parseEnum, parseNumber } from './utils';
4
5
  import { MeshoptDecoder } from '../lib/meshopt_decoder.module.js';
5
6
 
6
- const renderModes = new Map<string, number>([
7
+ const renderModes = new Map<'simple' | 'sliced' | 'tiled', number>([
7
8
  ['simple', SPRITE_RENDERMODE_SIMPLE],
8
9
  ['sliced', SPRITE_RENDERMODE_SLICED],
9
10
  ['tiled', SPRITE_RENDERMODE_TILED]
@@ -74,15 +75,59 @@ const processBufferView = (
74
75
  * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-asset/ | `<pc-asset>`} elements.
75
76
  * The AssetElement interface also inherits the properties and methods of the
76
77
  * {@link HTMLElement} interface.
78
+ *
79
+ * The element becomes ready once the containing application has started and the asset is in the
80
+ * state declared by the markup: loaded for preloaded assets (even if loading failed — check the
81
+ * asset's `resource`), or registered and awaiting a load for `lazy` assets. Elements inserted
82
+ * while the application is running are created and registered on insertion, and begin loading
83
+ * immediately unless `lazy`. A `pc-asset` must be a direct child of `pc-app` — elements placed
84
+ * elsewhere, or with an unsupported asset type, never become ready.
77
85
  */
78
- class AssetElement extends HTMLElement {
86
+ class AssetElement extends AsyncElement {
79
87
  private _lazy: boolean = false;
80
88
 
81
89
  /**
82
- * The asset that is loaded.
90
+ * The asset that is loaded. Available once the element is ready — await
91
+ * {@link whenReady} or the element's `ready()` promise before accessing it.
83
92
  */
84
93
  asset: Asset | null = null;
85
94
 
95
+ async connectedCallback() {
96
+ const appElement = this.closestApp;
97
+ if (!appElement) return;
98
+
99
+ // Assets must be direct children of pc-app (matches the boot query ':scope > pc-asset')
100
+ if (this.parentElement !== appElement) {
101
+ console.warn(`pc-asset '${this.getAttribute('id') ?? this.getAttribute('src')}' must be a direct child of pc-app - asset not created`);
102
+ return;
103
+ }
104
+
105
+ await appElement.ready();
106
+
107
+ // The element may have been removed or re-parented while waiting for the app
108
+ if (!this.isConnected || this.parentElement !== appElement) return;
109
+
110
+ // Assets present at startup are created by AppElement's boot; this branch handles
111
+ // elements inserted (or re-inserted) after the app is already running
112
+ if (!this.asset) {
113
+ const app = appElement.app;
114
+ if (!app) return; // pc-app is re-connecting; its own boot will create this asset
115
+
116
+ this.createAsset();
117
+ if (this.asset) {
118
+ app.assets.add(this.asset); // add() auto-loads when preload is true
119
+ if (!this.lazy) {
120
+ app.assets.load(this.asset);
121
+ }
122
+ }
123
+ }
124
+
125
+ // Never ready if createAsset failed (unsupported asset type)
126
+ if (this.asset) {
127
+ this._onReady();
128
+ }
129
+ }
130
+
86
131
  disconnectedCallback() {
87
132
  this.destroyAsset();
88
133
  }
@@ -167,12 +212,12 @@ class AssetElement extends HTMLElement {
167
212
 
168
213
  const pixelsPerUnit = this.getAttribute('pixels-per-unit');
169
214
  if (pixelsPerUnit !== null) {
170
- data.pixelsPerUnit = Number(pixelsPerUnit);
215
+ data.pixelsPerUnit = parseNumber(pixelsPerUnit, 1, 'pixels-per-unit');
171
216
  }
172
217
 
173
218
  const renderMode = this.getAttribute('render-mode');
174
219
  if (renderMode !== null) {
175
- data.renderMode = parseEnum(renderMode, renderModes, SPRITE_RENDERMODE_SIMPLE);
220
+ data.renderMode = renderModes.get(parseEnum(renderMode, renderModes, 'simple', 'render-mode'));
176
221
  }
177
222
 
178
223
  // Apply engine defaults for any values not supplied.
@@ -187,6 +232,8 @@ class AssetElement extends HTMLElement {
187
232
 
188
233
  destroyAsset() {
189
234
  if (this.asset) {
235
+ // Deregister first so unload() can still notify the registry
236
+ this.asset.registry?.remove(this.asset);
190
237
  this.asset.unload();
191
238
  this.asset = null;
192
239
  }
@@ -220,13 +267,19 @@ class AssetElement extends HTMLElement {
220
267
  return ['lazy'];
221
268
  }
222
269
 
223
- attributeChangedCallback(name: string, _oldValue: string, _newValue: string) {
270
+ attributeChangedCallback(name: string, _oldValue: string, newValue: string) {
224
271
  if (name === 'lazy') {
225
- this.lazy = this.hasAttribute('lazy');
272
+ this.lazy = parseBool(newValue, false);
226
273
  }
227
274
  }
228
275
  }
229
276
 
230
277
  customElements.define('pc-asset', AssetElement);
231
278
 
279
+ declare global {
280
+ interface HTMLElementTagNameMap {
281
+ 'pc-asset': AssetElement;
282
+ }
283
+ }
284
+
232
285
  export { AssetElement };
@@ -26,16 +26,18 @@ class AsyncElement extends HTMLElement {
26
26
  }
27
27
 
28
28
  /**
29
- * Called when the element is fully initialized and ready.
30
- * Subclasses should call this when they're ready.
29
+ * Called when the element is fully initialized and ready. Subclasses should call this when
30
+ * they're ready. Resolves the ready promise and dispatches a bubbling, composed `ready`
31
+ * event.
31
32
  */
32
33
  protected _onReady() {
33
34
  this._readyResolve();
34
- this.dispatchEvent(new CustomEvent('ready'));
35
+ this.dispatchEvent(new CustomEvent('ready', { bubbles: true, composed: true }));
35
36
  }
36
37
 
37
38
  /**
38
- * Returns a promise that resolves with this element when it's ready.
39
+ * Returns a promise that resolves with this element when it's ready. This is the low-level
40
+ * primitive underlying {@link whenReady}, which is the recommended way to wait for elements.
39
41
  * @returns A promise that resolves with this element when it's ready.
40
42
  */
41
43
  ready(): Promise<this> {
@@ -43,4 +45,83 @@ class AsyncElement extends HTMLElement {
43
45
  }
44
46
  }
45
47
 
46
- export { AsyncElement };
48
+ /**
49
+ * A union of the tag names of all elements that initialize asynchronously (i.e. elements whose
50
+ * classes extend {@link AsyncElement}).
51
+ */
52
+ type AsyncElementTagName = {
53
+ [K in keyof HTMLElementTagNameMap]: HTMLElementTagNameMap[K] extends AsyncElement ? K : never
54
+ }[keyof HTMLElementTagNameMap];
55
+
56
+ /**
57
+ * Waits for the first element matching the given tag name to be fully initialized. Note that the
58
+ * promise never settles if the element cannot finish initializing (for example, a `<pc-script>`
59
+ * that is not a direct child of `<pc-scripts>`). A component element outside a `<pc-entity>` is
60
+ * the exception: it still becomes ready, but its `component` is `null`. Either way, a misplaced
61
+ * element logs a warning naming the parent it requires.
62
+ * @param target - The tag name of the element to wait for (e.g. `'pc-app'`).
63
+ * @returns A promise that resolves with the element once it's ready.
64
+ * @example
65
+ * const { app } = await whenReady('pc-app');
66
+ */
67
+ function whenReady<K extends AsyncElementTagName>(target: K): Promise<HTMLElementTagNameMap[K]>;
68
+ /**
69
+ * Waits for the given element to be fully initialized. Note that the promise never settles if
70
+ * the element cannot finish initializing (for example, an element that is never added to the
71
+ * document).
72
+ * @param target - The element to wait for.
73
+ * @returns A promise that resolves with the element once it's ready.
74
+ * @example
75
+ * const appElement = document.createElement('pc-app');
76
+ * document.body.appendChild(appElement);
77
+ * const { app } = await whenReady(appElement);
78
+ */
79
+ function whenReady<T extends AsyncElement>(target: T): Promise<T>;
80
+ /**
81
+ * Waits for the first element matching the given CSS selector to be fully initialized. Note that
82
+ * the promise never settles if the element cannot finish initializing (for example, a `<pc-script>`
83
+ * that is not a direct child of `<pc-scripts>`). A component element outside a `<pc-entity>` is
84
+ * the exception: it still becomes ready, but its `component` is `null`. Either way, a misplaced
85
+ * element logs a warning naming the parent it requires.
86
+ * @param target - A CSS selector matching the element to wait for (e.g. `'#my-app'`).
87
+ * @returns A promise that resolves with the element once it's ready.
88
+ * @example
89
+ * // In TypeScript, supply the element type when using an arbitrary selector
90
+ * const { entity } = await whenReady<EntityElement>('pc-entity[name="camera"]');
91
+ */
92
+ function whenReady<T extends AsyncElement = AsyncElement, S extends string = string>(
93
+ target: S extends Exclude<keyof HTMLElementTagNameMap, AsyncElementTagName> ? never : S
94
+ ): Promise<T>;
95
+ async function whenReady(target: string | AsyncElement): Promise<AsyncElement> {
96
+ let element: Element | null;
97
+ if (typeof target === 'string') {
98
+ if (document.readyState === 'loading') {
99
+ await new Promise((resolve) => {
100
+ document.addEventListener('DOMContentLoaded', resolve, { once: true });
101
+ });
102
+ }
103
+
104
+ try {
105
+ element = document.querySelector(target);
106
+ } catch {
107
+ throw new Error(`whenReady: '${target}' is not a valid CSS selector`);
108
+ }
109
+ if (!element) {
110
+ throw new Error(`whenReady: no element found matching '${target}'`);
111
+ }
112
+ } else {
113
+ element = target;
114
+ }
115
+
116
+ if (!(element instanceof AsyncElement)) {
117
+ const description = element instanceof Element ? `<${element.tagName.toLowerCase()}>` : String(target);
118
+ throw new Error(`whenReady: ${description} does not initialize asynchronously`);
119
+ }
120
+
121
+ await element.ready();
122
+
123
+ return element;
124
+ }
125
+
126
+ export { AsyncElement, whenReady };
127
+ export type { AsyncElementTagName };