@playcanvas/web-components 0.9.0 → 0.10.1

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 (74) hide show
  1. package/README.md +18 -0
  2. package/dist/app.d.ts +55 -9
  3. package/dist/asset.d.ts +25 -1
  4. package/dist/async-element.d.ts +15 -2
  5. package/dist/components/button-component.d.ts +1 -1
  6. package/dist/components/camera-component.d.ts +1 -1
  7. package/dist/components/collision-component.d.ts +1 -1
  8. package/dist/components/component.d.ts +6 -5
  9. package/dist/components/element-component.d.ts +1 -1
  10. package/dist/components/gsplat-component.d.ts +1 -1
  11. package/dist/components/layoutchild-component.d.ts +1 -1
  12. package/dist/components/layoutgroup-component.d.ts +1 -1
  13. package/dist/components/light-component.d.ts +1 -1
  14. package/dist/components/particlesystem-component.d.ts +1 -1
  15. package/dist/components/render-component.d.ts +1 -1
  16. package/dist/components/rigidbody-component.d.ts +1 -1
  17. package/dist/components/screen-component.d.ts +1 -1
  18. package/dist/components/script.d.ts +8 -1
  19. package/dist/components/scrollbar-component.d.ts +1 -1
  20. package/dist/components/scrollview-component.d.ts +1 -1
  21. package/dist/components/sound-component.d.ts +1 -1
  22. package/dist/components/sound-slot.d.ts +9 -1
  23. package/dist/custom-elements.json +16704 -0
  24. package/dist/entity.d.ts +24 -5
  25. package/dist/loading-bar.d.ts +35 -0
  26. package/dist/material.d.ts +972 -4
  27. package/dist/model.d.ts +1 -1
  28. package/dist/module.d.ts +10 -0
  29. package/dist/{utils.d.ts → parse.d.ts} +63 -33
  30. package/dist/pwc.cjs +3070 -699
  31. package/dist/pwc.cjs.map +1 -1
  32. package/dist/pwc.js +3070 -699
  33. package/dist/pwc.js.map +1 -1
  34. package/dist/pwc.min.js +1 -1
  35. package/dist/pwc.min.js.map +1 -1
  36. package/dist/pwc.min.mjs +2 -0
  37. package/dist/pwc.min.mjs.map +1 -0
  38. package/dist/pwc.mjs +3071 -700
  39. package/dist/pwc.mjs.map +1 -1
  40. package/dist/scene.d.ts +12 -4
  41. package/dist/sky.d.ts +1 -1
  42. package/dist/vscode.html-custom-data.json +1800 -0
  43. package/dist/web-types.json +3836 -0
  44. package/package.json +29 -11
  45. package/src/app.ts +178 -78
  46. package/src/asset.ts +44 -2
  47. package/src/async-element.ts +17 -4
  48. package/src/components/button-component.ts +6 -6
  49. package/src/components/camera-component.ts +2 -2
  50. package/src/components/collision-component.ts +2 -2
  51. package/src/components/component.ts +8 -7
  52. package/src/components/element-component.ts +6 -6
  53. package/src/components/gsplat-component.ts +3 -3
  54. package/src/components/layoutchild-component.ts +2 -2
  55. package/src/components/layoutgroup-component.ts +2 -2
  56. package/src/components/light-component.ts +2 -2
  57. package/src/components/particlesystem-component.ts +2 -2
  58. package/src/components/render-component.ts +10 -5
  59. package/src/components/rigidbody-component.ts +2 -2
  60. package/src/components/screen-component.ts +2 -2
  61. package/src/components/script-component.ts +4 -4
  62. package/src/components/script.ts +9 -2
  63. package/src/components/scrollbar-component.ts +3 -3
  64. package/src/components/scrollview-component.ts +6 -6
  65. package/src/components/sound-component.ts +2 -2
  66. package/src/components/sound-slot.ts +31 -9
  67. package/src/entity.ts +56 -22
  68. package/src/loading-bar.ts +122 -0
  69. package/src/material.ts +2402 -59
  70. package/src/model.ts +2 -2
  71. package/src/module.ts +10 -0
  72. package/src/{utils.ts → parse.ts} +104 -65
  73. package/src/scene.ts +51 -21
  74. package/src/sky.ts +3 -3
package/dist/pwc.js CHANGED
@@ -6,8 +6,13 @@
6
6
 
7
7
  /**
8
8
  * Base class for all PlayCanvas Web Components that initialize asynchronously.
9
+ *
10
+ * @fires {CustomEvent} ready - Fired once the element is fully initialized. Bubbles and is
11
+ * composed.
9
12
  */
10
13
  class AsyncElement extends HTMLElement {
14
+ _readyPromise;
15
+ _readyResolve;
11
16
  /** @ignore */
12
17
  constructor() {
13
18
  super();
@@ -15,13 +20,21 @@
15
20
  this._readyResolve = resolve;
16
21
  });
17
22
  }
23
+ /**
24
+ * The nearest ancestor `<pc-app>` element, or `null` if this element has no `<pc-app>`
25
+ * ancestor. The search starts at the parent, so an element never resolves to itself.
26
+ * @returns The closest app element, or `null`.
27
+ */
18
28
  get closestApp() {
19
- var _a;
20
- return (_a = this.parentElement) === null || _a === void 0 ? void 0 : _a.closest('pc-app');
29
+ return this.parentElement?.closest('pc-app') ?? null;
21
30
  }
31
+ /**
32
+ * The nearest ancestor `<pc-entity>` element, or `null` if this element has no `<pc-entity>`
33
+ * ancestor. The search starts at the parent, so an element never resolves to itself.
34
+ * @returns The closest entity element, or `null`.
35
+ */
22
36
  get closestEntity() {
23
- var _a;
24
- return (_a = this.parentElement) === null || _a === void 0 ? void 0 : _a.closest('pc-entity');
37
+ return this.parentElement?.closest('pc-entity') ?? null;
25
38
  }
26
39
  /**
27
40
  * Called when the element is fully initialized and ready. Subclasses should call this when
@@ -52,7 +65,7 @@
52
65
  try {
53
66
  element = document.querySelector(target);
54
67
  }
55
- catch (_a) {
68
+ catch {
56
69
  throw new Error(`whenReady: '${target}' is not a valid CSS selector`);
57
70
  }
58
71
  if (!element) {
@@ -75,8 +88,19 @@
75
88
  * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-module/ | `<pc-module>`} elements.
76
89
  * The ModuleElement interface also inherits the properties and methods of the
77
90
  * {@link HTMLElement} interface.
91
+ *
92
+ * Note that these attributes are read once when the element is created, so changing them later
93
+ * has no effect.
94
+ *
95
+ * @attribute {string} name - The name of the WebAssembly module to configure, e.g. `Basis` or
96
+ * `Ammo`.
97
+ * @attribute {string} glue - The URL of the module's glue script.
98
+ * @attribute {string} wasm - The URL of the module's WebAssembly binary.
99
+ * @attribute {string} fallback - The URL of the module's asm.js fallback script, used when
100
+ * WebAssembly is unavailable.
78
101
  */
79
102
  class ModuleElement extends HTMLElement {
103
+ loadPromise;
80
104
  /** @ignore */
81
105
  constructor() {
82
106
  super();
@@ -104,6 +128,116 @@
104
128
  }
105
129
  customElements.define('pc-module', ModuleElement);
106
130
 
131
+ /** Covers the 0.2s opacity transition; jsdom never fires transitionend, so removal is timed. */
132
+ const REMOVAL_DELAY_MS = 250;
133
+ /**
134
+ * The slim progress bar `<pc-app>` shows while it boots and preloads. An implementation detail of
135
+ * AppElement rather than a custom element, so its shape can change without a breaking change.
136
+ *
137
+ * All styling is inline, so the library injects no stylesheet. The colors and height resolve CSS
138
+ * custom properties — `--pc-loading-bar-color`, `--pc-loading-bar-background` and
139
+ * `--pc-loading-bar-height` — so a page can theme the bar from `pc-app` or `:root`.
140
+ */
141
+ class LoadingBar {
142
+ _track;
143
+ _fill;
144
+ _sweep = null;
145
+ _removal = null;
146
+ /**
147
+ * Creates the bar and appends it to `parent`, starting in the indeterminate state.
148
+ * @param parent - The element to append the bar to.
149
+ */
150
+ constructor(parent) {
151
+ this._track = document.createElement('div');
152
+ this._track.setAttribute('role', 'progressbar');
153
+ this._track.setAttribute('aria-label', 'Loading');
154
+ this._track.setAttribute('aria-valuemin', '0');
155
+ this._track.setAttribute('aria-valuemax', '100');
156
+ // Fixed positioning matches the canvas, which always fills the window (FILLMODE_FILL_WINDOW)
157
+ this._track.style.cssText = [
158
+ 'position: fixed',
159
+ 'top: 0',
160
+ 'left: 0',
161
+ 'width: 100%',
162
+ 'height: var(--pc-loading-bar-height, 3px)',
163
+ 'background: var(--pc-loading-bar-background, rgba(0, 0, 0, 0.1))',
164
+ 'z-index: 10000',
165
+ 'pointer-events: none',
166
+ 'opacity: 1',
167
+ 'transition: opacity 0.2s ease'
168
+ ].join('; ');
169
+ this._fill = document.createElement('div');
170
+ this._fill.style.cssText = [
171
+ 'width: 100%',
172
+ 'height: 100%',
173
+ 'transform-origin: left center',
174
+ 'transform: scaleX(0)',
175
+ 'background: var(--pc-loading-bar-color, #f60)',
176
+ 'transition: transform 0.2s ease'
177
+ ].join('; ');
178
+ this._track.appendChild(this._fill);
179
+ parent.appendChild(this._track);
180
+ // Indeterminate sweep until the first progress() call reports a real total. No
181
+ // aria-valuenow is set, which is what marks a progressbar indeterminate. jsdom has no Web
182
+ // Animations API, so the guard degrades to a static bar there rather than crashing boot.
183
+ if (typeof this._fill.animate === 'function') {
184
+ this._sweep = this._fill.animate([
185
+ { transform: 'scaleX(0.25) translateX(-100%)' },
186
+ { transform: 'scaleX(0.25) translateX(500%)' }
187
+ ], {
188
+ duration: 1000,
189
+ iterations: Infinity,
190
+ easing: 'ease-in-out'
191
+ });
192
+ }
193
+ }
194
+ /**
195
+ * Reflects preload progress, switching the bar from indeterminate to determinate on the first
196
+ * call.
197
+ * @param loaded - The number of assets that have finished loading.
198
+ * @param total - The number of assets being preloaded.
199
+ */
200
+ progress(loaded, total) {
201
+ if (this._sweep) {
202
+ this._sweep.cancel();
203
+ this._sweep = null;
204
+ }
205
+ const fraction = total === 0 ? 1 : loaded / total;
206
+ this._track.setAttribute('aria-valuenow', String(Math.round(fraction * 100)));
207
+ this._fill.style.transform = `scaleX(${fraction})`;
208
+ }
209
+ /**
210
+ * Fills the bar, fades it out and removes it. Idempotent.
211
+ */
212
+ complete() {
213
+ if (this._removal !== null) {
214
+ return;
215
+ }
216
+ if (this._sweep) {
217
+ this._sweep.cancel();
218
+ this._sweep = null;
219
+ }
220
+ this._track.setAttribute('aria-valuenow', '100');
221
+ this._fill.style.transform = 'scaleX(1)';
222
+ this._track.style.opacity = '0';
223
+ this._removal = setTimeout(() => this._track.remove(), REMOVAL_DELAY_MS);
224
+ }
225
+ /**
226
+ * Removes the bar immediately, cancelling any pending fade. Idempotent.
227
+ */
228
+ destroy() {
229
+ if (this._sweep) {
230
+ this._sweep.cancel();
231
+ this._sweep = null;
232
+ }
233
+ if (this._removal !== null) {
234
+ clearTimeout(this._removal);
235
+ this._removal = null;
236
+ }
237
+ this._track.remove();
238
+ }
239
+ }
240
+
107
241
  const CSS_COLORS = {
108
242
  aliceblue: '#f0f8ff',
109
243
  antiquewhite: '#faebd7',
@@ -256,20 +390,22 @@
256
390
  };
257
391
 
258
392
  /**
259
- * Parse a boolean attribute value. The same rules apply to every boolean attribute:
393
+ * Converts HTML attribute values into the values the engine expects. Every element's
394
+ * `attributeChangedCallback` funnels through this module.
260
395
  *
261
- * - Attribute absent (or removed): the supplied default is used.
262
- * - Attribute set to the string 'false': `false`.
263
- * - Attribute present with any other value, including the empty string of a bare boolean
264
- * attribute (e.g. `<pc-light cast-shadows>`): `true`.
396
+ * The parsers share one contract:
265
397
  *
266
- * @param value - The attribute value to parse (`null` when the attribute is absent).
267
- * @param defaultValue - The value to use when the attribute is absent or removed.
268
- * @returns The parsed boolean.
398
+ * - A `null` value means the attribute is absent or was removed, and yields the supplied default.
399
+ * - A malformed value yields the same default and logs exactly one `console.warn` naming the
400
+ * attribute, so misuse is reported rather than thrown — nothing here throws or rejects.
401
+ * - A math-type default is cloned on the way out, which is what makes it safe to pass the engine's
402
+ * shared frozen constants (`Vec3.ZERO`, `Color.WHITE`) as defaults.
403
+ * - `parseBool` and `parseTags` take no attribute name, because every value is valid for them and
404
+ * so they never warn.
405
+ *
406
+ * `getEntity` is the exception: it resolves a reference to a live entity rather than parsing a
407
+ * literal, and returns `null` instead of falling back to a default.
269
408
  */
270
- const parseBool = (value, defaultValue) => {
271
- return value === null ? defaultValue : value !== 'false';
272
- };
273
409
  /**
274
410
  * Splits an attribute value into exactly `count` numeric components. Returns `null` when the
275
411
  * value does not consist of exactly `count` whitespace-separated finite numbers.
@@ -297,6 +433,21 @@
297
433
  const cloneDefault = (value) => {
298
434
  return (value === null ? null : value.clone());
299
435
  };
436
+ /**
437
+ * Parse a boolean attribute value. The same rules apply to every boolean attribute:
438
+ *
439
+ * - Attribute absent (or removed): the supplied default is used.
440
+ * - Attribute set to the string 'false': `false`.
441
+ * - Attribute present with any other value, including the empty string of a bare boolean
442
+ * attribute (e.g. `<pc-light cast-shadows>`): `true`.
443
+ *
444
+ * @param value - The attribute value to parse (`null` when the attribute is absent).
445
+ * @param defaultValue - The value to use when the attribute is absent or removed.
446
+ * @returns The parsed boolean.
447
+ */
448
+ const parseBool = (value, defaultValue) => {
449
+ return value === null ? defaultValue : value !== 'false';
450
+ };
300
451
  /**
301
452
  * Parse a color attribute value. The expected format is a CSS color name (e.g. 'rebeccapurple'),
302
453
  * a hex color (e.g. '#ff0000' or '#f00'), or 3 or 4 space-separated numbers in the range 0 to 1
@@ -310,7 +461,6 @@
310
461
  * @returns The parsed Color object.
311
462
  */
312
463
  const parseColor = (value, defaultValue, attribute) => {
313
- var _a;
314
464
  if (value === null) {
315
465
  return cloneDefault(defaultValue);
316
466
  }
@@ -328,13 +478,56 @@
328
478
  return new playcanvas.Color().fromString(`#${hex}`);
329
479
  }
330
480
  // 3 or 4 space-separated components (e.g. '1 0.5 0.5')
331
- const components = (_a = parseComponents(value, 4)) !== null && _a !== void 0 ? _a : parseComponents(value, 3);
481
+ const components = parseComponents(value, 4) ?? parseComponents(value, 3);
332
482
  if (components) {
333
483
  return new playcanvas.Color(components);
334
484
  }
335
485
  console.warn(`Invalid value '${value}' for attribute '${attribute}'. Expected a CSS color name, a hex color or 3 or 4 space-separated numbers. Using '${defaultValue}'.`);
336
486
  return cloneDefault(defaultValue);
337
487
  };
488
+ /**
489
+ * Resolves an enum attribute value against its set of valid names. Returns the value when it is
490
+ * one of the valid names. Returns `defaultValue` when the attribute is absent (`null`), or when
491
+ * the value is invalid — the latter also logs a warning listing the valid names.
492
+ *
493
+ * @param value - The attribute value to parse (`null` when the attribute is absent).
494
+ * @param valid - The valid names: an array, or a map whose keys are the valid names.
495
+ * @param defaultValue - The value to use when the attribute is absent or invalid.
496
+ * @param attribute - The attribute name, used in the warning message.
497
+ * @returns The resolved enum name.
498
+ */
499
+ const parseEnum = (value, valid, defaultValue, attribute) => {
500
+ if (value === null) {
501
+ return defaultValue;
502
+ }
503
+ const names = Array.isArray(valid) ? valid : [...valid.keys()];
504
+ if (names.includes(value)) {
505
+ return value;
506
+ }
507
+ console.warn(`Invalid value '${value}' for attribute '${attribute}'. Valid values: ${names.join(', ')}. Using '${defaultValue}'.`);
508
+ return defaultValue;
509
+ };
510
+ /**
511
+ * Parses a number attribute value. Returns the parsed number when the value is a finite number.
512
+ * Returns `defaultValue` when the attribute is absent (`null`), or when the value is not a
513
+ * finite number — the latter also logs a warning.
514
+ *
515
+ * @param value - The attribute value to parse (`null` when the attribute is absent).
516
+ * @param defaultValue - The value to use when the attribute is absent or invalid.
517
+ * @param attribute - The attribute name, used in the warning message.
518
+ * @returns The parsed number.
519
+ */
520
+ const parseNumber = (value, defaultValue, attribute) => {
521
+ if (value === null) {
522
+ return defaultValue;
523
+ }
524
+ const number = value.trim() === '' ? NaN : Number(value);
525
+ if (!Number.isFinite(number)) {
526
+ console.warn(`Invalid value '${value}' for attribute '${attribute}'. Expected a finite number. Using '${defaultValue}'.`);
527
+ return defaultValue;
528
+ }
529
+ return number;
530
+ };
338
531
  /**
339
532
  * Parse an Euler-angles attribute value into a quaternion. The expected format is 3
340
533
  * space-separated angles in degrees (e.g. '0 90 0'). Returns `defaultValue` (cloned, when it is
@@ -357,6 +550,26 @@
357
550
  }
358
551
  return new playcanvas.Quat().setFromEulerAngles(components[0], components[1], components[2]);
359
552
  };
553
+ /**
554
+ * Parse a tags attribute value. The expected format is a comma-separated list of tag names
555
+ * (e.g. 'enemy, flying'). Surrounding whitespace is trimmed from each name and empty names are
556
+ * discarded, so a trailing comma or a doubled separator does not produce a blank tag. Returns a
557
+ * copy of `defaultValue` when the attribute is absent or removed (`null`).
558
+ *
559
+ * Every value is valid, so this never warns.
560
+ *
561
+ * @param value - The attribute value to parse (`null` when the attribute is absent).
562
+ * @param defaultValue - The value to use when the attribute is absent or removed.
563
+ * @returns The parsed tag names.
564
+ */
565
+ const parseTags = (value, defaultValue = []) => {
566
+ if (value === null) {
567
+ // Copied for the same reason cloneDefault exists: a parsed result must never alias the
568
+ // caller's default, or a later mutation would write back through it.
569
+ return [...defaultValue];
570
+ }
571
+ return value.split(',').map(tag => tag.trim()).filter(tag => tag !== '');
572
+ };
360
573
  /**
361
574
  * Parse a Vec2 attribute value. The expected format is 2 space-separated numbers (e.g. '1 2').
362
575
  * Returns `defaultValue` (cloned, when it is a vector) when the attribute is absent (`null`),
@@ -420,49 +633,6 @@
420
633
  }
421
634
  return new playcanvas.Vec4(components);
422
635
  };
423
- /**
424
- * Resolves an enum attribute value against its set of valid names. Returns the value when it is
425
- * one of the valid names. Returns `defaultValue` when the attribute is absent (`null`), or when
426
- * the value is invalid — the latter also logs a warning listing the valid names.
427
- *
428
- * @param value - The attribute value to parse (`null` when the attribute is absent).
429
- * @param valid - The valid names: an array, or a map whose keys are the valid names.
430
- * @param defaultValue - The value to use when the attribute is absent or invalid.
431
- * @param attribute - The attribute name, used in the warning message.
432
- * @returns The resolved enum name.
433
- */
434
- const parseEnum = (value, valid, defaultValue, attribute) => {
435
- if (value === null) {
436
- return defaultValue;
437
- }
438
- const names = Array.isArray(valid) ? valid : [...valid.keys()];
439
- if (names.includes(value)) {
440
- return value;
441
- }
442
- console.warn(`Invalid value '${value}' for attribute '${attribute}'. Valid values: ${names.join(', ')}. Using '${defaultValue}'.`);
443
- return defaultValue;
444
- };
445
- /**
446
- * Parses a number attribute value. Returns the parsed number when the value is a finite number.
447
- * Returns `defaultValue` when the attribute is absent (`null`), or when the value is not a
448
- * finite number — the latter also logs a warning.
449
- *
450
- * @param value - The attribute value to parse (`null` when the attribute is absent).
451
- * @param defaultValue - The value to use when the attribute is absent or invalid.
452
- * @param attribute - The attribute name, used in the warning message.
453
- * @returns The parsed number.
454
- */
455
- const parseNumber = (value, defaultValue, attribute) => {
456
- if (value === null) {
457
- return defaultValue;
458
- }
459
- const number = value.trim() === '' ? NaN : Number(value);
460
- if (!Number.isFinite(number)) {
461
- console.warn(`Invalid value '${value}' for attribute '${attribute}'. Expected a finite number. Using '${defaultValue}'.`);
462
- return defaultValue;
463
- }
464
- return number;
465
- };
466
636
  /**
467
637
  * Resolves a reference string to the {@link Entity} backing a `<pc-entity>` element. The reference
468
638
  * can be a CSS selector (e.g. `#my-id`, `pc-entity[name="Foo"]`), a bare element id, or a bare
@@ -472,7 +642,6 @@
472
642
  * @returns The resolved entity, or `null`.
473
643
  */
474
644
  const getEntity = (ref) => {
475
- var _a, _b;
476
645
  if (!ref) {
477
646
  return null;
478
647
  }
@@ -482,13 +651,13 @@
482
651
  try {
483
652
  element = document.querySelector(ref);
484
653
  }
485
- catch (_c) {
654
+ catch {
486
655
  element = null;
487
656
  }
488
657
  if (!element) {
489
- element = (_a = document.getElementById(ref)) !== null && _a !== void 0 ? _a : document.querySelector(`pc-entity[name="${ref}"]`);
658
+ element = document.getElementById(ref) ?? document.querySelector(`pc-entity[name="${ref}"]`);
490
659
  }
491
- return (_b = element === null || element === void 0 ? void 0 : element.entity) !== null && _b !== void 0 ? _b : null;
660
+ return element?.entity ?? null;
492
661
  };
493
662
 
494
663
  /**
@@ -496,16 +665,63 @@
496
665
  * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-app/ | `<pc-app>`} elements.
497
666
  * The AppElement interface also inherits the properties and methods of the
498
667
  * {@link HTMLElement} interface.
668
+ *
669
+ * @fires {ProgressEvent} progress - Fired while the application preloads its assets. `loaded` and
670
+ * `total` are asset counts, not bytes, and an asset that fails to load still counts as loaded.
671
+ * Fired at least once per boot, and the final event always has `loaded` equal to `total`. Does
672
+ * not bubble.
499
673
  */
500
674
  class AppElement extends AsyncElement {
501
675
  /**
502
- * The PlayCanvas application instance. Available once the element is ready — await
503
- * {@link whenReady} or the element's `ready()` promise before accessing it.
504
- * @returns The application instance.
676
+ * The canvas element.
677
+ */
678
+ _canvas = null;
679
+ _alpha = true;
680
+ _backend = 'webgpu';
681
+ _antialias = true;
682
+ _depth = true;
683
+ _stencil = true;
684
+ _highResolution = true;
685
+ _loadingBar = true;
686
+ _bar = null;
687
+ _hierarchyReady = false;
688
+ _picker = null;
689
+ _hasPointerListeners = {
690
+ pointerenter: false,
691
+ pointerleave: false,
692
+ pointerdown: false,
693
+ pointerup: false,
694
+ pointermove: false
695
+ };
696
+ _hoveredEntity = null;
697
+ // Identifies the newest in-flight hover pick, so out-of-order results can be discarded
698
+ _pickToken = 0;
699
+ _pointerHandlers = {
700
+ pointermove: null,
701
+ pointerdown: null,
702
+ pointerup: null
703
+ };
704
+ _app = null;
705
+ _loadProgress = 0;
706
+ /**
707
+ * The PlayCanvas application instance. `null` until the element is ready, and again once it
708
+ * has been removed from the document — await {@link whenReady} or the element's `ready()`
709
+ * promise before accessing it.
710
+ * @returns The application instance, or `null`.
505
711
  */
506
712
  get app() {
507
713
  return this._app;
508
714
  }
715
+ /**
716
+ * The asset preload progress of the application, as a fraction from 0 to 1. It is 0 until
717
+ * preloading begins (and again once the element has been removed from the document), and 1
718
+ * once preloading has finished — including when there was nothing to preload. Read this to
719
+ * initialize a loading UI; subsequent updates arrive via the `progress` event.
720
+ * @returns The preload progress.
721
+ */
722
+ get loadProgress() {
723
+ return this._loadProgress;
724
+ }
509
725
  /**
510
726
  * Creates a new AppElement instance.
511
727
  *
@@ -513,36 +729,15 @@
513
729
  */
514
730
  constructor() {
515
731
  super();
516
- /**
517
- * The canvas element.
518
- */
519
- this._canvas = null;
520
- this._alpha = true;
521
- this._backend = 'webgl2';
522
- this._antialias = true;
523
- this._depth = true;
524
- this._stencil = true;
525
- this._highResolution = true;
526
- this._hierarchyReady = false;
527
- this._picker = null;
528
- this._hasPointerListeners = {
529
- pointerenter: false,
530
- pointerleave: false,
531
- pointerdown: false,
532
- pointerup: false,
533
- pointermove: false
534
- };
535
- this._hoveredEntity = null;
536
- this._pointerHandlers = {
537
- pointermove: null,
538
- pointerdown: null,
539
- pointerup: null
540
- };
541
- this._app = null;
542
732
  // Bind methods to maintain 'this' context
543
733
  this._onWindowResize = this._onWindowResize.bind(this);
544
734
  }
545
735
  async connectedCallback() {
736
+ // Created before the first await, so the bar is visible while modules and the graphics
737
+ // device are created, and exists before any disconnect could need to clean it up
738
+ if (this._loadingBar && !this._bar) {
739
+ this._bar = new LoadingBar(this);
740
+ }
546
741
  // Get all pc-module elements that are direct children of the pc-app element
547
742
  const moduleElements = this.querySelectorAll(':scope > pc-module');
548
743
  // Wait for all modules to load
@@ -630,10 +825,11 @@
630
825
  createOptions.lightmapper = playcanvas.Lightmapper;
631
826
  createOptions.batchManager = playcanvas.BatchManager;
632
827
  createOptions.xr = playcanvas.XrManager;
633
- this._app = new playcanvas.AppBase(this._canvas);
634
- this.app.init(createOptions);
635
- this.app.setCanvasFillMode(playcanvas.FILLMODE_FILL_WINDOW);
636
- this.app.setCanvasResolution(playcanvas.RESOLUTION_AUTO);
828
+ const app = new playcanvas.AppBase(this._canvas);
829
+ this._app = app;
830
+ app.init(createOptions);
831
+ app.setCanvasFillMode(playcanvas.FILLMODE_FILL_WINDOW);
832
+ app.setCanvasResolution(playcanvas.RESOLUTION_AUTO);
637
833
  this._pickerCreate();
638
834
  // Get all pc-asset elements that are direct children of the pc-app element
639
835
  const assetElements = this.querySelectorAll(':scope > pc-asset');
@@ -641,7 +837,7 @@
641
837
  assetElement.createAsset();
642
838
  const asset = assetElement.asset;
643
839
  if (asset) {
644
- this.app.assets.add(asset);
840
+ app.assets.add(asset);
645
841
  }
646
842
  });
647
843
  // Get all pc-material elements that are direct children of the pc-app element
@@ -652,17 +848,39 @@
652
848
  // Create all entities
653
849
  const entityElements = this.querySelectorAll('pc-entity');
654
850
  Array.from(entityElements).forEach((entityElement) => {
655
- entityElement.createEntity(this.app);
851
+ entityElement.createEntity(app);
656
852
  });
657
853
  // Build hierarchy
658
854
  entityElements.forEach((entityElement) => {
659
- entityElement.buildHierarchy(this.app);
855
+ entityElement.buildHierarchy(app);
660
856
  });
661
857
  this._hierarchyReady = true;
858
+ // Forward the engine's preload lifecycle as DOM ProgressEvents on this element. The
859
+ // listener must be attached before preload() is called: an asset that is already loaded
860
+ // ticks synchronously inside it.
861
+ const total = app.assets.list({ preload: true }).length;
862
+ let loaded = 0;
863
+ const onPreloadProgress = () => {
864
+ loaded += 1;
865
+ this._loadProgress = loaded / total;
866
+ this._bar?.progress(loaded, total);
867
+ this.dispatchEvent(new ProgressEvent('progress', { lengthComputable: true, loaded, total }));
868
+ };
869
+ app.on('preload:progress', onPreloadProgress);
870
+ this._loadProgress = total === 0 ? 1 : 0;
871
+ this._bar?.progress(0, total);
872
+ this.dispatchEvent(new ProgressEvent('progress', { lengthComputable: true, loaded: 0, total }));
662
873
  // Load assets before starting the application
663
- this.app.preload(() => {
874
+ app.preload(() => {
875
+ // Scope the counter to this preload pass, so a later app.preload() call by user code
876
+ // cannot push `loaded` past `total`
877
+ app.off('preload:progress', onPreloadProgress);
878
+ this._loadProgress = 1;
664
879
  // Start the application
665
- this.app.start();
880
+ app.start();
881
+ // Dismiss the bar only once a frame has actually rendered; ready fires before the
882
+ // first rAF tick
883
+ app.once('frameend', () => this._bar?.complete());
666
884
  // Handle window resize to keep the canvas responsive
667
885
  window.addEventListener('resize', this._onWindowResize);
668
886
  this._onReady();
@@ -671,10 +889,13 @@
671
889
  disconnectedCallback() {
672
890
  this._pickerDestroy();
673
891
  // Clean up the application
674
- if (this.app) {
675
- this.app.destroy();
892
+ if (this._app) {
893
+ this._app.destroy();
676
894
  this._app = null;
677
895
  }
896
+ this._loadProgress = 0;
897
+ this._bar?.destroy();
898
+ this._bar = null;
678
899
  // Remove event listeners
679
900
  window.removeEventListener('resize', this._onWindowResize);
680
901
  // Remove the canvas
@@ -691,10 +912,17 @@
691
912
  _pickerCreate() {
692
913
  const { width, height } = this.app.graphicsDevice;
693
914
  this._picker = new playcanvas.Picker(this.app, width, height);
694
- // Create bound handlers but don't attach them yet
695
- this._pointerHandlers.pointermove = this._onPointerMove.bind(this);
696
- this._pointerHandlers.pointerdown = this._onPointerDown.bind(this);
697
- this._pointerHandlers.pointerup = this._onPointerUp.bind(this);
915
+ // Create bound handlers but don't attach them yet. The handlers pick asynchronously, so
916
+ // each is wrapped to discard the promise - a listener must not return one, and nothing
917
+ // awaits the result.
918
+ const listener = (handler) => {
919
+ return (event) => {
920
+ handler.call(this, event);
921
+ };
922
+ };
923
+ this._pointerHandlers.pointermove = listener(this._onPointerMove);
924
+ this._pointerHandlers.pointerdown = listener(this._onPointerDown);
925
+ this._pointerHandlers.pointerup = listener(this._onPointerUp);
698
926
  // Listen for pointer listeners being added/removed
699
927
  ['pointermove', 'pointerdown', 'pointerup', 'pointerenter', 'pointerleave'].forEach((type) => {
700
928
  this.addEventListener(`${type}:connect`, () => this._onPointerListenerAdded(type));
@@ -742,29 +970,49 @@
742
970
  const y = (event.clientY - canvasRect.top) * scaleY;
743
971
  return { x, y };
744
972
  }
745
- _onPointerMove(event) {
746
- if (!this._picker || !this.app)
747
- return;
973
+ /**
974
+ * Picks the scene under the pointer and returns the graph node that was hit, or `null`.
975
+ *
976
+ * The read back is asynchronous because the synchronous {@link Picker.getSelection} is not
977
+ * supported on WebGPU, where it returns an empty selection rather than failing - which
978
+ * silently disabled every `onpointer*` handler once WebGPU became the resolved backend. The
979
+ * async variant works on both backends and does not block the main thread on a GPU read.
980
+ *
981
+ * @param event - The pointer event to pick under.
982
+ * @returns The graph node under the pointer, or `null` if nothing was hit.
983
+ */
984
+ async _pickNode(event) {
748
985
  const camera = this.app.root.findComponent('camera');
749
986
  if (!camera)
750
- return;
751
- // Use the helper to convert event coordinates into canvas/picker coordinates.
987
+ return null;
752
988
  const { x, y } = this._getPickerCoordinates(event);
753
989
  this._picker.prepare(camera, this.app.scene);
754
- const selection = this._picker.getSelection(x, y);
990
+ const selection = await this._picker.getSelectionAsync(x, y);
991
+ if (selection.length === 0)
992
+ return null;
993
+ const item = selection[0];
994
+ return item instanceof playcanvas.MeshInstance ? item.node : item.entity;
995
+ }
996
+ async _onPointerMove(event) {
997
+ if (!this._picker || !this.app)
998
+ return;
999
+ // Moves arrive faster than a pick resolves, so results can land out of order. Only the
1000
+ // newest pick may update the hover state - an older one describes a pointer position the
1001
+ // user has already left.
1002
+ const token = ++this._pickToken;
1003
+ const node = await this._pickNode(event);
1004
+ if (token !== this._pickToken || !this._picker)
1005
+ return;
755
1006
  // Get the currently hovered entity by walking up the hierarchy
756
1007
  let newHoverEntity = null;
757
- if (selection.length > 0) {
758
- const item = selection[0];
759
- let currentNode = item instanceof playcanvas.MeshInstance ? item.node : item.entity;
760
- while (currentNode !== null) {
761
- const entityElement = this.querySelector(`pc-entity[name="${currentNode.name}"]`);
762
- if (entityElement) {
763
- newHoverEntity = entityElement;
764
- break;
765
- }
766
- currentNode = currentNode.parent;
1008
+ let currentNode = node;
1009
+ while (currentNode !== null) {
1010
+ const entityElement = this.querySelector(`pc-entity[name="${currentNode.name}"]`);
1011
+ if (entityElement) {
1012
+ newHoverEntity = entityElement;
1013
+ break;
767
1014
  }
1015
+ currentNode = currentNode.parent;
768
1016
  }
769
1017
  // Handle enter/leave events
770
1018
  if (this._hoveredEntity !== newHoverEntity) {
@@ -782,46 +1030,30 @@
782
1030
  newHoverEntity.dispatchEvent(new PointerEvent('pointermove', event));
783
1031
  }
784
1032
  }
785
- _onPointerDown(event) {
1033
+ async _onPointerDown(event) {
786
1034
  if (!this._picker || !this.app)
787
1035
  return;
788
- const camera = this.app.root.findComponent('camera');
789
- if (!camera)
790
- return;
791
- // Convert the event's pointer coordinates
792
- const { x, y } = this._getPickerCoordinates(event);
793
- this._picker.prepare(camera, this.app.scene);
794
- const selection = this._picker.getSelection(x, y);
795
- if (selection.length > 0) {
796
- const item = selection[0];
797
- let currentNode = item instanceof playcanvas.MeshInstance ? item.node : item.entity;
798
- while (currentNode !== null) {
799
- const entityElement = this.querySelector(`pc-entity[name="${currentNode.name}"]`);
800
- if (entityElement && entityElement.hasListeners('pointerdown')) {
801
- entityElement.dispatchEvent(new PointerEvent('pointerdown', event));
802
- break;
803
- }
804
- currentNode = currentNode.parent;
1036
+ let currentNode = await this._pickNode(event);
1037
+ if (!this._picker)
1038
+ return; // the element disconnected while the pick was in flight
1039
+ while (currentNode !== null) {
1040
+ const entityElement = this.querySelector(`pc-entity[name="${currentNode.name}"]`);
1041
+ if (entityElement && entityElement.hasListeners('pointerdown')) {
1042
+ entityElement.dispatchEvent(new PointerEvent('pointerdown', event));
1043
+ break;
805
1044
  }
1045
+ currentNode = currentNode.parent;
806
1046
  }
807
1047
  }
808
- _onPointerUp(event) {
1048
+ async _onPointerUp(event) {
809
1049
  if (!this._picker || !this.app)
810
1050
  return;
811
- const camera = this.app.root.findComponent('camera');
812
- if (!camera)
1051
+ const node = await this._pickNode(event);
1052
+ if (!node || !this._picker)
813
1053
  return;
814
- // Convert CSS coordinates to picker coordinates
815
- const { x, y } = this._getPickerCoordinates(event);
816
- this._picker.prepare(camera, this.app.scene);
817
- const selection = this._picker.getSelection(x, y);
818
- if (selection.length > 0) {
819
- const item = selection[0];
820
- const node = item instanceof playcanvas.MeshInstance ? item.node : item.entity;
821
- const entityElement = this.querySelector(`pc-entity[name="${node.name}"]`);
822
- if (entityElement && entityElement.hasListeners('pointerup')) {
823
- entityElement.dispatchEvent(new PointerEvent('pointerup', event));
824
- }
1054
+ const entityElement = this.querySelector(`pc-entity[name="${node.name}"]`);
1055
+ if (entityElement && entityElement.hasListeners('pointerup')) {
1056
+ entityElement.dispatchEvent(new PointerEvent('pointerup', event));
825
1057
  }
826
1058
  }
827
1059
  _onPointerListenerAdded(type) {
@@ -878,7 +1110,8 @@
878
1110
  return this._antialias;
879
1111
  }
880
1112
  /**
881
- * Sets the graphics backend.
1113
+ * Sets the graphics backend. Defaults to 'webgpu', which falls back to 'webgl2' if WebGPU
1114
+ * is not supported by the browser.
882
1115
  * @param value - The graphics backend ('webgpu', 'webgl2', or 'null').
883
1116
  */
884
1117
  set backend(value) {
@@ -931,6 +1164,29 @@
931
1164
  get highResolution() {
932
1165
  return this._highResolution;
933
1166
  }
1167
+ /**
1168
+ * Sets whether the application shows its built-in loading bar while it boots and preloads its
1169
+ * assets. Enabled by default; setting `false` removes the bar immediately, while setting
1170
+ * `true` has no effect until the element is next connected. The bar can be themed with the
1171
+ * CSS custom properties `--pc-loading-bar-color`, `--pc-loading-bar-background` and
1172
+ * `--pc-loading-bar-height`.
1173
+ * @param value - The loading bar flag.
1174
+ */
1175
+ set loadingBar(value) {
1176
+ this._loadingBar = value;
1177
+ if (!value && this._bar) {
1178
+ this._bar.destroy();
1179
+ this._bar = null;
1180
+ }
1181
+ }
1182
+ /**
1183
+ * Gets whether the application shows its built-in loading bar while it boots and preloads
1184
+ * its assets.
1185
+ * @returns The loading bar flag.
1186
+ */
1187
+ get loadingBar() {
1188
+ return this._loadingBar;
1189
+ }
934
1190
  /**
935
1191
  * Sets the stencil flag.
936
1192
  * @param value - The stencil flag.
@@ -946,7 +1202,7 @@
946
1202
  return this._stencil;
947
1203
  }
948
1204
  static get observedAttributes() {
949
- return ['alpha', 'antialias', 'backend', 'depth', 'stencil', 'high-resolution'];
1205
+ return ['alpha', 'antialias', 'backend', 'depth', 'stencil', 'high-resolution', 'loading-bar'];
950
1206
  }
951
1207
  attributeChangedCallback(name, _oldValue, newValue) {
952
1208
  switch (name) {
@@ -957,7 +1213,7 @@
957
1213
  this.antialias = parseBool(newValue, true);
958
1214
  break;
959
1215
  case 'backend':
960
- this.backend = parseEnum(newValue, ['webgpu', 'webgl2', 'null'], 'webgl2', name);
1216
+ this.backend = parseEnum(newValue, ['webgpu', 'webgl2', 'null'], 'webgpu', name);
961
1217
  break;
962
1218
  case 'depth':
963
1219
  this.depth = parseBool(newValue, true);
@@ -965,6 +1221,9 @@
965
1221
  case 'high-resolution':
966
1222
  this.highResolution = parseBool(newValue, true);
967
1223
  break;
1224
+ case 'loading-bar':
1225
+ this.loadingBar = parseBool(newValue, true);
1226
+ break;
968
1227
  case 'stencil':
969
1228
  this.stencil = parseBool(newValue, true);
970
1229
  break;
@@ -978,52 +1237,68 @@
978
1237
  * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-entity/ | `<pc-entity>`} elements.
979
1238
  * The EntityElement interface also inherits the properties and methods of the
980
1239
  * {@link HTMLElement} interface.
1240
+ *
1241
+ * The pointer events below are dispatched by the containing `<pc-app>` element when the pointer
1242
+ * intersects this entity's geometry. They are only generated while the entity has a listener for
1243
+ * them, registered either with {@link addEventListener} or with the matching inline `onpointer*`
1244
+ * attribute.
1245
+ *
1246
+ * @attribute {string} onpointerenter - Script to run when the pointer moves onto the entity.
1247
+ * @attribute {string} onpointerleave - Script to run when the pointer moves off the entity.
1248
+ * @attribute {string} onpointermove - Script to run when the pointer moves over the entity.
1249
+ * @attribute {string} onpointerdown - Script to run when a pointer button is pressed over the
1250
+ * entity.
1251
+ * @attribute {string} onpointerup - Script to run when a pointer button is released over the
1252
+ * entity.
1253
+ * @fires {PointerEvent} pointerenter - Fired when the pointer moves onto the entity.
1254
+ * @fires {PointerEvent} pointerleave - Fired when the pointer moves off the entity.
1255
+ * @fires {PointerEvent} pointermove - Fired when the pointer moves over the entity.
1256
+ * @fires {PointerEvent} pointerdown - Fired when a pointer button is pressed over the entity.
1257
+ * @fires {PointerEvent} pointerup - Fired when a pointer button is released over the entity.
981
1258
  */
982
1259
  class EntityElement extends AsyncElement {
983
- constructor() {
984
- super(...arguments);
985
- /**
986
- * Whether the entity is enabled.
987
- */
988
- this._enabled = true;
989
- /**
990
- * The name of the entity.
991
- */
992
- this._name = 'Untitled';
993
- /**
994
- * The position of the entity.
995
- */
996
- this._position = new playcanvas.Vec3();
997
- /**
998
- * The rotation of the entity.
999
- */
1000
- this._rotation = new playcanvas.Vec3();
1001
- /**
1002
- * The scale of the entity.
1003
- */
1004
- this._scale = new playcanvas.Vec3(1, 1, 1);
1005
- /**
1006
- * The tags of the entity.
1007
- */
1008
- this._tags = [];
1009
- /**
1010
- * The pointer event listeners for the entity.
1011
- */
1012
- this._listeners = {};
1013
- /**
1014
- * The event types for which an inline `onpointer*` attribute is currently present.
1015
- */
1016
- this._inlineHandlerTypes = new Set();
1017
- /**
1018
- * Whether the hierarchy has been built for this entity.
1019
- */
1020
- this._built = false;
1021
- this._entity = null;
1022
- }
1023
1260
  /**
1024
- * The PlayCanvas entity instance. Available once the element is ready — await
1025
- * {@link whenReady} or the element's `ready()` promise before accessing it.
1026
- * @returns The entity instance.
1261
+ * Whether the entity is enabled.
1262
+ */
1263
+ _enabled = true;
1264
+ /**
1265
+ * The name of the entity.
1266
+ */
1267
+ _name = 'Untitled';
1268
+ /**
1269
+ * The position of the entity.
1270
+ */
1271
+ _position = new playcanvas.Vec3();
1272
+ /**
1273
+ * The rotation of the entity.
1274
+ */
1275
+ _rotation = new playcanvas.Vec3();
1276
+ /**
1277
+ * The scale of the entity.
1278
+ */
1279
+ _scale = new playcanvas.Vec3(1, 1, 1);
1280
+ /**
1281
+ * The tags of the entity.
1282
+ */
1283
+ _tags = [];
1284
+ /**
1285
+ * The pointer event listeners for the entity.
1286
+ */
1287
+ _listeners = {};
1288
+ /**
1289
+ * The event types for which an inline `onpointer*` attribute is currently present.
1290
+ */
1291
+ _inlineHandlerTypes = new Set();
1292
+ /**
1293
+ * Whether the hierarchy has been built for this entity.
1294
+ */
1295
+ _built = false;
1296
+ _entity = null;
1297
+ /**
1298
+ * The PlayCanvas entity instance. `null` until the element is ready, and again once it has
1299
+ * been removed from the document — await {@link whenReady} or the element's `ready()`
1300
+ * promise before accessing it.
1301
+ * @returns The entity instance, or `null`.
1027
1302
  */
1028
1303
  get entity() {
1029
1304
  return this._entity;
@@ -1035,16 +1310,18 @@
1035
1310
  if (this._entity) {
1036
1311
  return;
1037
1312
  }
1038
- // Create a new entity
1039
- const entity = new playcanvas.Entity(this.getAttribute('name') || this._name, app);
1313
+ // Seed from the cached fields rather than re-reading the attributes. Every observed
1314
+ // attribute is routed through its property setter by attributeChangedCallback, so the field
1315
+ // already holds the parsed attribute value - and it also holds anything assigned through the
1316
+ // property API before the app booted, which reading the attribute back would discard.
1317
+ const entity = new playcanvas.Entity(this._name, app);
1040
1318
  this._entity = entity;
1041
- entity.enabled = parseBool(this.getAttribute('enabled'), true);
1042
- entity.setLocalPosition(parseVec3(this.getAttribute('position'), playcanvas.Vec3.ZERO, 'position'));
1043
- entity.setLocalEulerAngles(parseVec3(this.getAttribute('rotation'), playcanvas.Vec3.ZERO, 'rotation'));
1044
- entity.setLocalScale(parseVec3(this.getAttribute('scale'), playcanvas.Vec3.ONE, 'scale'));
1045
- const tags = this.getAttribute('tags');
1046
- if (tags) {
1047
- entity.tags.add(tags.split(',').map(tag => tag.trim()));
1319
+ entity.enabled = this._enabled;
1320
+ entity.setLocalPosition(this._position);
1321
+ entity.setLocalEulerAngles(this._rotation);
1322
+ entity.setLocalScale(this._scale);
1323
+ if (this._tags.length > 0) {
1324
+ entity.tags.add(this._tags);
1048
1325
  }
1049
1326
  }
1050
1327
  buildHierarchy(app) {
@@ -1052,7 +1329,7 @@
1052
1329
  return;
1053
1330
  this._built = true;
1054
1331
  const closestEntity = this.closestEntity;
1055
- if (closestEntity === null || closestEntity === void 0 ? void 0 : closestEntity.entity) {
1332
+ if (closestEntity?.entity) {
1056
1333
  closestEntity.entity.addChild(this.entity);
1057
1334
  }
1058
1335
  else {
@@ -1063,8 +1340,15 @@
1063
1340
  connectedCallback() {
1064
1341
  // Wait for app to be ready
1065
1342
  const closestApp = this.closestApp;
1066
- if (!closestApp)
1343
+ if (!closestApp) {
1344
+ // An entity outside an application is inert and never becomes ready, so awaiting it
1345
+ // hangs. Warn rather than fail silently, naming the parent it requires, as every other
1346
+ // misplaced element does.
1347
+ const name = this.getAttribute('name');
1348
+ const label = name ? ` '${name}'` : '';
1349
+ console.warn(`pc-entity${label} must be a descendant of pc-app - entity not created`);
1067
1350
  return;
1351
+ }
1068
1352
  // If app is already running, create entity immediately
1069
1353
  if (closestApp.hierarchyReady) {
1070
1354
  const app = closestApp.app;
@@ -1082,10 +1366,15 @@
1082
1366
  }
1083
1367
  disconnectedCallback() {
1084
1368
  if (this.entity) {
1085
- // Notify all children that their entities are about to become invalid
1369
+ // Notify all children that their entities are about to become invalid. Both fields have
1370
+ // to be reset here, not just _entity: a descendant's own disconnectedCallback runs after
1371
+ // this one and skips its reset behind the `if (this.entity)` guard, because we have
1372
+ // already nulled the entity it tests. Leaving _built set would make buildHierarchy bail
1373
+ // on re-insertion, so the descendant would get a fresh entity that is never parented.
1086
1374
  const children = this.querySelectorAll('pc-entity');
1087
1375
  children.forEach((child) => {
1088
1376
  child._entity = null;
1377
+ child._built = false;
1089
1378
  });
1090
1379
  // Destroy the entity
1091
1380
  this.entity.destroy();
@@ -1241,7 +1530,7 @@
1241
1530
  this.enabled = parseBool(newValue, true);
1242
1531
  break;
1243
1532
  case 'name':
1244
- this.name = newValue;
1533
+ this.name = newValue ?? 'Untitled';
1245
1534
  break;
1246
1535
  case 'position':
1247
1536
  this.position = parseVec3(newValue, playcanvas.Vec3.ZERO, name);
@@ -1253,7 +1542,7 @@
1253
1542
  this.scale = parseVec3(newValue, playcanvas.Vec3.ONE, name);
1254
1543
  break;
1255
1544
  case 'tags':
1256
- this.tags = newValue.split(',').map(tag => tag.trim());
1545
+ this.tags = parseTags(newValue);
1257
1546
  break;
1258
1547
  case 'onpointerenter':
1259
1548
  case 'onpointerleave':
@@ -1284,8 +1573,7 @@
1284
1573
  }
1285
1574
  }
1286
1575
  hasListeners(type) {
1287
- var _a;
1288
- return Boolean((_a = this._listeners[type]) === null || _a === void 0 ? void 0 : _a.length) || this._inlineHandlerTypes.has(type);
1576
+ return Boolean(this._listeners[type]?.length) || this._inlineHandlerTypes.has(type);
1289
1577
  }
1290
1578
  }
1291
1579
  customElements.define('pc-entity', EntityElement);
@@ -1477,11 +1765,13 @@
1477
1765
  ['frag', 'shader'],
1478
1766
  ['glb', 'container'],
1479
1767
  ['glsl', 'shader'],
1768
+ ['gltf', 'container'],
1480
1769
  ['hdr', 'texture'],
1481
1770
  ['html', 'html'],
1482
1771
  ['jpg', 'texture'],
1483
1772
  ['js', 'script'],
1484
1773
  ['json', 'json'],
1774
+ ['ktx2', 'texture'],
1485
1775
  ['mp3', 'audio'],
1486
1776
  ['mjs', 'script'],
1487
1777
  ['ply', 'gsplat'],
@@ -1524,25 +1814,43 @@
1524
1814
  * while the application is running are created and registered on insertion, and begin loading
1525
1815
  * immediately unless `lazy`. A `pc-asset` must be a direct child of `pc-app` — elements placed
1526
1816
  * elsewhere, or with an unsupported asset type, never become ready.
1817
+ *
1818
+ * Apart from `lazy`, these attributes are read once when the asset is created, so changing them
1819
+ * later has no effect.
1820
+ *
1821
+ * @attribute {string} id - The identifier used to reference the asset from other elements.
1822
+ * @attribute {string} src - The URL of the asset to load.
1823
+ * @attribute {string} type - The asset type. Inferred from the `src` file extension when omitted.
1824
+ * @attribute {string} data - Additional asset data, as a JSON object.
1825
+ * @attribute {string} atlas - For a `sprite` asset, the `id` of the texture atlas asset it uses.
1826
+ * The atlas must be declared before the sprite.
1827
+ * @attribute {string} frame-keys - For a `sprite` asset, the atlas frame keys it uses, separated
1828
+ * by spaces or commas.
1829
+ * @attribute {number} pixels-per-unit - For a `sprite` asset, the number of pixels per world unit.
1830
+ * @attribute {'simple' | 'sliced' | 'tiled'} render-mode - For a `sprite` asset, how the sprite is
1831
+ * rendered when resized.
1832
+ *
1833
+ * @fires {Event} load - Fired each time the asset finishes loading, including a `lazy` asset
1834
+ * loaded later and any subsequent reloads. Does not bubble — listen on this element, or use a
1835
+ * capture-phase listener on an ancestor to observe every asset.
1836
+ * @fires {ErrorEvent} error - Fired when the asset fails to load, with the engine's error in
1837
+ * `message`. Does not bubble. The element still becomes ready — readiness means the load settled,
1838
+ * not that it succeeded.
1527
1839
  */
1528
1840
  class AssetElement extends AsyncElement {
1529
- constructor() {
1530
- super(...arguments);
1531
- this._lazy = false;
1532
- /**
1533
- * The asset that is loaded. Available once the element is ready — await
1534
- * {@link whenReady} or the element's `ready()` promise before accessing it.
1535
- */
1536
- this.asset = null;
1537
- }
1841
+ _lazy = false;
1842
+ /**
1843
+ * The asset that is loaded. Available once the element is ready — await
1844
+ * {@link whenReady} or the element's `ready()` promise before accessing it.
1845
+ */
1846
+ asset = null;
1538
1847
  async connectedCallback() {
1539
- var _a;
1540
1848
  const appElement = this.closestApp;
1541
1849
  if (!appElement)
1542
1850
  return;
1543
1851
  // Assets must be direct children of pc-app (matches the boot query ':scope > pc-asset')
1544
1852
  if (this.parentElement !== appElement) {
1545
- console.warn(`pc-asset '${(_a = this.getAttribute('id')) !== null && _a !== void 0 ? _a : this.getAttribute('src')}' must be a direct child of pc-app - asset not created`);
1853
+ console.warn(`pc-asset '${this.getAttribute('id') ?? this.getAttribute('src')}' must be a direct child of pc-app - asset not created`);
1546
1854
  return;
1547
1855
  }
1548
1856
  await appElement.ready();
@@ -1571,15 +1879,22 @@
1571
1879
  disconnectedCallback() {
1572
1880
  this.destroyAsset();
1573
1881
  }
1882
+ _onAssetLoad() {
1883
+ this.dispatchEvent(new Event('load'));
1884
+ }
1885
+ _onAssetError(err) {
1886
+ this.dispatchEvent(new ErrorEvent('error', {
1887
+ message: err instanceof Error ? err.message : String(err)
1888
+ }));
1889
+ }
1574
1890
  createAsset() {
1575
- var _a;
1576
1891
  const id = this.getAttribute('id') || '';
1577
1892
  const src = this.getAttribute('src') || '';
1578
1893
  let type = this.getAttribute('type');
1579
1894
  // If no type is specified, try to infer it from the file extension.
1580
1895
  if (!type) {
1581
1896
  const ext = src.split('.').pop();
1582
- type = (_a = extToType.get(ext || '')) !== null && _a !== void 0 ? _a : null;
1897
+ type = extToType.get(ext || '') ?? null;
1583
1898
  }
1584
1899
  if (!type) {
1585
1900
  console.warn(`Unsupported asset type: ${src}`);
@@ -1606,6 +1921,10 @@
1606
1921
  this.asset = new playcanvas.Asset(id, type, src ? { url: src } : null, data);
1607
1922
  }
1608
1923
  this.asset.preload = !this._lazy;
1924
+ // Forward the engine asset's load outcome as DOM events on this element, like <img>.
1925
+ // Attached before the asset joins the registry, which is what starts a preloaded load.
1926
+ this.asset.on('load', this._onAssetLoad, this);
1927
+ this.asset.on('error', this._onAssetError, this);
1609
1928
  }
1610
1929
  /**
1611
1930
  * Builds the `data` object for the asset from an optional inline `data` attribute (JSON) and,
@@ -1615,7 +1934,6 @@
1615
1934
  * @returns The asset data, or `undefined`.
1616
1935
  */
1617
1936
  _buildData(type) {
1618
- var _a, _b, _c, _d;
1619
1937
  let data;
1620
1938
  const dataAttr = this.getAttribute('data');
1621
1939
  if (dataAttr) {
@@ -1627,10 +1945,10 @@
1627
1945
  }
1628
1946
  }
1629
1947
  if (type === 'sprite') {
1630
- data = data !== null && data !== void 0 ? data : {};
1948
+ data = data ?? {};
1631
1949
  // Resolve the referenced texture atlas to its (numeric) asset id. The atlas must be
1632
1950
  // declared before the sprite so its asset already exists in the registry.
1633
- const atlas = (_a = this.getAttribute('atlas')) !== null && _a !== void 0 ? _a : data.textureAtlasAsset;
1951
+ const atlas = this.getAttribute('atlas') ?? data.textureAtlasAsset;
1634
1952
  if (typeof atlas === 'string') {
1635
1953
  const atlasAsset = AssetElement.get(atlas);
1636
1954
  if (atlasAsset) {
@@ -1653,17 +1971,19 @@
1653
1971
  data.renderMode = renderModes.get(parseEnum(renderMode, renderModes, 'simple', 'render-mode'));
1654
1972
  }
1655
1973
  // Apply engine defaults for any values not supplied.
1656
- data.renderMode = (_b = data.renderMode) !== null && _b !== void 0 ? _b : playcanvas.SPRITE_RENDERMODE_SIMPLE;
1657
- data.pixelsPerUnit = (_c = data.pixelsPerUnit) !== null && _c !== void 0 ? _c : 1;
1658
- data.frameKeys = (_d = data.frameKeys) !== null && _d !== void 0 ? _d : [];
1974
+ data.renderMode = data.renderMode ?? playcanvas.SPRITE_RENDERMODE_SIMPLE;
1975
+ data.pixelsPerUnit = data.pixelsPerUnit ?? 1;
1976
+ data.frameKeys = data.frameKeys ?? [];
1659
1977
  }
1660
1978
  return data;
1661
1979
  }
1662
1980
  destroyAsset() {
1663
- var _a;
1664
1981
  if (this.asset) {
1982
+ // A caller that keeps the Asset alive must not dispatch on a removed element
1983
+ this.asset.off('load', this._onAssetLoad, this);
1984
+ this.asset.off('error', this._onAssetError, this);
1665
1985
  // Deregister first so unload() can still notify the registry
1666
- (_a = this.asset.registry) === null || _a === void 0 ? void 0 : _a.remove(this.asset);
1986
+ this.asset.registry?.remove(this.asset);
1667
1987
  this.asset.unload();
1668
1988
  this.asset = null;
1669
1989
  }
@@ -1687,7 +2007,7 @@
1687
2007
  }
1688
2008
  static get(id) {
1689
2009
  const assetElement = document.querySelector(`pc-asset[id="${id}"]`);
1690
- return assetElement === null || assetElement === void 0 ? void 0 : assetElement.asset;
2010
+ return assetElement?.asset;
1691
2011
  }
1692
2012
  static get observedAttributes() {
1693
2013
  return ['lazy'];
@@ -1706,6 +2026,10 @@
1706
2026
  * @category Components
1707
2027
  */
1708
2028
  class ComponentElement extends AsyncElement {
2029
+ _componentName;
2030
+ _enabled = true;
2031
+ _component = null;
2032
+ _appElement = null;
1709
2033
  /**
1710
2034
  * Creates a new ComponentElement instance.
1711
2035
  *
@@ -1714,9 +2038,6 @@
1714
2038
  */
1715
2039
  constructor(componentName) {
1716
2040
  super();
1717
- this._enabled = true;
1718
- this._component = null;
1719
- this._appElement = null;
1720
2041
  this._componentName = componentName;
1721
2042
  }
1722
2043
  // Method to be overridden by subclasses to provide initial component data
@@ -1739,28 +2060,27 @@
1739
2060
  }
1740
2061
  initComponent() { }
1741
2062
  async connectedCallback() {
1742
- var _a, _b;
1743
- this._appElement = (_a = this.closestApp) !== null && _a !== void 0 ? _a : null;
1744
- await ((_b = this._appElement) === null || _b === void 0 ? void 0 : _b.ready());
2063
+ this._appElement = this.closestApp ?? null;
2064
+ await this._appElement?.ready();
1745
2065
  await this.addComponent();
1746
2066
  this.initComponent();
1747
2067
  this._onReady();
1748
2068
  }
1749
2069
  disconnectedCallback() {
1750
- var _a, _b;
1751
2070
  // Remove the component when the element is disconnected. Skip this when the owning
1752
2071
  // application has already been destroyed — removing a <pc-app> disconnects it before
1753
2072
  // its children, taking the component systems with it.
1754
- if (((_a = this._appElement) === null || _a === void 0 ? void 0 : _a.app) && ((_b = this._component) === null || _b === void 0 ? void 0 : _b.entity)) {
2073
+ if (this._appElement?.app && this._component?.entity) {
1755
2074
  this._component.entity.removeComponent(this._componentName);
1756
2075
  }
1757
2076
  this._component = null;
1758
2077
  this._appElement = null;
1759
2078
  }
1760
2079
  /**
1761
- * The PlayCanvas component instance. Available once the element is ready await
1762
- * {@link whenReady} or the element's `ready()` promise before accessing it.
1763
- * @returns The component instance.
2080
+ * The PlayCanvas component instance. `null` until the element is ready, and also for an
2081
+ * element that is not a descendant of a `<pc-entity>` — await {@link whenReady} or the
2082
+ * element's `ready()` promise before accessing it.
2083
+ * @returns The component instance, or `null`.
1764
2084
  */
1765
2085
  get component() {
1766
2086
  return this._component;
@@ -1830,26 +2150,25 @@
1830
2150
  * @category Components
1831
2151
  */
1832
2152
  class ButtonComponentElement extends ComponentElement {
2153
+ _active = true;
2154
+ _image = '';
2155
+ _hitPadding = new playcanvas.Vec4(0, 0, 0, 0);
2156
+ _transitionMode = 'tint';
2157
+ _hoverTint = new playcanvas.Color(1, 1, 1, 1);
2158
+ _pressedTint = new playcanvas.Color(1, 1, 1, 1);
2159
+ _inactiveTint = new playcanvas.Color(1, 1, 1, 1);
2160
+ _fadeDuration = 0;
2161
+ _hoverSpriteAsset = '';
2162
+ _hoverSpriteFrame = 0;
2163
+ _pressedSpriteAsset = '';
2164
+ _pressedSpriteFrame = 0;
2165
+ _inactiveSpriteAsset = '';
2166
+ _inactiveSpriteFrame = 0;
1833
2167
  /** @ignore */
1834
2168
  constructor() {
1835
2169
  super('button');
1836
- this._active = true;
1837
- this._image = '';
1838
- this._hitPadding = new playcanvas.Vec4(0, 0, 0, 0);
1839
- this._transitionMode = 'tint';
1840
- this._hoverTint = new playcanvas.Color(1, 1, 1, 1);
1841
- this._pressedTint = new playcanvas.Color(1, 1, 1, 1);
1842
- this._inactiveTint = new playcanvas.Color(1, 1, 1, 1);
1843
- this._fadeDuration = 0;
1844
- this._hoverSpriteAsset = '';
1845
- this._hoverSpriteFrame = 0;
1846
- this._pressedSpriteAsset = '';
1847
- this._pressedSpriteFrame = 0;
1848
- this._inactiveSpriteAsset = '';
1849
- this._inactiveSpriteFrame = 0;
1850
2170
  }
1851
2171
  getInitialComponentData() {
1852
- var _a;
1853
2172
  const data = {
1854
2173
  active: this._active,
1855
2174
  hitPadding: this._hitPadding,
@@ -1864,7 +2183,7 @@
1864
2183
  };
1865
2184
  // The image entity defaults to the button's own entity (which carries the image element)
1866
2185
  // when no explicit reference is provided.
1867
- const imageEntity = this._image ? getEntity(this._image) : (_a = this.closestEntity) === null || _a === void 0 ? void 0 : _a.entity;
2186
+ const imageEntity = this._image ? getEntity(this._image) : this.closestEntity?.entity;
1868
2187
  if (imageEntity) {
1869
2188
  data.imageEntity = imageEntity;
1870
2189
  }
@@ -1948,10 +2267,9 @@
1948
2267
  * @param value - The transition mode.
1949
2268
  */
1950
2269
  set transitionMode(value) {
1951
- var _a;
1952
2270
  this._transitionMode = value;
1953
2271
  if (this.component) {
1954
- this.component.transitionMode = (_a = transitionModes.get(value)) !== null && _a !== void 0 ? _a : playcanvas.BUTTON_TRANSITION_MODE_TINT;
2272
+ this.component.transitionMode = transitionModes.get(value) ?? playcanvas.BUTTON_TRANSITION_MODE_TINT;
1955
2273
  }
1956
2274
  }
1957
2275
  /**
@@ -2166,7 +2484,7 @@
2166
2484
  this.active = parseBool(newValue, true);
2167
2485
  break;
2168
2486
  case 'image':
2169
- this.image = newValue;
2487
+ this.image = newValue ?? '';
2170
2488
  break;
2171
2489
  case 'hit-padding':
2172
2490
  this.hitPadding = parseVec4(newValue, playcanvas.Vec4.ZERO, name);
@@ -2187,19 +2505,19 @@
2187
2505
  this.fadeDuration = parseNumber(newValue, 0, name);
2188
2506
  break;
2189
2507
  case 'hover-sprite-asset':
2190
- this.hoverSpriteAsset = newValue;
2508
+ this.hoverSpriteAsset = newValue ?? '';
2191
2509
  break;
2192
2510
  case 'hover-sprite-frame':
2193
2511
  this.hoverSpriteFrame = parseNumber(newValue, 0, name);
2194
2512
  break;
2195
2513
  case 'pressed-sprite-asset':
2196
- this.pressedSpriteAsset = newValue;
2514
+ this.pressedSpriteAsset = newValue ?? '';
2197
2515
  break;
2198
2516
  case 'pressed-sprite-frame':
2199
2517
  this.pressedSpriteFrame = parseNumber(newValue, 0, name);
2200
2518
  break;
2201
2519
  case 'inactive-sprite-asset':
2202
- this.inactiveSpriteAsset = newValue;
2520
+ this.inactiveSpriteAsset = newValue ?? '';
2203
2521
  break;
2204
2522
  case 'inactive-sprite-frame':
2205
2523
  this.inactiveSpriteFrame = parseNumber(newValue, 0, name);
@@ -2227,27 +2545,27 @@
2227
2545
  * @category Components
2228
2546
  */
2229
2547
  class CameraComponentElement extends ComponentElement {
2548
+ _clearColor = new playcanvas.Color(0.75, 0.75, 0.75, 1);
2549
+ _clearColorBuffer = true;
2550
+ _clearDepthBuffer = true;
2551
+ _clearStencilBuffer = false;
2552
+ _cullFaces = true;
2553
+ _farClip = 1000;
2554
+ _flipFaces = false;
2555
+ _fov = 45;
2556
+ _frustumCulling = true;
2557
+ _gamma = 'srgb';
2558
+ _horizontalFov = false;
2559
+ _nearClip = 0.1;
2560
+ _orthographic = false;
2561
+ _orthoHeight = 10;
2562
+ _priority = 0;
2563
+ _rect = new playcanvas.Vec4(0, 0, 1, 1);
2564
+ _scissorRect = new playcanvas.Vec4(0, 0, 1, 1);
2565
+ _tonemap = 'none';
2230
2566
  /** @ignore */
2231
2567
  constructor() {
2232
2568
  super('camera');
2233
- this._clearColor = new playcanvas.Color(0.75, 0.75, 0.75, 1);
2234
- this._clearColorBuffer = true;
2235
- this._clearDepthBuffer = true;
2236
- this._clearStencilBuffer = false;
2237
- this._cullFaces = true;
2238
- this._farClip = 1000;
2239
- this._flipFaces = false;
2240
- this._fov = 45;
2241
- this._frustumCulling = true;
2242
- this._gamma = 'srgb';
2243
- this._horizontalFov = false;
2244
- this._nearClip = 0.1;
2245
- this._orthographic = false;
2246
- this._orthoHeight = 10;
2247
- this._priority = 0;
2248
- this._rect = new playcanvas.Vec4(0, 0, 1, 1);
2249
- this._scissorRect = new playcanvas.Vec4(0, 0, 1, 1);
2250
- this._tonemap = 'none';
2251
2569
  }
2252
2570
  getInitialComponentData() {
2253
2571
  return {
@@ -2272,8 +2590,7 @@
2272
2590
  };
2273
2591
  }
2274
2592
  get xrAvailable() {
2275
- var _a;
2276
- const xrManager = (_a = this.component) === null || _a === void 0 ? void 0 : _a.system.app.xr;
2593
+ const xrManager = this.component?.system.app.xr;
2277
2594
  return xrManager && xrManager.supported && xrManager.isAvailable(playcanvas.XRTYPE_VR);
2278
2595
  }
2279
2596
  /**
@@ -2601,10 +2918,9 @@
2601
2918
  * @param value - The tone mapping.
2602
2919
  */
2603
2920
  set tonemap(value) {
2604
- var _a;
2605
2921
  this._tonemap = value;
2606
2922
  if (this.component) {
2607
- this.component.toneMapping = (_a = tonemaps.get(value)) !== null && _a !== void 0 ? _a : playcanvas.TONEMAP_NONE;
2923
+ this.component.toneMapping = tonemaps.get(value) ?? playcanvas.TONEMAP_NONE;
2608
2924
  }
2609
2925
  }
2610
2926
  /**
@@ -2708,17 +3024,17 @@
2708
3024
  * @category Components
2709
3025
  */
2710
3026
  class CollisionComponentElement extends ComponentElement {
3027
+ _angularOffset = new playcanvas.Quat();
3028
+ _axis = 1;
3029
+ _convexHull = false;
3030
+ _halfExtents = new playcanvas.Vec3(0.5, 0.5, 0.5);
3031
+ _height = 2;
3032
+ _linearOffset = new playcanvas.Vec3();
3033
+ _radius = 0.5;
3034
+ _type = 'box';
2711
3035
  /** @ignore */
2712
3036
  constructor() {
2713
3037
  super('collision');
2714
- this._angularOffset = new playcanvas.Quat();
2715
- this._axis = 1;
2716
- this._convexHull = false;
2717
- this._halfExtents = new playcanvas.Vec3(0.5, 0.5, 0.5);
2718
- this._height = 2;
2719
- this._linearOffset = new playcanvas.Vec3();
2720
- this._radius = 0.5;
2721
- this._type = 'box';
2722
3038
  }
2723
3039
  getInitialComponentData() {
2724
3040
  return {
@@ -2855,52 +3171,51 @@
2855
3171
  * @category Components
2856
3172
  */
2857
3173
  class ElementComponentElement extends ComponentElement {
3174
+ _anchor = new playcanvas.Vec4(0.5, 0.5, 0.5, 0.5);
3175
+ _autoWidth = true;
3176
+ _autoHeight = true;
3177
+ _autoFitWidth = false;
3178
+ _autoFitHeight = false;
3179
+ _color = new playcanvas.Color(1, 1, 1, 1);
3180
+ _enableMarkup = false;
3181
+ _fontAsset = '';
3182
+ _fontSize = 32;
3183
+ _maxFontSize = 32;
3184
+ _minFontSize = 8;
3185
+ _height = 0;
3186
+ _lineHeight = 32;
3187
+ _margin = null;
3188
+ _mask = false;
3189
+ _opacity = 1;
3190
+ _pivot = new playcanvas.Vec2(0.5, 0.5);
3191
+ _pixelsPerUnit = null;
3192
+ _spriteAsset = '';
3193
+ _spriteFrame = 0;
3194
+ _text = '';
3195
+ _textureAsset = '';
3196
+ _type = 'group';
3197
+ _useInput = false;
3198
+ _width = 0;
3199
+ _wrapLines = false;
2858
3200
  /** @ignore */
2859
3201
  constructor() {
2860
3202
  super('element');
2861
- this._anchor = new playcanvas.Vec4(0.5, 0.5, 0.5, 0.5);
2862
- this._autoWidth = true;
2863
- this._autoHeight = true;
2864
- this._autoFitWidth = false;
2865
- this._autoFitHeight = false;
2866
- this._color = new playcanvas.Color(1, 1, 1, 1);
2867
- this._enableMarkup = false;
2868
- this._fontAsset = '';
2869
- this._fontSize = 32;
2870
- this._maxFontSize = 32;
2871
- this._minFontSize = 8;
2872
- this._height = 0;
2873
- this._lineHeight = 32;
2874
- this._margin = null;
2875
- this._mask = false;
2876
- this._opacity = 1;
2877
- this._pivot = new playcanvas.Vec2(0.5, 0.5);
2878
- this._pixelsPerUnit = null;
2879
- this._spriteAsset = '';
2880
- this._spriteFrame = 0;
2881
- this._text = '';
2882
- this._textureAsset = '';
2883
- this._type = 'group';
2884
- this._useInput = false;
2885
- this._width = 0;
2886
- this._wrapLines = false;
2887
3203
  }
2888
3204
  initComponent() {
2889
- var _a, _b;
2890
3205
  const component = this.component;
2891
3206
  if (!component) {
2892
3207
  return;
2893
3208
  }
2894
3209
  // Text elements render through their own material; enable fog on it so 3D text respects
2895
3210
  // scene fog. Image/group elements have no text material, so guard the access.
2896
- if ((_a = component._text) === null || _a === void 0 ? void 0 : _a._material) {
3211
+ if (component._text?._material) {
2897
3212
  component._text._material.useFog = true;
2898
3213
  }
2899
3214
  // The engine establishes element masking in ElementComponent._onInsert, which fires when an
2900
3215
  // entity is inserted into the hierarchy. Web-components inserts the entity first and adds
2901
3216
  // the element component afterwards, so that pass is missed. Re-dirty the mask state here so
2902
3217
  // masks (e.g. a scroll view viewport) correctly clip this element and any added at runtime.
2903
- (_b = component._dirtifyMask) === null || _b === void 0 ? void 0 : _b.call(component);
3218
+ component._dirtifyMask?.();
2904
3219
  }
2905
3220
  getInitialComponentData() {
2906
3221
  const data = {
@@ -3463,7 +3778,7 @@
3463
3778
  this.enableMarkup = parseBool(newValue, false);
3464
3779
  break;
3465
3780
  case 'font-asset':
3466
- this.fontAsset = newValue;
3781
+ this.fontAsset = newValue ?? '';
3467
3782
  break;
3468
3783
  case 'font-size':
3469
3784
  this.fontSize = parseNumber(newValue, 32, name);
@@ -3496,16 +3811,16 @@
3496
3811
  this.pixelsPerUnit = parseNumber(newValue, null, name);
3497
3812
  break;
3498
3813
  case 'sprite-asset':
3499
- this.spriteAsset = newValue;
3814
+ this.spriteAsset = newValue ?? '';
3500
3815
  break;
3501
3816
  case 'sprite-frame':
3502
3817
  this.spriteFrame = parseNumber(newValue, 0, name);
3503
3818
  break;
3504
3819
  case 'text':
3505
- this.text = newValue;
3820
+ this.text = newValue ?? '';
3506
3821
  break;
3507
3822
  case 'texture-asset':
3508
- this.textureAsset = newValue;
3823
+ this.textureAsset = newValue ?? '';
3509
3824
  break;
3510
3825
  case 'type':
3511
3826
  this.type = parseEnum(newValue, ['group', 'image', 'text'], 'group', name);
@@ -3533,16 +3848,16 @@
3533
3848
  * @category Components
3534
3849
  */
3535
3850
  class LayoutChildComponentElement extends ComponentElement {
3851
+ _minWidth = 0;
3852
+ _minHeight = 0;
3853
+ _maxWidth = null;
3854
+ _maxHeight = null;
3855
+ _fitWidthProportion = 0;
3856
+ _fitHeightProportion = 0;
3857
+ _excludeFromLayout = false;
3536
3858
  /** @ignore */
3537
3859
  constructor() {
3538
3860
  super('layoutchild');
3539
- this._minWidth = 0;
3540
- this._minHeight = 0;
3541
- this._maxWidth = null;
3542
- this._maxHeight = null;
3543
- this._fitWidthProportion = 0;
3544
- this._fitHeightProportion = 0;
3545
- this._excludeFromLayout = false;
3546
3861
  }
3547
3862
  getInitialComponentData() {
3548
3863
  return {
@@ -3743,18 +4058,18 @@
3743
4058
  * @category Components
3744
4059
  */
3745
4060
  class LayoutGroupComponentElement extends ComponentElement {
4061
+ _orientation = 'horizontal';
4062
+ _reverseX = false;
4063
+ _reverseY = false;
4064
+ _alignment = new playcanvas.Vec2(0, 1);
4065
+ _padding = new playcanvas.Vec4(0, 0, 0, 0);
4066
+ _spacing = new playcanvas.Vec2(0, 0);
4067
+ _widthFitting = 'none';
4068
+ _heightFitting = 'none';
4069
+ _wrap = false;
3746
4070
  /** @ignore */
3747
4071
  constructor() {
3748
4072
  super('layoutgroup');
3749
- this._orientation = 'horizontal';
3750
- this._reverseX = false;
3751
- this._reverseY = false;
3752
- this._alignment = new playcanvas.Vec2(0, 1);
3753
- this._padding = new playcanvas.Vec4(0, 0, 0, 0);
3754
- this._spacing = new playcanvas.Vec2(0, 0);
3755
- this._widthFitting = 'none';
3756
- this._heightFitting = 'none';
3757
- this._wrap = false;
3758
4073
  }
3759
4074
  getInitialComponentData() {
3760
4075
  return {
@@ -3782,10 +4097,9 @@
3782
4097
  * @param value - The orientation.
3783
4098
  */
3784
4099
  set orientation(value) {
3785
- var _a;
3786
4100
  this._orientation = value;
3787
4101
  if (this.component) {
3788
- this.component.orientation = (_a = orientations$1.get(value)) !== null && _a !== void 0 ? _a : playcanvas.ORIENTATION_HORIZONTAL;
4102
+ this.component.orientation = orientations$1.get(value) ?? playcanvas.ORIENTATION_HORIZONTAL;
3789
4103
  }
3790
4104
  }
3791
4105
  /**
@@ -3886,10 +4200,9 @@
3886
4200
  * @param value - The width fitting mode.
3887
4201
  */
3888
4202
  set widthFitting(value) {
3889
- var _a;
3890
4203
  this._widthFitting = value;
3891
4204
  if (this.component) {
3892
- this.component.widthFitting = (_a = fittings.get(value)) !== null && _a !== void 0 ? _a : playcanvas.FITTING_NONE;
4205
+ this.component.widthFitting = fittings.get(value) ?? playcanvas.FITTING_NONE;
3893
4206
  }
3894
4207
  }
3895
4208
  /**
@@ -3905,10 +4218,9 @@
3905
4218
  * @param value - The height fitting mode.
3906
4219
  */
3907
4220
  set heightFitting(value) {
3908
- var _a;
3909
4221
  this._heightFitting = value;
3910
4222
  if (this.component) {
3911
- this.component.heightFitting = (_a = fittings.get(value)) !== null && _a !== void 0 ? _a : playcanvas.FITTING_NONE;
4223
+ this.component.heightFitting = fittings.get(value) ?? playcanvas.FITTING_NONE;
3912
4224
  }
3913
4225
  }
3914
4226
  /**
@@ -4004,28 +4316,28 @@
4004
4316
  * @category Components
4005
4317
  */
4006
4318
  class LightComponentElement extends ComponentElement {
4319
+ _castShadows = false;
4320
+ _color = new playcanvas.Color(1, 1, 1);
4321
+ _innerConeAngle = 40;
4322
+ _intensity = 1;
4323
+ _normalOffsetBias = 0.05;
4324
+ _outerConeAngle = 45;
4325
+ _range = 10;
4326
+ _shadowBias = 0.2;
4327
+ _shadowDistance = 16;
4328
+ _shadowIntensity = 1;
4329
+ _shadowResolution = 1024;
4330
+ _shadowType = 'pcf3-32f';
4331
+ _type = 'directional';
4332
+ _vsmBias = 0.01;
4333
+ _vsmBlurSize = 11;
4334
+ _penumbraSize = 1;
4335
+ _penumbraFalloff = 1;
4336
+ _shadowSamples = 16;
4337
+ _shadowBlockerSamples = 16;
4007
4338
  /** @ignore */
4008
4339
  constructor() {
4009
4340
  super('light');
4010
- this._castShadows = false;
4011
- this._color = new playcanvas.Color(1, 1, 1);
4012
- this._innerConeAngle = 40;
4013
- this._intensity = 1;
4014
- this._normalOffsetBias = 0.05;
4015
- this._outerConeAngle = 45;
4016
- this._range = 10;
4017
- this._shadowBias = 0.2;
4018
- this._shadowDistance = 16;
4019
- this._shadowIntensity = 1;
4020
- this._shadowResolution = 1024;
4021
- this._shadowType = 'pcf3-32f';
4022
- this._type = 'directional';
4023
- this._vsmBias = 0.01;
4024
- this._vsmBlurSize = 11;
4025
- this._penumbraSize = 1;
4026
- this._penumbraFalloff = 1;
4027
- this._shadowSamples = 16;
4028
- this._shadowBlockerSamples = 16;
4029
4341
  }
4030
4342
  getInitialComponentData() {
4031
4343
  return {
@@ -4259,10 +4571,9 @@
4259
4571
  * - `pcss-32f` - Percentage-closer soft shadow with 32-bit depth.
4260
4572
  */
4261
4573
  set shadowType(value) {
4262
- var _a;
4263
4574
  this._shadowType = value;
4264
4575
  if (this.component) {
4265
- this.component.shadowType = (_a = shadowTypes.get(value)) !== null && _a !== void 0 ? _a : playcanvas.SHADOW_PCF3_32F;
4576
+ this.component.shadowType = shadowTypes.get(value) ?? playcanvas.SHADOW_PCF3_32F;
4266
4577
  }
4267
4578
  }
4268
4579
  /**
@@ -4490,20 +4801,19 @@
4490
4801
  * @category Components
4491
4802
  */
4492
4803
  class ParticleSystemComponentElement extends ComponentElement {
4804
+ _asset = '';
4493
4805
  /** @ignore */
4494
4806
  constructor() {
4495
4807
  super('particlesystem');
4496
- this._asset = '';
4497
4808
  }
4498
4809
  getInitialComponentData() {
4499
- var _a;
4500
4810
  const asset = AssetElement.get(this._asset);
4501
4811
  if (!asset) {
4502
4812
  return {};
4503
4813
  }
4504
4814
  if (asset.resource.colorMapAsset) {
4505
4815
  const id = asset.resource.colorMapAsset;
4506
- const colorMapAsset = (_a = AssetElement.get(id)) === null || _a === void 0 ? void 0 : _a.id;
4816
+ const colorMapAsset = AssetElement.get(id)?.id;
4507
4817
  if (colorMapAsset) {
4508
4818
  asset.resource.colorMapAsset = colorMapAsset;
4509
4819
  }
@@ -4529,9 +4839,8 @@
4529
4839
  }
4530
4840
  }
4531
4841
  async _loadAsset() {
4532
- var _a;
4533
- const appElement = await ((_a = this.closestApp) === null || _a === void 0 ? void 0 : _a.ready());
4534
- const app = appElement === null || appElement === void 0 ? void 0 : appElement.app;
4842
+ const appElement = await this.closestApp?.ready();
4843
+ const app = appElement?.app;
4535
4844
  const asset = AssetElement.get(this._asset);
4536
4845
  if (!asset) {
4537
4846
  return;
@@ -4606,148 +4915,2183 @@
4606
4915
  super.attributeChangedCallback(name, _oldValue, newValue);
4607
4916
  switch (name) {
4608
4917
  case 'asset':
4609
- this.asset = newValue;
4918
+ this.asset = newValue ?? '';
4610
4919
  break;
4611
4920
  }
4612
4921
  }
4613
4922
  }
4614
4923
  customElements.define('pc-particles', ParticleSystemComponentElement);
4615
4924
 
4616
- /**
4617
- * The MaterialElement interface provides properties and methods for manipulating
4618
- * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-material/ | `<pc-material>`} elements.
4925
+ const blendTypes = new Map([
4926
+ ['none', playcanvas.BLEND_NONE],
4927
+ ['normal', playcanvas.BLEND_NORMAL],
4928
+ ['additive', playcanvas.BLEND_ADDITIVE],
4929
+ ['additive-alpha', playcanvas.BLEND_ADDITIVEALPHA],
4930
+ ['premultiplied', playcanvas.BLEND_PREMULTIPLIED],
4931
+ ['multiplicative', playcanvas.BLEND_MULTIPLICATIVE],
4932
+ ['multiplicative-2x', playcanvas.BLEND_MULTIPLICATIVE2X],
4933
+ ['screen', playcanvas.BLEND_SCREEN],
4934
+ ['min', playcanvas.BLEND_MIN],
4935
+ ['max', playcanvas.BLEND_MAX],
4936
+ ['subtractive', playcanvas.BLEND_SUBTRACTIVE]
4937
+ ]);
4938
+ const cullModes = new Map([
4939
+ ['none', playcanvas.CULLFACE_NONE],
4940
+ ['back', playcanvas.CULLFACE_BACK],
4941
+ ['front', playcanvas.CULLFACE_FRONT],
4942
+ ['front-and-back', playcanvas.CULLFACE_FRONTANDBACK]
4943
+ ]);
4944
+ const fresnelModels = new Map([
4945
+ ['none', playcanvas.FRESNEL_NONE],
4946
+ ['schlick', playcanvas.FRESNEL_SCHLICK]
4947
+ ]);
4948
+ const occludeSpeculars = new Map([
4949
+ ['none', playcanvas.SPECOCC_NONE],
4950
+ ['ao', playcanvas.SPECOCC_AO],
4951
+ ['gloss-dependent', playcanvas.SPECOCC_GLOSSDEPENDENT]
4952
+ ]);
4953
+ const opacityDithers = ['none', 'bayer8', 'bluenoise', 'ignnoise'];
4954
+ const colorChannels = ['r', 'g', 'b', 'a', 'rgb'];
4955
+ const scalarChannels = ['r', 'g', 'b', 'a'];
4956
+ /**
4957
+ * The attributes that contradict a `roughness-*` attribute: each one carries the opposite
4958
+ * interpretation of a value the aliases also write. The `gloss-map-*` modifiers are deliberately
4959
+ * absent - they only configure the shared slot (tiling, offset, channel and so on) and carry no
4960
+ * interpretation of their own, so they are the supported way to configure a `roughness-map`.
4961
+ */
4962
+ const glossConflicts = ['gloss', 'gloss-invert', 'gloss-map'];
4963
+ /** The aliases those attributes contradict. */
4964
+ const roughnessAliases = ['roughness', 'roughness-map'];
4965
+ /**
4966
+ * The MaterialElement interface provides properties and methods for manipulating
4967
+ * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-material/ | `<pc-material>`} elements.
4619
4968
  * The MaterialElement interface also inherits the properties and methods of the
4620
4969
  * {@link HTMLElement} interface.
4621
4970
  *
4622
4971
  * A `pc-material` must be a direct child of `pc-app` — elements placed elsewhere log a warning
4623
4972
  * and never create a material. Elements inserted while the application is already running are
4624
4973
  * created on insertion.
4974
+ *
4975
+ * The element is metal/rough by default: unlike a bare `StandardMaterial` it enables the metalness
4976
+ * workflow, which is what the `metalness-*` attributes assume and what glTF means by PBR. It also
4977
+ * defaults `metalness` to 0 rather than the engine's 1, because those two defaults have to be
4978
+ * chosen together - the engine's 1 is unreachable under its own `useMetalness` of false, and with
4979
+ * the workflow on it would make every material fully metallic, so `<pc-material diffuse="crimson">`
4980
+ * would render as dark tinted reflections of an environment that may not exist rather than as a
4981
+ * crimson surface. `metalness="1"` remains one attribute away.
4982
+ *
4983
+ * The `roughness` and `roughness-map` attributes are aliases for `gloss` and `gloss-map` that
4984
+ * additionally invert the gloss channel; do not mix the two families on one element.
4985
+ *
4986
+ * The two aliases are documented here rather than on an accessor, because they resolve to the
4987
+ * `gloss` properties and would otherwise inherit gloss's description - which reads inverted.
4988
+ *
4989
+ * @attribute {number} roughness - The roughness of the material, from 0 (shiny) to 1 (rough). An
4990
+ * alias for `gloss` that also inverts it, so do not combine it with the `gloss` attributes.
4991
+ * @attribute {string} roughness-map - The id of the `pc-asset` to use as the roughness map. An
4992
+ * alias for `gloss-map` that also inverts the gloss channel, so do not combine it with the `gloss`
4993
+ * attributes.
4625
4994
  */
4626
4995
  class MaterialElement extends HTMLElement {
4627
- constructor() {
4628
- super(...arguments);
4629
- this._diffuse = new playcanvas.Color(1, 1, 1);
4630
- this._diffuseMap = '';
4631
- this._metalnessMap = '';
4632
- this._normalMap = '';
4633
- this._roughnessMap = '';
4634
- this.material = null;
4635
- }
4996
+ _alphaTest = 0;
4997
+ _alphaToCoverage = false;
4998
+ _aoIntensity = 1;
4999
+ _aoMap = '';
5000
+ _aoMapChannel = 'g';
5001
+ _aoMapOffset = new playcanvas.Vec2(0, 0);
5002
+ _aoMapRotation = 0;
5003
+ _aoMapTiling = new playcanvas.Vec2(1, 1);
5004
+ _aoMapUv = 0;
5005
+ _blendType = 'none';
5006
+ _bumpiness = 1;
5007
+ _cull = 'back';
5008
+ _depthBias = 0;
5009
+ _depthTest = true;
5010
+ _depthWrite = true;
5011
+ _diffuse = new playcanvas.Color(1, 1, 1);
5012
+ _diffuseMap = '';
5013
+ _diffuseMapChannel = 'rgb';
5014
+ _diffuseMapOffset = new playcanvas.Vec2(0, 0);
5015
+ _diffuseMapRotation = 0;
5016
+ _diffuseMapTiling = new playcanvas.Vec2(1, 1);
5017
+ _diffuseMapUv = 0;
5018
+ _emissive = new playcanvas.Color(0, 0, 0);
5019
+ _emissiveIntensity = 1;
5020
+ _emissiveMap = '';
5021
+ _emissiveMapChannel = 'rgb';
5022
+ _emissiveMapOffset = new playcanvas.Vec2(0, 0);
5023
+ _emissiveMapRotation = 0;
5024
+ _emissiveMapTiling = new playcanvas.Vec2(1, 1);
5025
+ _emissiveMapUv = 0;
5026
+ _enableGGXSpecular = false;
5027
+ _fresnelModel = 'schlick';
5028
+ _gloss = 0.25;
5029
+ _glossInvert = false;
5030
+ _glossMap = '';
5031
+ _glossMapChannel = 'g';
5032
+ _glossMapOffset = new playcanvas.Vec2(0, 0);
5033
+ _glossMapRotation = 0;
5034
+ _glossMapTiling = new playcanvas.Vec2(1, 1);
5035
+ _glossMapUv = 0;
5036
+ _heightMap = '';
5037
+ _heightMapChannel = 'g';
5038
+ _heightMapFactor = 1;
5039
+ _heightMapOffset = new playcanvas.Vec2(0, 0);
5040
+ _heightMapRotation = 0;
5041
+ _heightMapTiling = new playcanvas.Vec2(1, 1);
5042
+ _heightMapUv = 0;
5043
+ _metalness = 0;
5044
+ _metalnessMap = '';
5045
+ _metalnessMapChannel = 'g';
5046
+ _metalnessMapOffset = new playcanvas.Vec2(0, 0);
5047
+ _metalnessMapRotation = 0;
5048
+ _metalnessMapTiling = new playcanvas.Vec2(1, 1);
5049
+ _metalnessMapUv = 0;
5050
+ _normalMap = '';
5051
+ _normalMapOffset = new playcanvas.Vec2(0, 0);
5052
+ _normalMapRotation = 0;
5053
+ _normalMapTiling = new playcanvas.Vec2(1, 1);
5054
+ _normalMapUv = 0;
5055
+ _occludeDirect = false;
5056
+ _occludeSpecular = 'ao';
5057
+ _opacity = 1;
5058
+ _opacityDither = 'none';
5059
+ _opacityFadesSpecular = true;
5060
+ _opacityMap = '';
5061
+ _opacityMapChannel = 'a';
5062
+ _opacityMapOffset = new playcanvas.Vec2(0, 0);
5063
+ _opacityMapRotation = 0;
5064
+ _opacityMapTiling = new playcanvas.Vec2(1, 1);
5065
+ _opacityMapUv = 0;
5066
+ _slopeDepthBias = 0;
5067
+ _specular = new playcanvas.Color(0, 0, 0);
5068
+ _specularityFactor = 1;
5069
+ _twoSidedLighting = false;
5070
+ _useFog = true;
5071
+ _useLighting = true;
5072
+ // Diverges from the engine default of false - see the class docblock and createMaterial()
5073
+ _useMetalness = true;
5074
+ _useMetalnessSpecularColor = false;
5075
+ _useSkybox = true;
5076
+ _useTonemap = true;
5077
+ /**
5078
+ * Pending `load` handlers, one per texture slot. A slot's handler is torn down when the slot is
5079
+ * reassigned or the element disconnects, so a late-arriving asset can never write a texture the
5080
+ * element no longer wants.
5081
+ */
5082
+ _mapHandles = new Map();
5083
+ _updateScheduled = false;
5084
+ _glossConflictWarned = false;
5085
+ material = null;
4636
5086
  async connectedCallback() {
4637
- var _a, _b;
4638
- const appElement = (_b = (_a = this.parentElement) === null || _a === void 0 ? void 0 : _a.closest('pc-app')) !== null && _b !== void 0 ? _b : null;
5087
+ const appElement = this.parentElement?.closest('pc-app') ?? null;
4639
5088
  // Materials must be direct children of pc-app (matches the boot query ':scope > pc-material')
4640
5089
  if (!appElement || this.parentElement !== appElement) {
4641
5090
  console.warn(`pc-material '${this.id}' must be a direct child of pc-app - material not created`);
4642
5091
  return;
4643
5092
  }
4644
- await appElement.ready();
4645
- // The element may have been removed or re-parented while waiting for the app
4646
- if (!this.isConnected || this.parentElement !== appElement)
4647
- return;
4648
- // Materials present at startup are created by AppElement's boot; this branch handles
4649
- // elements inserted (or re-inserted) after the app is already running
4650
- if (!this.material) {
4651
- if (!appElement.app)
4652
- return; // pc-app is re-connecting; its own boot will create this
4653
- this.createMaterial();
5093
+ await appElement.ready();
5094
+ // The element may have been removed or re-parented while waiting for the app
5095
+ if (!this.isConnected || this.parentElement !== appElement)
5096
+ return;
5097
+ // Materials present at startup are created by AppElement's boot; this branch handles
5098
+ // elements inserted (or re-inserted) after the app is already running
5099
+ if (!this.material) {
5100
+ if (!appElement.app)
5101
+ return; // pc-app is re-connecting; its own boot will create this
5102
+ this.createMaterial();
5103
+ }
5104
+ }
5105
+ createMaterial() {
5106
+ const material = new playcanvas.StandardMaterial();
5107
+ this.material = material;
5108
+ material.alphaTest = this._alphaTest;
5109
+ material.alphaToCoverage = this._alphaToCoverage;
5110
+ material.aoIntensity = this._aoIntensity;
5111
+ material.aoMapChannel = this._aoMapChannel;
5112
+ material.aoMapOffset = this._aoMapOffset;
5113
+ material.aoMapRotation = this._aoMapRotation;
5114
+ material.aoMapTiling = this._aoMapTiling;
5115
+ material.aoMapUv = this._aoMapUv;
5116
+ material.blendType = blendTypes.get(this._blendType) ?? playcanvas.BLEND_NONE;
5117
+ material.bumpiness = this._bumpiness;
5118
+ material.cull = cullModes.get(this._cull) ?? playcanvas.CULLFACE_BACK;
5119
+ material.depthBias = this._depthBias;
5120
+ material.depthTest = this._depthTest;
5121
+ material.depthWrite = this._depthWrite;
5122
+ material.diffuse = this._diffuse;
5123
+ material.diffuseMapChannel = this._diffuseMapChannel;
5124
+ material.diffuseMapOffset = this._diffuseMapOffset;
5125
+ material.diffuseMapRotation = this._diffuseMapRotation;
5126
+ material.diffuseMapTiling = this._diffuseMapTiling;
5127
+ material.diffuseMapUv = this._diffuseMapUv;
5128
+ material.emissive = this._emissive;
5129
+ material.emissiveIntensity = this._emissiveIntensity;
5130
+ material.emissiveMapChannel = this._emissiveMapChannel;
5131
+ material.emissiveMapOffset = this._emissiveMapOffset;
5132
+ material.emissiveMapRotation = this._emissiveMapRotation;
5133
+ material.emissiveMapTiling = this._emissiveMapTiling;
5134
+ material.emissiveMapUv = this._emissiveMapUv;
5135
+ material.enableGGXSpecular = this._enableGGXSpecular;
5136
+ material.fresnelModel = fresnelModels.get(this._fresnelModel) ?? playcanvas.FRESNEL_SCHLICK;
5137
+ material.gloss = this._gloss;
5138
+ material.glossInvert = this._glossInvert;
5139
+ material.glossMapChannel = this._glossMapChannel;
5140
+ material.glossMapOffset = this._glossMapOffset;
5141
+ material.glossMapRotation = this._glossMapRotation;
5142
+ material.glossMapTiling = this._glossMapTiling;
5143
+ material.glossMapUv = this._glossMapUv;
5144
+ material.heightMapChannel = this._heightMapChannel;
5145
+ material.heightMapFactor = this._heightMapFactor;
5146
+ material.heightMapOffset = this._heightMapOffset;
5147
+ material.heightMapRotation = this._heightMapRotation;
5148
+ material.heightMapTiling = this._heightMapTiling;
5149
+ material.heightMapUv = this._heightMapUv;
5150
+ material.metalness = this._metalness;
5151
+ material.metalnessMapChannel = this._metalnessMapChannel;
5152
+ material.metalnessMapOffset = this._metalnessMapOffset;
5153
+ material.metalnessMapRotation = this._metalnessMapRotation;
5154
+ material.metalnessMapTiling = this._metalnessMapTiling;
5155
+ material.metalnessMapUv = this._metalnessMapUv;
5156
+ material.normalMapOffset = this._normalMapOffset;
5157
+ material.normalMapRotation = this._normalMapRotation;
5158
+ material.normalMapTiling = this._normalMapTiling;
5159
+ material.normalMapUv = this._normalMapUv;
5160
+ // @ts-ignore the engine's generated .d.ts types occludeDirect as a number, but its own
5161
+ // JSDoc documents it as a boolean and its runtime default is `false`
5162
+ material.occludeDirect = this._occludeDirect;
5163
+ material.occludeSpecular = occludeSpeculars.get(this._occludeSpecular) ?? playcanvas.SPECOCC_AO;
5164
+ material.opacity = this._opacity;
5165
+ material.opacityDither = this._opacityDither;
5166
+ material.opacityFadesSpecular = this._opacityFadesSpecular;
5167
+ material.opacityMapChannel = this._opacityMapChannel;
5168
+ material.opacityMapOffset = this._opacityMapOffset;
5169
+ material.opacityMapRotation = this._opacityMapRotation;
5170
+ material.opacityMapTiling = this._opacityMapTiling;
5171
+ material.opacityMapUv = this._opacityMapUv;
5172
+ material.slopeDepthBias = this._slopeDepthBias;
5173
+ material.specular = this._specular;
5174
+ material.specularityFactor = this._specularityFactor;
5175
+ material.twoSidedLighting = this._twoSidedLighting;
5176
+ material.useFog = this._useFog;
5177
+ material.useLighting = this._useLighting;
5178
+ // The engine defaults to the older specular/gloss workflow, in which metalnessMap is never
5179
+ // sampled at all - useMetalness drives the LIT_METALNESS define. This element defaults the
5180
+ // other way, so that `metalness-map` does what its name says.
5181
+ material.useMetalness = this._useMetalness;
5182
+ material.useMetalnessSpecularColor = this._useMetalnessSpecularColor;
5183
+ material.useSkybox = this._useSkybox;
5184
+ material.useTonemap = this._useTonemap;
5185
+ // Texture slots resolve a pc-asset id, which may not have loaded yet
5186
+ this.aoMap = this._aoMap;
5187
+ this.diffuseMap = this._diffuseMap;
5188
+ this.emissiveMap = this._emissiveMap;
5189
+ this.glossMap = this._glossMap;
5190
+ this.heightMap = this._heightMap;
5191
+ this.metalnessMap = this._metalnessMap;
5192
+ this.normalMap = this._normalMap;
5193
+ this.opacityMap = this._opacityMap;
5194
+ material.update();
5195
+ }
5196
+ disconnectedCallback() {
5197
+ for (const handle of this._mapHandles.values()) {
5198
+ handle.off();
5199
+ }
5200
+ this._mapHandles.clear();
5201
+ if (this.material) {
5202
+ this.material.destroy();
5203
+ this.material = null;
5204
+ }
5205
+ }
5206
+ /**
5207
+ * Coalesces `material.update()` across a burst of attribute or property writes, so that setting
5208
+ * a dozen attributes in one parse costs one update rather than a dozen.
5209
+ */
5210
+ _scheduleUpdate() {
5211
+ if (this._updateScheduled)
5212
+ return;
5213
+ this._updateScheduled = true;
5214
+ queueMicrotask(() => {
5215
+ this._updateScheduled = false;
5216
+ this.material?.update();
5217
+ });
5218
+ }
5219
+ /**
5220
+ * Warns when a `roughness-*` attribute is combined with one that carries the opposite
5221
+ * interpretation of the same value. They write the same engine properties but disagree about
5222
+ * whether the channel is inverted, so the result would depend on attribute order rather than
5223
+ * on intent.
5224
+ *
5225
+ * Called from both families rather than only from the roughness branches, because the two
5226
+ * orderings are equally wrong and only one of them would otherwise be caught. The conflict is
5227
+ * a property of the element rather than of any one write - and an upgrading element already
5228
+ * has all of its attributes, so every branch would otherwise report the same clash - so the
5229
+ * warning latches and reports once per episode, clearing when the clash is resolved.
5230
+ */
5231
+ _warnGlossConflict() {
5232
+ const quote = (names) => `'${names.join('\', \'')}'`;
5233
+ const roughness = roughnessAliases.filter(name => this.hasAttribute(name));
5234
+ const gloss = glossConflicts.filter(name => this.hasAttribute(name));
5235
+ if (roughness.length === 0 || gloss.length === 0) {
5236
+ this._glossConflictWarned = false;
5237
+ return;
5238
+ }
5239
+ if (this._glossConflictWarned)
5240
+ return;
5241
+ this._glossConflictWarned = true;
5242
+ console.warn(`pc-material '${this.id}' sets both ${quote(roughness)} and ${quote(gloss)} - ` +
5243
+ 'the roughness-* attributes invert gloss, so the two families contradict each other. Use one or the other.');
5244
+ }
5245
+ /**
5246
+ * Points a texture slot at the resource of a `pc-asset`, waiting for the asset to load when it
5247
+ * has not already. An empty id clears the slot.
5248
+ *
5249
+ * @param id - The id of the `pc-asset`, or an empty string to clear the slot.
5250
+ * @param slot - The material property to write.
5251
+ */
5252
+ setMap(id, slot) {
5253
+ // Drop any load still pending for this slot - its texture is no longer the one we want
5254
+ this._mapHandles.get(slot)?.off();
5255
+ this._mapHandles.delete(slot);
5256
+ if (!this.material)
5257
+ return;
5258
+ if (!id) {
5259
+ this.material[slot] = null;
5260
+ this._scheduleUpdate();
5261
+ return;
5262
+ }
5263
+ const asset = AssetElement.get(id);
5264
+ if (!asset)
5265
+ return;
5266
+ if (asset.loaded) {
5267
+ this._applyMap(slot, asset.resource);
5268
+ return;
5269
+ }
5270
+ this._mapHandles.set(slot, asset.once('load', () => {
5271
+ this._mapHandles.delete(slot);
5272
+ this._applyMap(slot, asset.resource);
5273
+ }));
5274
+ }
5275
+ /**
5276
+ * @param slot - The material property to write.
5277
+ * @param texture - The loaded texture.
5278
+ */
5279
+ _applyMap(slot, texture) {
5280
+ if (!this.material)
5281
+ return;
5282
+ this.material[slot] = texture;
5283
+ texture.anisotropy = 4;
5284
+ this._scheduleUpdate();
5285
+ }
5286
+ /**
5287
+ * Sets the alpha test reference value. Fragments with an opacity below this value are discarded.
5288
+ * @param value - The alpha test reference value.
5289
+ */
5290
+ set alphaTest(value) {
5291
+ this._alphaTest = value;
5292
+ if (this.material) {
5293
+ this.material.alphaTest = value;
5294
+ this._scheduleUpdate();
5295
+ }
5296
+ }
5297
+ /**
5298
+ * Gets the alpha test reference value.
5299
+ * @returns The alpha test reference value.
5300
+ */
5301
+ get alphaTest() {
5302
+ return this._alphaTest;
5303
+ }
5304
+ /**
5305
+ * Sets whether to use alpha to coverage, which resolves transparency using multisampling.
5306
+ * @param value - The alpha to coverage flag.
5307
+ */
5308
+ set alphaToCoverage(value) {
5309
+ this._alphaToCoverage = value;
5310
+ if (this.material) {
5311
+ this.material.alphaToCoverage = value;
5312
+ this._scheduleUpdate();
5313
+ }
5314
+ }
5315
+ /**
5316
+ * Gets whether to use alpha to coverage.
5317
+ * @returns The alpha to coverage flag.
5318
+ */
5319
+ get alphaToCoverage() {
5320
+ return this._alphaToCoverage;
5321
+ }
5322
+ /**
5323
+ * Sets the strength of the ambient occlusion map, from 0 to 1.
5324
+ * @param value - The ambient occlusion intensity.
5325
+ */
5326
+ set aoIntensity(value) {
5327
+ this._aoIntensity = value;
5328
+ if (this.material) {
5329
+ this.material.aoIntensity = value;
5330
+ this._scheduleUpdate();
5331
+ }
5332
+ }
5333
+ /**
5334
+ * Gets the strength of the ambient occlusion map.
5335
+ * @returns The ambient occlusion intensity.
5336
+ */
5337
+ get aoIntensity() {
5338
+ return this._aoIntensity;
5339
+ }
5340
+ /**
5341
+ * Sets the id of the `pc-asset` to use as the ambient occlusion map.
5342
+ * @param value - The asset id.
5343
+ */
5344
+ set aoMap(value) {
5345
+ this._aoMap = value;
5346
+ this.setMap(value, 'aoMap');
5347
+ }
5348
+ /**
5349
+ * Gets the id of the `pc-asset` used as the ambient occlusion map.
5350
+ * @returns The asset id.
5351
+ */
5352
+ get aoMap() {
5353
+ return this._aoMap;
5354
+ }
5355
+ /**
5356
+ * Sets the color channel of the ambient occlusion map to sample.
5357
+ * @param value - The channel.
5358
+ */
5359
+ set aoMapChannel(value) {
5360
+ this._aoMapChannel = value;
5361
+ if (this.material) {
5362
+ this.material.aoMapChannel = value;
5363
+ this._scheduleUpdate();
5364
+ }
5365
+ }
5366
+ /**
5367
+ * Gets the color channel of the ambient occlusion map to sample.
5368
+ * @returns The channel.
5369
+ */
5370
+ get aoMapChannel() {
5371
+ return this._aoMapChannel;
5372
+ }
5373
+ /**
5374
+ * Sets the 2D offset of the ambient occlusion map.
5375
+ * @param value - The offset.
5376
+ */
5377
+ set aoMapOffset(value) {
5378
+ this._aoMapOffset = value;
5379
+ if (this.material) {
5380
+ this.material.aoMapOffset = value;
5381
+ this._scheduleUpdate();
5382
+ }
5383
+ }
5384
+ /**
5385
+ * Gets the 2D offset of the ambient occlusion map.
5386
+ * @returns The offset.
5387
+ */
5388
+ get aoMapOffset() {
5389
+ return this._aoMapOffset;
5390
+ }
5391
+ /**
5392
+ * Sets the 2D rotation of the ambient occlusion map, in degrees.
5393
+ * @param value - The rotation.
5394
+ */
5395
+ set aoMapRotation(value) {
5396
+ this._aoMapRotation = value;
5397
+ if (this.material) {
5398
+ this.material.aoMapRotation = value;
5399
+ this._scheduleUpdate();
5400
+ }
5401
+ }
5402
+ /**
5403
+ * Gets the 2D rotation of the ambient occlusion map.
5404
+ * @returns The rotation.
5405
+ */
5406
+ get aoMapRotation() {
5407
+ return this._aoMapRotation;
5408
+ }
5409
+ /**
5410
+ * Sets the 2D tiling of the ambient occlusion map.
5411
+ * @param value - The tiling.
5412
+ */
5413
+ set aoMapTiling(value) {
5414
+ this._aoMapTiling = value;
5415
+ if (this.material) {
5416
+ this.material.aoMapTiling = value;
5417
+ this._scheduleUpdate();
5418
+ }
5419
+ }
5420
+ /**
5421
+ * Gets the 2D tiling of the ambient occlusion map.
5422
+ * @returns The tiling.
5423
+ */
5424
+ get aoMapTiling() {
5425
+ return this._aoMapTiling;
5426
+ }
5427
+ /**
5428
+ * Sets the UV channel the ambient occlusion map samples.
5429
+ * @param value - The UV channel.
5430
+ */
5431
+ set aoMapUv(value) {
5432
+ this._aoMapUv = value;
5433
+ if (this.material) {
5434
+ this.material.aoMapUv = value;
5435
+ this._scheduleUpdate();
5436
+ }
5437
+ }
5438
+ /**
5439
+ * Gets the UV channel the ambient occlusion map samples.
5440
+ * @returns The UV channel.
5441
+ */
5442
+ get aoMapUv() {
5443
+ return this._aoMapUv;
5444
+ }
5445
+ /**
5446
+ * Sets how the material is blended with the scene behind it.
5447
+ * @param value - The blend type.
5448
+ */
5449
+ set blendType(value) {
5450
+ this._blendType = value;
5451
+ if (this.material) {
5452
+ this.material.blendType = blendTypes.get(value) ?? playcanvas.BLEND_NONE;
5453
+ this._scheduleUpdate();
5454
+ }
5455
+ }
5456
+ /**
5457
+ * Gets how the material is blended with the scene behind it.
5458
+ * @returns The blend type.
5459
+ */
5460
+ get blendType() {
5461
+ return this._blendType;
5462
+ }
5463
+ /**
5464
+ * Sets the strength of the normal map, where 0 is flat and 1 is the map's full effect.
5465
+ * @param value - The bumpiness.
5466
+ */
5467
+ set bumpiness(value) {
5468
+ this._bumpiness = value;
5469
+ if (this.material) {
5470
+ this.material.bumpiness = value;
5471
+ this._scheduleUpdate();
5472
+ }
5473
+ }
5474
+ /**
5475
+ * Gets the strength of the normal map.
5476
+ * @returns The bumpiness.
5477
+ */
5478
+ get bumpiness() {
5479
+ return this._bumpiness;
5480
+ }
5481
+ /**
5482
+ * Sets which faces of a mesh are culled.
5483
+ * @param value - The cull mode.
5484
+ */
5485
+ set cull(value) {
5486
+ this._cull = value;
5487
+ if (this.material) {
5488
+ this.material.cull = cullModes.get(value) ?? playcanvas.CULLFACE_BACK;
5489
+ this._scheduleUpdate();
5490
+ }
5491
+ }
5492
+ /**
5493
+ * Gets which faces of a mesh are culled.
5494
+ * @returns The cull mode.
5495
+ */
5496
+ get cull() {
5497
+ return this._cull;
5498
+ }
5499
+ /**
5500
+ * Sets the offset applied to the depth of a fragment, used to resolve z-fighting.
5501
+ * @param value - The depth bias.
5502
+ */
5503
+ set depthBias(value) {
5504
+ this._depthBias = value;
5505
+ if (this.material) {
5506
+ this.material.depthBias = value;
5507
+ this._scheduleUpdate();
5508
+ }
5509
+ }
5510
+ /**
5511
+ * Gets the offset applied to the depth of a fragment.
5512
+ * @returns The depth bias.
5513
+ */
5514
+ get depthBias() {
5515
+ return this._depthBias;
5516
+ }
5517
+ /**
5518
+ * Sets whether fragments are tested against the depth buffer.
5519
+ * @param value - The depth test flag.
5520
+ */
5521
+ set depthTest(value) {
5522
+ this._depthTest = value;
5523
+ if (this.material) {
5524
+ this.material.depthTest = value;
5525
+ this._scheduleUpdate();
5526
+ }
5527
+ }
5528
+ /**
5529
+ * Gets whether fragments are tested against the depth buffer.
5530
+ * @returns The depth test flag.
5531
+ */
5532
+ get depthTest() {
5533
+ return this._depthTest;
5534
+ }
5535
+ /**
5536
+ * Sets whether fragments write to the depth buffer.
5537
+ * @param value - The depth write flag.
5538
+ */
5539
+ set depthWrite(value) {
5540
+ this._depthWrite = value;
5541
+ if (this.material) {
5542
+ this.material.depthWrite = value;
5543
+ this._scheduleUpdate();
5544
+ }
5545
+ }
5546
+ /**
5547
+ * Gets whether fragments write to the depth buffer.
5548
+ * @returns The depth write flag.
5549
+ */
5550
+ get depthWrite() {
5551
+ return this._depthWrite;
5552
+ }
5553
+ /**
5554
+ * Sets the diffuse color of the material. With the metalness workflow this doubles as the
5555
+ * specular color where the surface is metallic.
5556
+ * @param value - The diffuse color.
5557
+ */
5558
+ set diffuse(value) {
5559
+ this._diffuse = value;
5560
+ if (this.material) {
5561
+ this.material.diffuse = value;
5562
+ this._scheduleUpdate();
5563
+ }
5564
+ }
5565
+ /**
5566
+ * Gets the diffuse color of the material.
5567
+ * @returns The diffuse color.
5568
+ */
5569
+ get diffuse() {
5570
+ return this._diffuse;
5571
+ }
5572
+ /**
5573
+ * Sets the id of the `pc-asset` to use as the diffuse map.
5574
+ * @param value - The asset id.
5575
+ */
5576
+ set diffuseMap(value) {
5577
+ this._diffuseMap = value;
5578
+ this.setMap(value, 'diffuseMap');
5579
+ }
5580
+ /**
5581
+ * Gets the id of the `pc-asset` used as the diffuse map.
5582
+ * @returns The asset id.
5583
+ */
5584
+ get diffuseMap() {
5585
+ return this._diffuseMap;
5586
+ }
5587
+ /**
5588
+ * Sets the color channels of the diffuse map to sample.
5589
+ * @param value - The channels.
5590
+ */
5591
+ set diffuseMapChannel(value) {
5592
+ this._diffuseMapChannel = value;
5593
+ if (this.material) {
5594
+ this.material.diffuseMapChannel = value;
5595
+ this._scheduleUpdate();
5596
+ }
5597
+ }
5598
+ /**
5599
+ * Gets the color channels of the diffuse map to sample.
5600
+ * @returns The channels.
5601
+ */
5602
+ get diffuseMapChannel() {
5603
+ return this._diffuseMapChannel;
5604
+ }
5605
+ /**
5606
+ * Sets the 2D offset of the diffuse map.
5607
+ * @param value - The offset.
5608
+ */
5609
+ set diffuseMapOffset(value) {
5610
+ this._diffuseMapOffset = value;
5611
+ if (this.material) {
5612
+ this.material.diffuseMapOffset = value;
5613
+ this._scheduleUpdate();
5614
+ }
5615
+ }
5616
+ /**
5617
+ * Gets the 2D offset of the diffuse map.
5618
+ * @returns The offset.
5619
+ */
5620
+ get diffuseMapOffset() {
5621
+ return this._diffuseMapOffset;
5622
+ }
5623
+ /**
5624
+ * Sets the 2D rotation of the diffuse map, in degrees.
5625
+ * @param value - The rotation.
5626
+ */
5627
+ set diffuseMapRotation(value) {
5628
+ this._diffuseMapRotation = value;
5629
+ if (this.material) {
5630
+ this.material.diffuseMapRotation = value;
5631
+ this._scheduleUpdate();
5632
+ }
5633
+ }
5634
+ /**
5635
+ * Gets the 2D rotation of the diffuse map.
5636
+ * @returns The rotation.
5637
+ */
5638
+ get diffuseMapRotation() {
5639
+ return this._diffuseMapRotation;
5640
+ }
5641
+ /**
5642
+ * Sets the 2D tiling of the diffuse map.
5643
+ * @param value - The tiling.
5644
+ */
5645
+ set diffuseMapTiling(value) {
5646
+ this._diffuseMapTiling = value;
5647
+ if (this.material) {
5648
+ this.material.diffuseMapTiling = value;
5649
+ this._scheduleUpdate();
5650
+ }
5651
+ }
5652
+ /**
5653
+ * Gets the 2D tiling of the diffuse map.
5654
+ * @returns The tiling.
5655
+ */
5656
+ get diffuseMapTiling() {
5657
+ return this._diffuseMapTiling;
5658
+ }
5659
+ /**
5660
+ * Sets the UV channel the diffuse map samples.
5661
+ * @param value - The UV channel.
5662
+ */
5663
+ set diffuseMapUv(value) {
5664
+ this._diffuseMapUv = value;
5665
+ if (this.material) {
5666
+ this.material.diffuseMapUv = value;
5667
+ this._scheduleUpdate();
5668
+ }
5669
+ }
5670
+ /**
5671
+ * Gets the UV channel the diffuse map samples.
5672
+ * @returns The UV channel.
5673
+ */
5674
+ get diffuseMapUv() {
5675
+ return this._diffuseMapUv;
5676
+ }
5677
+ /**
5678
+ * Sets the emissive color of the material, which is added to the lit result.
5679
+ * @param value - The emissive color.
5680
+ */
5681
+ set emissive(value) {
5682
+ this._emissive = value;
5683
+ if (this.material) {
5684
+ this.material.emissive = value;
5685
+ this._scheduleUpdate();
5686
+ }
5687
+ }
5688
+ /**
5689
+ * Gets the emissive color of the material.
5690
+ * @returns The emissive color.
5691
+ */
5692
+ get emissive() {
5693
+ return this._emissive;
5694
+ }
5695
+ /**
5696
+ * Sets the multiplier applied to the emissive color and map.
5697
+ * @param value - The emissive intensity.
5698
+ */
5699
+ set emissiveIntensity(value) {
5700
+ this._emissiveIntensity = value;
5701
+ if (this.material) {
5702
+ this.material.emissiveIntensity = value;
5703
+ this._scheduleUpdate();
5704
+ }
5705
+ }
5706
+ /**
5707
+ * Gets the multiplier applied to the emissive color and map.
5708
+ * @returns The emissive intensity.
5709
+ */
5710
+ get emissiveIntensity() {
5711
+ return this._emissiveIntensity;
5712
+ }
5713
+ /**
5714
+ * Sets the id of the `pc-asset` to use as the emissive map.
5715
+ * @param value - The asset id.
5716
+ */
5717
+ set emissiveMap(value) {
5718
+ this._emissiveMap = value;
5719
+ this.setMap(value, 'emissiveMap');
5720
+ }
5721
+ /**
5722
+ * Gets the id of the `pc-asset` used as the emissive map.
5723
+ * @returns The asset id.
5724
+ */
5725
+ get emissiveMap() {
5726
+ return this._emissiveMap;
5727
+ }
5728
+ /**
5729
+ * Sets the color channels of the emissive map to sample.
5730
+ * @param value - The channels.
5731
+ */
5732
+ set emissiveMapChannel(value) {
5733
+ this._emissiveMapChannel = value;
5734
+ if (this.material) {
5735
+ this.material.emissiveMapChannel = value;
5736
+ this._scheduleUpdate();
5737
+ }
5738
+ }
5739
+ /**
5740
+ * Gets the color channels of the emissive map to sample.
5741
+ * @returns The channels.
5742
+ */
5743
+ get emissiveMapChannel() {
5744
+ return this._emissiveMapChannel;
5745
+ }
5746
+ /**
5747
+ * Sets the 2D offset of the emissive map.
5748
+ * @param value - The offset.
5749
+ */
5750
+ set emissiveMapOffset(value) {
5751
+ this._emissiveMapOffset = value;
5752
+ if (this.material) {
5753
+ this.material.emissiveMapOffset = value;
5754
+ this._scheduleUpdate();
5755
+ }
5756
+ }
5757
+ /**
5758
+ * Gets the 2D offset of the emissive map.
5759
+ * @returns The offset.
5760
+ */
5761
+ get emissiveMapOffset() {
5762
+ return this._emissiveMapOffset;
5763
+ }
5764
+ /**
5765
+ * Sets the 2D rotation of the emissive map, in degrees.
5766
+ * @param value - The rotation.
5767
+ */
5768
+ set emissiveMapRotation(value) {
5769
+ this._emissiveMapRotation = value;
5770
+ if (this.material) {
5771
+ this.material.emissiveMapRotation = value;
5772
+ this._scheduleUpdate();
5773
+ }
5774
+ }
5775
+ /**
5776
+ * Gets the 2D rotation of the emissive map.
5777
+ * @returns The rotation.
5778
+ */
5779
+ get emissiveMapRotation() {
5780
+ return this._emissiveMapRotation;
5781
+ }
5782
+ /**
5783
+ * Sets the 2D tiling of the emissive map.
5784
+ * @param value - The tiling.
5785
+ */
5786
+ set emissiveMapTiling(value) {
5787
+ this._emissiveMapTiling = value;
5788
+ if (this.material) {
5789
+ this.material.emissiveMapTiling = value;
5790
+ this._scheduleUpdate();
5791
+ }
5792
+ }
5793
+ /**
5794
+ * Gets the 2D tiling of the emissive map.
5795
+ * @returns The tiling.
5796
+ */
5797
+ get emissiveMapTiling() {
5798
+ return this._emissiveMapTiling;
5799
+ }
5800
+ /**
5801
+ * Sets the UV channel the emissive map samples.
5802
+ * @param value - The UV channel.
5803
+ */
5804
+ set emissiveMapUv(value) {
5805
+ this._emissiveMapUv = value;
5806
+ if (this.material) {
5807
+ this.material.emissiveMapUv = value;
5808
+ this._scheduleUpdate();
5809
+ }
5810
+ }
5811
+ /**
5812
+ * Gets the UV channel the emissive map samples.
5813
+ * @returns The UV channel.
5814
+ */
5815
+ get emissiveMapUv() {
5816
+ return this._emissiveMapUv;
5817
+ }
5818
+ /**
5819
+ * Sets whether to use the GGX specular model, which supports anisotropy.
5820
+ * @param value - The GGX specular flag.
5821
+ */
5822
+ set enableGGXSpecular(value) {
5823
+ this._enableGGXSpecular = value;
5824
+ if (this.material) {
5825
+ this.material.enableGGXSpecular = value;
5826
+ this._scheduleUpdate();
5827
+ }
5828
+ }
5829
+ /**
5830
+ * Gets whether to use the GGX specular model.
5831
+ * @returns The GGX specular flag.
5832
+ */
5833
+ get enableGGXSpecular() {
5834
+ return this._enableGGXSpecular;
5835
+ }
5836
+ /**
5837
+ * Sets the Fresnel model used for specular reflections at grazing angles.
5838
+ * @param value - The Fresnel model.
5839
+ */
5840
+ set fresnelModel(value) {
5841
+ this._fresnelModel = value;
5842
+ if (this.material) {
5843
+ this.material.fresnelModel = fresnelModels.get(value) ?? playcanvas.FRESNEL_SCHLICK;
5844
+ this._scheduleUpdate();
5845
+ }
5846
+ }
5847
+ /**
5848
+ * Gets the Fresnel model used for specular reflections at grazing angles.
5849
+ * @returns The Fresnel model.
5850
+ */
5851
+ get fresnelModel() {
5852
+ return this._fresnelModel;
5853
+ }
5854
+ /**
5855
+ * Sets the glossiness of the material, from 0 (rough) to 1 (shiny). See also `roughness`.
5856
+ * @param value - The gloss.
5857
+ */
5858
+ set gloss(value) {
5859
+ this._gloss = value;
5860
+ if (this.material) {
5861
+ this.material.gloss = value;
5862
+ this._scheduleUpdate();
5863
+ }
5864
+ }
5865
+ /**
5866
+ * Gets the glossiness of the material.
5867
+ * @returns The gloss.
5868
+ */
5869
+ get gloss() {
5870
+ return this._gloss;
5871
+ }
5872
+ /**
5873
+ * Sets whether the gloss value and map are inverted, which makes the material treat them as
5874
+ * roughness. Setting `roughness` or `roughness-map` enables this automatically.
5875
+ * @param value - The gloss invert flag.
5876
+ */
5877
+ set glossInvert(value) {
5878
+ this._glossInvert = value;
5879
+ if (this.material) {
5880
+ this.material.glossInvert = value;
5881
+ this._scheduleUpdate();
5882
+ }
5883
+ }
5884
+ /**
5885
+ * Gets whether the gloss value and map are inverted.
5886
+ * @returns The gloss invert flag.
5887
+ */
5888
+ get glossInvert() {
5889
+ return this._glossInvert;
5890
+ }
5891
+ /**
5892
+ * Sets the id of the `pc-asset` to use as the gloss map. See also `roughnessMap`.
5893
+ * @param value - The asset id.
5894
+ */
5895
+ set glossMap(value) {
5896
+ this._glossMap = value;
5897
+ this.setMap(value, 'glossMap');
5898
+ }
5899
+ /**
5900
+ * Gets the id of the `pc-asset` used as the gloss map.
5901
+ * @returns The asset id.
5902
+ */
5903
+ get glossMap() {
5904
+ return this._glossMap;
5905
+ }
5906
+ /**
5907
+ * Sets the color channel of the gloss map to sample.
5908
+ * @param value - The channel.
5909
+ */
5910
+ set glossMapChannel(value) {
5911
+ this._glossMapChannel = value;
5912
+ if (this.material) {
5913
+ this.material.glossMapChannel = value;
5914
+ this._scheduleUpdate();
5915
+ }
5916
+ }
5917
+ /**
5918
+ * Gets the color channel of the gloss map to sample.
5919
+ * @returns The channel.
5920
+ */
5921
+ get glossMapChannel() {
5922
+ return this._glossMapChannel;
5923
+ }
5924
+ /**
5925
+ * Sets the 2D offset of the gloss map.
5926
+ * @param value - The offset.
5927
+ */
5928
+ set glossMapOffset(value) {
5929
+ this._glossMapOffset = value;
5930
+ if (this.material) {
5931
+ this.material.glossMapOffset = value;
5932
+ this._scheduleUpdate();
5933
+ }
5934
+ }
5935
+ /**
5936
+ * Gets the 2D offset of the gloss map.
5937
+ * @returns The offset.
5938
+ */
5939
+ get glossMapOffset() {
5940
+ return this._glossMapOffset;
5941
+ }
5942
+ /**
5943
+ * Sets the 2D rotation of the gloss map, in degrees.
5944
+ * @param value - The rotation.
5945
+ */
5946
+ set glossMapRotation(value) {
5947
+ this._glossMapRotation = value;
5948
+ if (this.material) {
5949
+ this.material.glossMapRotation = value;
5950
+ this._scheduleUpdate();
5951
+ }
5952
+ }
5953
+ /**
5954
+ * Gets the 2D rotation of the gloss map.
5955
+ * @returns The rotation.
5956
+ */
5957
+ get glossMapRotation() {
5958
+ return this._glossMapRotation;
5959
+ }
5960
+ /**
5961
+ * Sets the 2D tiling of the gloss map.
5962
+ * @param value - The tiling.
5963
+ */
5964
+ set glossMapTiling(value) {
5965
+ this._glossMapTiling = value;
5966
+ if (this.material) {
5967
+ this.material.glossMapTiling = value;
5968
+ this._scheduleUpdate();
5969
+ }
5970
+ }
5971
+ /**
5972
+ * Gets the 2D tiling of the gloss map.
5973
+ * @returns The tiling.
5974
+ */
5975
+ get glossMapTiling() {
5976
+ return this._glossMapTiling;
5977
+ }
5978
+ /**
5979
+ * Sets the UV channel the gloss map samples.
5980
+ * @param value - The UV channel.
5981
+ */
5982
+ set glossMapUv(value) {
5983
+ this._glossMapUv = value;
5984
+ if (this.material) {
5985
+ this.material.glossMapUv = value;
5986
+ this._scheduleUpdate();
5987
+ }
5988
+ }
5989
+ /**
5990
+ * Gets the UV channel the gloss map samples.
5991
+ * @returns The UV channel.
5992
+ */
5993
+ get glossMapUv() {
5994
+ return this._glossMapUv;
5995
+ }
5996
+ /**
5997
+ * Sets the id of the `pc-asset` to use as the height map, which drives parallax mapping.
5998
+ * @param value - The asset id.
5999
+ */
6000
+ set heightMap(value) {
6001
+ this._heightMap = value;
6002
+ this.setMap(value, 'heightMap');
6003
+ }
6004
+ /**
6005
+ * Gets the id of the `pc-asset` used as the height map.
6006
+ * @returns The asset id.
6007
+ */
6008
+ get heightMap() {
6009
+ return this._heightMap;
6010
+ }
6011
+ /**
6012
+ * Sets the color channel of the height map to sample.
6013
+ * @param value - The channel.
6014
+ */
6015
+ set heightMapChannel(value) {
6016
+ this._heightMapChannel = value;
6017
+ if (this.material) {
6018
+ this.material.heightMapChannel = value;
6019
+ this._scheduleUpdate();
6020
+ }
6021
+ }
6022
+ /**
6023
+ * Gets the color channel of the height map to sample.
6024
+ * @returns The channel.
6025
+ */
6026
+ get heightMapChannel() {
6027
+ return this._heightMapChannel;
6028
+ }
6029
+ /**
6030
+ * Sets the strength of the parallax effect driven by the height map.
6031
+ * @param value - The height map factor.
6032
+ */
6033
+ set heightMapFactor(value) {
6034
+ this._heightMapFactor = value;
6035
+ if (this.material) {
6036
+ this.material.heightMapFactor = value;
6037
+ this._scheduleUpdate();
6038
+ }
6039
+ }
6040
+ /**
6041
+ * Gets the strength of the parallax effect driven by the height map.
6042
+ * @returns The height map factor.
6043
+ */
6044
+ get heightMapFactor() {
6045
+ return this._heightMapFactor;
6046
+ }
6047
+ /**
6048
+ * Sets the 2D offset of the height map.
6049
+ * @param value - The offset.
6050
+ */
6051
+ set heightMapOffset(value) {
6052
+ this._heightMapOffset = value;
6053
+ if (this.material) {
6054
+ this.material.heightMapOffset = value;
6055
+ this._scheduleUpdate();
6056
+ }
6057
+ }
6058
+ /**
6059
+ * Gets the 2D offset of the height map.
6060
+ * @returns The offset.
6061
+ */
6062
+ get heightMapOffset() {
6063
+ return this._heightMapOffset;
6064
+ }
6065
+ /**
6066
+ * Sets the 2D rotation of the height map, in degrees.
6067
+ * @param value - The rotation.
6068
+ */
6069
+ set heightMapRotation(value) {
6070
+ this._heightMapRotation = value;
6071
+ if (this.material) {
6072
+ this.material.heightMapRotation = value;
6073
+ this._scheduleUpdate();
6074
+ }
6075
+ }
6076
+ /**
6077
+ * Gets the 2D rotation of the height map.
6078
+ * @returns The rotation.
6079
+ */
6080
+ get heightMapRotation() {
6081
+ return this._heightMapRotation;
6082
+ }
6083
+ /**
6084
+ * Sets the 2D tiling of the height map.
6085
+ * @param value - The tiling.
6086
+ */
6087
+ set heightMapTiling(value) {
6088
+ this._heightMapTiling = value;
6089
+ if (this.material) {
6090
+ this.material.heightMapTiling = value;
6091
+ this._scheduleUpdate();
6092
+ }
6093
+ }
6094
+ /**
6095
+ * Gets the 2D tiling of the height map.
6096
+ * @returns The tiling.
6097
+ */
6098
+ get heightMapTiling() {
6099
+ return this._heightMapTiling;
6100
+ }
6101
+ /**
6102
+ * Sets the UV channel the height map samples.
6103
+ * @param value - The UV channel.
6104
+ */
6105
+ set heightMapUv(value) {
6106
+ this._heightMapUv = value;
6107
+ if (this.material) {
6108
+ this.material.heightMapUv = value;
6109
+ this._scheduleUpdate();
6110
+ }
6111
+ }
6112
+ /**
6113
+ * Gets the UV channel the height map samples.
6114
+ * @returns The UV channel.
6115
+ */
6116
+ get heightMapUv() {
6117
+ return this._heightMapUv;
6118
+ }
6119
+ /**
6120
+ * Sets how metallic the surface is, from 0 (dielectric) to 1 (metal).
6121
+ * @param value - The metalness.
6122
+ */
6123
+ set metalness(value) {
6124
+ this._metalness = value;
6125
+ if (this.material) {
6126
+ this.material.metalness = value;
6127
+ this._scheduleUpdate();
6128
+ }
6129
+ }
6130
+ /**
6131
+ * Gets how metallic the surface is.
6132
+ * @returns The metalness.
6133
+ */
6134
+ get metalness() {
6135
+ return this._metalness;
6136
+ }
6137
+ /**
6138
+ * Sets the id of the `pc-asset` to use as the metalness map.
6139
+ * @param value - The asset id.
6140
+ */
6141
+ set metalnessMap(value) {
6142
+ this._metalnessMap = value;
6143
+ this.setMap(value, 'metalnessMap');
6144
+ }
6145
+ /**
6146
+ * Gets the id of the `pc-asset` used as the metalness map.
6147
+ * @returns The asset id.
6148
+ */
6149
+ get metalnessMap() {
6150
+ return this._metalnessMap;
6151
+ }
6152
+ /**
6153
+ * Sets the color channel of the metalness map to sample.
6154
+ * @param value - The channel.
6155
+ */
6156
+ set metalnessMapChannel(value) {
6157
+ this._metalnessMapChannel = value;
6158
+ if (this.material) {
6159
+ this.material.metalnessMapChannel = value;
6160
+ this._scheduleUpdate();
6161
+ }
6162
+ }
6163
+ /**
6164
+ * Gets the color channel of the metalness map to sample.
6165
+ * @returns The channel.
6166
+ */
6167
+ get metalnessMapChannel() {
6168
+ return this._metalnessMapChannel;
6169
+ }
6170
+ /**
6171
+ * Sets the 2D offset of the metalness map.
6172
+ * @param value - The offset.
6173
+ */
6174
+ set metalnessMapOffset(value) {
6175
+ this._metalnessMapOffset = value;
6176
+ if (this.material) {
6177
+ this.material.metalnessMapOffset = value;
6178
+ this._scheduleUpdate();
6179
+ }
6180
+ }
6181
+ /**
6182
+ * Gets the 2D offset of the metalness map.
6183
+ * @returns The offset.
6184
+ */
6185
+ get metalnessMapOffset() {
6186
+ return this._metalnessMapOffset;
6187
+ }
6188
+ /**
6189
+ * Sets the 2D rotation of the metalness map, in degrees.
6190
+ * @param value - The rotation.
6191
+ */
6192
+ set metalnessMapRotation(value) {
6193
+ this._metalnessMapRotation = value;
6194
+ if (this.material) {
6195
+ this.material.metalnessMapRotation = value;
6196
+ this._scheduleUpdate();
6197
+ }
6198
+ }
6199
+ /**
6200
+ * Gets the 2D rotation of the metalness map.
6201
+ * @returns The rotation.
6202
+ */
6203
+ get metalnessMapRotation() {
6204
+ return this._metalnessMapRotation;
6205
+ }
6206
+ /**
6207
+ * Sets the 2D tiling of the metalness map.
6208
+ * @param value - The tiling.
6209
+ */
6210
+ set metalnessMapTiling(value) {
6211
+ this._metalnessMapTiling = value;
6212
+ if (this.material) {
6213
+ this.material.metalnessMapTiling = value;
6214
+ this._scheduleUpdate();
6215
+ }
6216
+ }
6217
+ /**
6218
+ * Gets the 2D tiling of the metalness map.
6219
+ * @returns The tiling.
6220
+ */
6221
+ get metalnessMapTiling() {
6222
+ return this._metalnessMapTiling;
6223
+ }
6224
+ /**
6225
+ * Sets the UV channel the metalness map samples.
6226
+ * @param value - The UV channel.
6227
+ */
6228
+ set metalnessMapUv(value) {
6229
+ this._metalnessMapUv = value;
6230
+ if (this.material) {
6231
+ this.material.metalnessMapUv = value;
6232
+ this._scheduleUpdate();
6233
+ }
6234
+ }
6235
+ /**
6236
+ * Gets the UV channel the metalness map samples.
6237
+ * @returns The UV channel.
6238
+ */
6239
+ get metalnessMapUv() {
6240
+ return this._metalnessMapUv;
6241
+ }
6242
+ /**
6243
+ * Sets the id of the `pc-asset` to use as the normal map.
6244
+ * @param value - The asset id.
6245
+ */
6246
+ set normalMap(value) {
6247
+ this._normalMap = value;
6248
+ this.setMap(value, 'normalMap');
6249
+ }
6250
+ /**
6251
+ * Gets the id of the `pc-asset` used as the normal map.
6252
+ * @returns The asset id.
6253
+ */
6254
+ get normalMap() {
6255
+ return this._normalMap;
6256
+ }
6257
+ /**
6258
+ * Sets the 2D offset of the normal map.
6259
+ * @param value - The offset.
6260
+ */
6261
+ set normalMapOffset(value) {
6262
+ this._normalMapOffset = value;
6263
+ if (this.material) {
6264
+ this.material.normalMapOffset = value;
6265
+ this._scheduleUpdate();
6266
+ }
6267
+ }
6268
+ /**
6269
+ * Gets the 2D offset of the normal map.
6270
+ * @returns The offset.
6271
+ */
6272
+ get normalMapOffset() {
6273
+ return this._normalMapOffset;
6274
+ }
6275
+ /**
6276
+ * Sets the 2D rotation of the normal map, in degrees.
6277
+ * @param value - The rotation.
6278
+ */
6279
+ set normalMapRotation(value) {
6280
+ this._normalMapRotation = value;
6281
+ if (this.material) {
6282
+ this.material.normalMapRotation = value;
6283
+ this._scheduleUpdate();
6284
+ }
6285
+ }
6286
+ /**
6287
+ * Gets the 2D rotation of the normal map.
6288
+ * @returns The rotation.
6289
+ */
6290
+ get normalMapRotation() {
6291
+ return this._normalMapRotation;
6292
+ }
6293
+ /**
6294
+ * Sets the 2D tiling of the normal map.
6295
+ * @param value - The tiling.
6296
+ */
6297
+ set normalMapTiling(value) {
6298
+ this._normalMapTiling = value;
6299
+ if (this.material) {
6300
+ this.material.normalMapTiling = value;
6301
+ this._scheduleUpdate();
6302
+ }
6303
+ }
6304
+ /**
6305
+ * Gets the 2D tiling of the normal map.
6306
+ * @returns The tiling.
6307
+ */
6308
+ get normalMapTiling() {
6309
+ return this._normalMapTiling;
6310
+ }
6311
+ /**
6312
+ * Sets the UV channel the normal map samples.
6313
+ * @param value - The UV channel.
6314
+ */
6315
+ set normalMapUv(value) {
6316
+ this._normalMapUv = value;
6317
+ if (this.material) {
6318
+ this.material.normalMapUv = value;
6319
+ this._scheduleUpdate();
6320
+ }
6321
+ }
6322
+ /**
6323
+ * Gets the UV channel the normal map samples.
6324
+ * @returns The UV channel.
6325
+ */
6326
+ get normalMapUv() {
6327
+ return this._normalMapUv;
6328
+ }
6329
+ /**
6330
+ * Sets whether ambient occlusion also attenuates direct lighting.
6331
+ * @param value - The occlude direct flag.
6332
+ */
6333
+ set occludeDirect(value) {
6334
+ this._occludeDirect = value;
6335
+ if (this.material) {
6336
+ // @ts-ignore see createMaterial() - the engine mistypes occludeDirect as a number
6337
+ this.material.occludeDirect = value;
6338
+ this._scheduleUpdate();
6339
+ }
6340
+ }
6341
+ /**
6342
+ * Gets whether ambient occlusion also attenuates direct lighting.
6343
+ * @returns The occlude direct flag.
6344
+ */
6345
+ get occludeDirect() {
6346
+ return this._occludeDirect;
6347
+ }
6348
+ /**
6349
+ * Sets how specular reflections are occluded.
6350
+ * @param value - The specular occlusion mode.
6351
+ */
6352
+ set occludeSpecular(value) {
6353
+ this._occludeSpecular = value;
6354
+ if (this.material) {
6355
+ this.material.occludeSpecular = occludeSpeculars.get(value) ?? playcanvas.SPECOCC_AO;
6356
+ this._scheduleUpdate();
6357
+ }
6358
+ }
6359
+ /**
6360
+ * Gets how specular reflections are occluded.
6361
+ * @returns The specular occlusion mode.
6362
+ */
6363
+ get occludeSpecular() {
6364
+ return this._occludeSpecular;
6365
+ }
6366
+ /**
6367
+ * Sets the opacity of the material, from 0 (transparent) to 1 (opaque), which requires a
6368
+ * `blend-type` other than `none` to have any visible effect.
6369
+ * @param value - The opacity.
6370
+ */
6371
+ set opacity(value) {
6372
+ this._opacity = value;
6373
+ if (this.material) {
6374
+ this.material.opacity = value;
6375
+ this._scheduleUpdate();
6376
+ }
6377
+ }
6378
+ /**
6379
+ * Gets the opacity of the material, which requires a `blend-type` other than `none` to have
6380
+ * any visible effect.
6381
+ * @returns The opacity.
6382
+ */
6383
+ get opacity() {
6384
+ return this._opacity;
6385
+ }
6386
+ /**
6387
+ * Sets the dithering used to render opacity, which approximates transparency without blending.
6388
+ * @param value - The dither mode.
6389
+ */
6390
+ set opacityDither(value) {
6391
+ this._opacityDither = value;
6392
+ if (this.material) {
6393
+ this.material.opacityDither = value;
6394
+ this._scheduleUpdate();
6395
+ }
6396
+ }
6397
+ /**
6398
+ * Gets the dithering used to render opacity.
6399
+ * @returns The dither mode.
6400
+ */
6401
+ get opacityDither() {
6402
+ return this._opacityDither;
6403
+ }
6404
+ /**
6405
+ * Sets whether specular highlights fade out as the material becomes transparent.
6406
+ * @param value - The opacity fades specular flag.
6407
+ */
6408
+ set opacityFadesSpecular(value) {
6409
+ this._opacityFadesSpecular = value;
6410
+ if (this.material) {
6411
+ this.material.opacityFadesSpecular = value;
6412
+ this._scheduleUpdate();
6413
+ }
6414
+ }
6415
+ /**
6416
+ * Gets whether specular highlights fade out as the material becomes transparent.
6417
+ * @returns The opacity fades specular flag.
6418
+ */
6419
+ get opacityFadesSpecular() {
6420
+ return this._opacityFadesSpecular;
6421
+ }
6422
+ /**
6423
+ * Sets the id of the `pc-asset` to use as the opacity map.
6424
+ * @param value - The asset id.
6425
+ */
6426
+ set opacityMap(value) {
6427
+ this._opacityMap = value;
6428
+ this.setMap(value, 'opacityMap');
6429
+ }
6430
+ /**
6431
+ * Gets the id of the `pc-asset` used as the opacity map.
6432
+ * @returns The asset id.
6433
+ */
6434
+ get opacityMap() {
6435
+ return this._opacityMap;
6436
+ }
6437
+ /**
6438
+ * Sets the color channel of the opacity map to sample.
6439
+ * @param value - The channel.
6440
+ */
6441
+ set opacityMapChannel(value) {
6442
+ this._opacityMapChannel = value;
6443
+ if (this.material) {
6444
+ this.material.opacityMapChannel = value;
6445
+ this._scheduleUpdate();
6446
+ }
6447
+ }
6448
+ /**
6449
+ * Gets the color channel of the opacity map to sample.
6450
+ * @returns The channel.
6451
+ */
6452
+ get opacityMapChannel() {
6453
+ return this._opacityMapChannel;
6454
+ }
6455
+ /**
6456
+ * Sets the 2D offset of the opacity map.
6457
+ * @param value - The offset.
6458
+ */
6459
+ set opacityMapOffset(value) {
6460
+ this._opacityMapOffset = value;
6461
+ if (this.material) {
6462
+ this.material.opacityMapOffset = value;
6463
+ this._scheduleUpdate();
6464
+ }
6465
+ }
6466
+ /**
6467
+ * Gets the 2D offset of the opacity map.
6468
+ * @returns The offset.
6469
+ */
6470
+ get opacityMapOffset() {
6471
+ return this._opacityMapOffset;
6472
+ }
6473
+ /**
6474
+ * Sets the 2D rotation of the opacity map, in degrees.
6475
+ * @param value - The rotation.
6476
+ */
6477
+ set opacityMapRotation(value) {
6478
+ this._opacityMapRotation = value;
6479
+ if (this.material) {
6480
+ this.material.opacityMapRotation = value;
6481
+ this._scheduleUpdate();
6482
+ }
6483
+ }
6484
+ /**
6485
+ * Gets the 2D rotation of the opacity map.
6486
+ * @returns The rotation.
6487
+ */
6488
+ get opacityMapRotation() {
6489
+ return this._opacityMapRotation;
6490
+ }
6491
+ /**
6492
+ * Sets the 2D tiling of the opacity map.
6493
+ * @param value - The tiling.
6494
+ */
6495
+ set opacityMapTiling(value) {
6496
+ this._opacityMapTiling = value;
6497
+ if (this.material) {
6498
+ this.material.opacityMapTiling = value;
6499
+ this._scheduleUpdate();
6500
+ }
6501
+ }
6502
+ /**
6503
+ * Gets the 2D tiling of the opacity map.
6504
+ * @returns The tiling.
6505
+ */
6506
+ get opacityMapTiling() {
6507
+ return this._opacityMapTiling;
6508
+ }
6509
+ /**
6510
+ * Sets the UV channel the opacity map samples.
6511
+ * @param value - The UV channel.
6512
+ */
6513
+ set opacityMapUv(value) {
6514
+ this._opacityMapUv = value;
6515
+ if (this.material) {
6516
+ this.material.opacityMapUv = value;
6517
+ this._scheduleUpdate();
6518
+ }
6519
+ }
6520
+ /**
6521
+ * Gets the UV channel the opacity map samples.
6522
+ * @returns The UV channel.
6523
+ */
6524
+ get opacityMapUv() {
6525
+ return this._opacityMapUv;
6526
+ }
6527
+ /**
6528
+ * Sets the roughness of the material, from 0 (shiny) to 1 (rough). This is an alias for `gloss`
6529
+ * that also inverts the gloss channel, so do not combine it with the `gloss` attributes.
6530
+ * @param value - The roughness.
6531
+ */
6532
+ set roughness(value) {
6533
+ this.gloss = value;
6534
+ this.glossInvert = true;
6535
+ }
6536
+ /**
6537
+ * Gets the roughness of the material.
6538
+ * @returns The roughness.
6539
+ */
6540
+ get roughness() {
6541
+ return this._gloss;
6542
+ }
6543
+ /**
6544
+ * Sets the id of the `pc-asset` to use as the roughness map. This is an alias for `glossMap`
6545
+ * that also inverts the gloss channel, so do not combine it with the `gloss` attributes.
6546
+ * @param value - The asset id.
6547
+ */
6548
+ set roughnessMap(value) {
6549
+ this.glossMap = value;
6550
+ this.glossInvert = true;
6551
+ }
6552
+ /**
6553
+ * Gets the id of the `pc-asset` used as the roughness map.
6554
+ * @returns The asset id.
6555
+ */
6556
+ get roughnessMap() {
6557
+ return this._glossMap;
6558
+ }
6559
+ /**
6560
+ * Sets the depth offset applied in proportion to a surface's slope, used to resolve z-fighting.
6561
+ * @param value - The slope depth bias.
6562
+ */
6563
+ set slopeDepthBias(value) {
6564
+ this._slopeDepthBias = value;
6565
+ if (this.material) {
6566
+ this.material.slopeDepthBias = value;
6567
+ this._scheduleUpdate();
6568
+ }
6569
+ }
6570
+ /**
6571
+ * Gets the depth offset applied in proportion to a surface's slope.
6572
+ * @returns The slope depth bias.
6573
+ */
6574
+ get slopeDepthBias() {
6575
+ return this._slopeDepthBias;
6576
+ }
6577
+ /**
6578
+ * Sets the specular color of the material, which applies only when the metalness workflow is
6579
+ * disabled or `use-metalness-specular-color` is enabled.
6580
+ * @param value - The specular color.
6581
+ */
6582
+ set specular(value) {
6583
+ this._specular = value;
6584
+ if (this.material) {
6585
+ this.material.specular = value;
6586
+ this._scheduleUpdate();
6587
+ }
6588
+ }
6589
+ /**
6590
+ * Gets the specular color of the material, which applies only when the metalness workflow is
6591
+ * disabled or `use-metalness-specular-color` is enabled.
6592
+ * @returns The specular color.
6593
+ */
6594
+ get specular() {
6595
+ return this._specular;
6596
+ }
6597
+ /**
6598
+ * Sets the strength of specular reflections at direct angles, from 0 to 1, which applies only
6599
+ * when `use-metalness-specular-color` is enabled.
6600
+ * @param value - The specularity factor.
6601
+ */
6602
+ set specularityFactor(value) {
6603
+ this._specularityFactor = value;
6604
+ if (this.material) {
6605
+ this.material.specularityFactor = value;
6606
+ this._scheduleUpdate();
4654
6607
  }
4655
6608
  }
4656
- createMaterial() {
4657
- this.material = new playcanvas.StandardMaterial();
4658
- this.material.glossInvert = false;
4659
- this.material.useMetalness = false;
4660
- this.material.diffuse = this._diffuse;
4661
- this.diffuseMap = this._diffuseMap;
4662
- this.metalnessMap = this._metalnessMap;
4663
- this.normalMap = this._normalMap;
4664
- this.roughnessMap = this._roughnessMap;
4665
- this.material.update();
6609
+ /**
6610
+ * Gets the strength of specular reflections at direct angles, which applies only when
6611
+ * `use-metalness-specular-color` is enabled.
6612
+ * @returns The specularity factor.
6613
+ */
6614
+ get specularityFactor() {
6615
+ return this._specularityFactor;
4666
6616
  }
4667
- disconnectedCallback() {
6617
+ /**
6618
+ * Sets whether back faces are lit as though their normals were flipped.
6619
+ * @param value - The two sided lighting flag.
6620
+ */
6621
+ set twoSidedLighting(value) {
6622
+ this._twoSidedLighting = value;
4668
6623
  if (this.material) {
4669
- this.material.destroy();
4670
- this.material = null;
6624
+ this.material.twoSidedLighting = value;
6625
+ this._scheduleUpdate();
4671
6626
  }
4672
6627
  }
4673
- setMap(map, property) {
6628
+ /**
6629
+ * Gets whether back faces are lit as though their normals were flipped.
6630
+ * @returns The two sided lighting flag.
6631
+ */
6632
+ get twoSidedLighting() {
6633
+ return this._twoSidedLighting;
6634
+ }
6635
+ /**
6636
+ * Sets whether the material is affected by scene fog.
6637
+ * @param value - The use fog flag.
6638
+ */
6639
+ set useFog(value) {
6640
+ this._useFog = value;
4674
6641
  if (this.material) {
4675
- const asset = AssetElement.get(map);
4676
- if (asset) {
4677
- if (asset.loaded) {
4678
- this.material[property] = asset.resource;
4679
- this.material[property].anisotropy = 4;
4680
- }
4681
- else {
4682
- asset.once('load', () => {
4683
- this.material[property] = asset.resource;
4684
- this.material[property].anisotropy = 4;
4685
- this.material.update();
4686
- });
4687
- }
4688
- }
6642
+ this.material.useFog = value;
6643
+ this._scheduleUpdate();
4689
6644
  }
4690
6645
  }
4691
- set diffuse(value) {
4692
- this._diffuse = value;
6646
+ /**
6647
+ * Gets whether the material is affected by scene fog.
6648
+ * @returns The use fog flag.
6649
+ */
6650
+ get useFog() {
6651
+ return this._useFog;
6652
+ }
6653
+ /**
6654
+ * Sets whether the material is affected by scene lights. When disabled the material renders
6655
+ * unlit, using the diffuse color and map alone.
6656
+ * @param value - The use lighting flag.
6657
+ */
6658
+ set useLighting(value) {
6659
+ this._useLighting = value;
4693
6660
  if (this.material) {
4694
- this.material.diffuse = value;
6661
+ this.material.useLighting = value;
6662
+ this._scheduleUpdate();
4695
6663
  }
4696
6664
  }
4697
- get diffuse() {
4698
- return this._diffuse;
6665
+ /**
6666
+ * Gets whether the material is affected by scene lights.
6667
+ * @returns The use lighting flag.
6668
+ */
6669
+ get useLighting() {
6670
+ return this._useLighting;
4699
6671
  }
4700
- set diffuseMap(value) {
4701
- this._diffuseMap = value;
4702
- this.setMap(value, 'diffuseMap');
6672
+ /**
6673
+ * Sets whether to use the metalness workflow rather than the older specular workflow. Unlike a
6674
+ * bare `StandardMaterial` this defaults to `true`, because the `metalness-*` attributes have no
6675
+ * effect without it.
6676
+ * @param value - The use metalness flag.
6677
+ */
6678
+ set useMetalness(value) {
6679
+ this._useMetalness = value;
6680
+ if (this.material) {
6681
+ this.material.useMetalness = value;
6682
+ this._scheduleUpdate();
6683
+ }
4703
6684
  }
4704
- get diffuseMap() {
4705
- return this._diffuseMap;
6685
+ /**
6686
+ * Gets whether to use the metalness workflow.
6687
+ * @returns The use metalness flag.
6688
+ */
6689
+ get useMetalness() {
6690
+ return this._useMetalness;
4706
6691
  }
4707
- set metalnessMap(value) {
4708
- this._metalnessMap = value;
4709
- this.setMap(value, 'metalnessMap');
6692
+ /**
6693
+ * Sets whether the specular color tints reflections while the metalness workflow is in use.
6694
+ * @param value - The use metalness specular color flag.
6695
+ */
6696
+ set useMetalnessSpecularColor(value) {
6697
+ this._useMetalnessSpecularColor = value;
6698
+ if (this.material) {
6699
+ this.material.useMetalnessSpecularColor = value;
6700
+ this._scheduleUpdate();
6701
+ }
4710
6702
  }
4711
- get metalnessMap() {
4712
- return this._metalnessMap;
6703
+ /**
6704
+ * Gets whether the specular color tints reflections while the metalness workflow is in use.
6705
+ * @returns The use metalness specular color flag.
6706
+ */
6707
+ get useMetalnessSpecularColor() {
6708
+ return this._useMetalnessSpecularColor;
4713
6709
  }
4714
- set normalMap(value) {
4715
- this._normalMap = value;
4716
- this.setMap(value, 'normalMap');
6710
+ /**
6711
+ * Sets whether the material is lit by the scene's skybox.
6712
+ * @param value - The use skybox flag.
6713
+ */
6714
+ set useSkybox(value) {
6715
+ this._useSkybox = value;
6716
+ if (this.material) {
6717
+ this.material.useSkybox = value;
6718
+ this._scheduleUpdate();
6719
+ }
4717
6720
  }
4718
- get normalMap() {
4719
- return this._normalMap;
6721
+ /**
6722
+ * Gets whether the material is lit by the scene's skybox.
6723
+ * @returns The use skybox flag.
6724
+ */
6725
+ get useSkybox() {
6726
+ return this._useSkybox;
4720
6727
  }
4721
- set roughnessMap(value) {
4722
- this._roughnessMap = value;
4723
- this.setMap(value, 'glossMap');
6728
+ /**
6729
+ * Sets whether the camera's tone mapping is applied to the material.
6730
+ * @param value - The use tonemap flag.
6731
+ */
6732
+ set useTonemap(value) {
6733
+ this._useTonemap = value;
6734
+ if (this.material) {
6735
+ this.material.useTonemap = value;
6736
+ this._scheduleUpdate();
6737
+ }
4724
6738
  }
4725
- get roughnessMap() {
4726
- return this._roughnessMap;
6739
+ /**
6740
+ * Gets whether the camera's tone mapping is applied to the material.
6741
+ * @returns The use tonemap flag.
6742
+ */
6743
+ get useTonemap() {
6744
+ return this._useTonemap;
4727
6745
  }
4728
6746
  static get(id) {
4729
6747
  const materialElement = document.querySelector(`pc-material[id="${id}"]`);
4730
- return materialElement === null || materialElement === void 0 ? void 0 : materialElement.material;
6748
+ return materialElement?.material;
4731
6749
  }
4732
6750
  static get observedAttributes() {
4733
- return ['diffuse', 'diffuse-map', 'metalness-map', 'normal-map', 'roughness-map'];
6751
+ return [
6752
+ 'alpha-test',
6753
+ 'alpha-to-coverage',
6754
+ 'ao-intensity',
6755
+ 'ao-map',
6756
+ 'ao-map-channel',
6757
+ 'ao-map-offset',
6758
+ 'ao-map-rotation',
6759
+ 'ao-map-tiling',
6760
+ 'ao-map-uv',
6761
+ 'blend-type',
6762
+ 'bumpiness',
6763
+ 'cull',
6764
+ 'depth-bias',
6765
+ 'depth-test',
6766
+ 'depth-write',
6767
+ 'diffuse',
6768
+ 'diffuse-map',
6769
+ 'diffuse-map-channel',
6770
+ 'diffuse-map-offset',
6771
+ 'diffuse-map-rotation',
6772
+ 'diffuse-map-tiling',
6773
+ 'diffuse-map-uv',
6774
+ 'emissive',
6775
+ 'emissive-intensity',
6776
+ 'emissive-map',
6777
+ 'emissive-map-channel',
6778
+ 'emissive-map-offset',
6779
+ 'emissive-map-rotation',
6780
+ 'emissive-map-tiling',
6781
+ 'emissive-map-uv',
6782
+ 'enable-ggx-specular',
6783
+ 'fresnel-model',
6784
+ 'gloss',
6785
+ 'gloss-invert',
6786
+ 'gloss-map',
6787
+ 'gloss-map-channel',
6788
+ 'gloss-map-offset',
6789
+ 'gloss-map-rotation',
6790
+ 'gloss-map-tiling',
6791
+ 'gloss-map-uv',
6792
+ 'height-map',
6793
+ 'height-map-channel',
6794
+ 'height-map-factor',
6795
+ 'height-map-offset',
6796
+ 'height-map-rotation',
6797
+ 'height-map-tiling',
6798
+ 'height-map-uv',
6799
+ 'metalness',
6800
+ 'metalness-map',
6801
+ 'metalness-map-channel',
6802
+ 'metalness-map-offset',
6803
+ 'metalness-map-rotation',
6804
+ 'metalness-map-tiling',
6805
+ 'metalness-map-uv',
6806
+ 'normal-map',
6807
+ 'normal-map-offset',
6808
+ 'normal-map-rotation',
6809
+ 'normal-map-tiling',
6810
+ 'normal-map-uv',
6811
+ 'occlude-direct',
6812
+ 'occlude-specular',
6813
+ 'opacity',
6814
+ 'opacity-dither',
6815
+ 'opacity-fades-specular',
6816
+ 'opacity-map',
6817
+ 'opacity-map-channel',
6818
+ 'opacity-map-offset',
6819
+ 'opacity-map-rotation',
6820
+ 'opacity-map-tiling',
6821
+ 'opacity-map-uv',
6822
+ 'roughness',
6823
+ 'roughness-map',
6824
+ 'slope-depth-bias',
6825
+ 'specular',
6826
+ 'specularity-factor',
6827
+ 'two-sided-lighting',
6828
+ 'use-fog',
6829
+ 'use-lighting',
6830
+ 'use-metalness',
6831
+ 'use-metalness-specular-color',
6832
+ 'use-skybox',
6833
+ 'use-tonemap'
6834
+ ];
4734
6835
  }
6836
+ // newValue is null when an attribute is removed, which several branches below rely on. The
6837
+ // other elements still declare it as `string`; widening those surfaces 21 real removal bugs of
6838
+ // the #309 shape, which is its own change rather than a signature tweak.
4735
6839
  attributeChangedCallback(name, _oldValue, newValue) {
4736
6840
  switch (name) {
6841
+ case 'alpha-test':
6842
+ this.alphaTest = parseNumber(newValue, 0, name);
6843
+ break;
6844
+ case 'alpha-to-coverage':
6845
+ this.alphaToCoverage = parseBool(newValue, false);
6846
+ break;
6847
+ case 'ao-intensity':
6848
+ this.aoIntensity = parseNumber(newValue, 1, name);
6849
+ break;
6850
+ case 'ao-map':
6851
+ this.aoMap = newValue ?? '';
6852
+ break;
6853
+ case 'ao-map-channel':
6854
+ this.aoMapChannel = parseEnum(newValue, scalarChannels, 'g', name);
6855
+ break;
6856
+ case 'ao-map-offset':
6857
+ this.aoMapOffset = parseVec2(newValue, new playcanvas.Vec2(0, 0), name);
6858
+ break;
6859
+ case 'ao-map-rotation':
6860
+ this.aoMapRotation = parseNumber(newValue, 0, name);
6861
+ break;
6862
+ case 'ao-map-tiling':
6863
+ this.aoMapTiling = parseVec2(newValue, new playcanvas.Vec2(1, 1), name);
6864
+ break;
6865
+ case 'ao-map-uv':
6866
+ this.aoMapUv = parseNumber(newValue, 0, name);
6867
+ break;
6868
+ case 'blend-type':
6869
+ this.blendType = parseEnum(newValue, blendTypes, 'none', name);
6870
+ break;
6871
+ case 'bumpiness':
6872
+ this.bumpiness = parseNumber(newValue, 1, name);
6873
+ break;
6874
+ case 'cull':
6875
+ this.cull = parseEnum(newValue, cullModes, 'back', name);
6876
+ break;
6877
+ case 'depth-bias':
6878
+ this.depthBias = parseNumber(newValue, 0, name);
6879
+ break;
6880
+ case 'depth-test':
6881
+ this.depthTest = parseBool(newValue, true);
6882
+ break;
6883
+ case 'depth-write':
6884
+ this.depthWrite = parseBool(newValue, true);
6885
+ break;
4737
6886
  case 'diffuse':
4738
- this.diffuse = parseColor(newValue, playcanvas.Color.WHITE, name);
6887
+ this.diffuse = parseColor(newValue, new playcanvas.Color(1, 1, 1), name);
4739
6888
  break;
4740
6889
  case 'diffuse-map':
4741
- this.diffuseMap = newValue;
6890
+ this.diffuseMap = newValue ?? '';
6891
+ break;
6892
+ case 'diffuse-map-channel':
6893
+ this.diffuseMapChannel = parseEnum(newValue, colorChannels, 'rgb', name);
6894
+ break;
6895
+ case 'diffuse-map-offset':
6896
+ this.diffuseMapOffset = parseVec2(newValue, new playcanvas.Vec2(0, 0), name);
6897
+ break;
6898
+ case 'diffuse-map-rotation':
6899
+ this.diffuseMapRotation = parseNumber(newValue, 0, name);
6900
+ break;
6901
+ case 'diffuse-map-tiling':
6902
+ this.diffuseMapTiling = parseVec2(newValue, new playcanvas.Vec2(1, 1), name);
6903
+ break;
6904
+ case 'diffuse-map-uv':
6905
+ this.diffuseMapUv = parseNumber(newValue, 0, name);
6906
+ break;
6907
+ case 'emissive':
6908
+ this.emissive = parseColor(newValue, new playcanvas.Color(0, 0, 0), name);
6909
+ break;
6910
+ case 'emissive-intensity':
6911
+ this.emissiveIntensity = parseNumber(newValue, 1, name);
6912
+ break;
6913
+ case 'emissive-map':
6914
+ this.emissiveMap = newValue ?? '';
6915
+ break;
6916
+ case 'emissive-map-channel':
6917
+ this.emissiveMapChannel = parseEnum(newValue, colorChannels, 'rgb', name);
6918
+ break;
6919
+ case 'emissive-map-offset':
6920
+ this.emissiveMapOffset = parseVec2(newValue, new playcanvas.Vec2(0, 0), name);
6921
+ break;
6922
+ case 'emissive-map-rotation':
6923
+ this.emissiveMapRotation = parseNumber(newValue, 0, name);
6924
+ break;
6925
+ case 'emissive-map-tiling':
6926
+ this.emissiveMapTiling = parseVec2(newValue, new playcanvas.Vec2(1, 1), name);
6927
+ break;
6928
+ case 'emissive-map-uv':
6929
+ this.emissiveMapUv = parseNumber(newValue, 0, name);
6930
+ break;
6931
+ case 'enable-ggx-specular':
6932
+ this.enableGGXSpecular = parseBool(newValue, false);
6933
+ break;
6934
+ case 'fresnel-model':
6935
+ this.fresnelModel = parseEnum(newValue, fresnelModels, 'schlick', name);
6936
+ break;
6937
+ case 'gloss':
6938
+ this.gloss = parseNumber(newValue, 0.25, name);
6939
+ this._warnGlossConflict();
6940
+ break;
6941
+ case 'gloss-invert':
6942
+ this.glossInvert = parseBool(newValue, false);
6943
+ this._warnGlossConflict();
6944
+ break;
6945
+ case 'gloss-map':
6946
+ this.glossMap = newValue ?? '';
6947
+ this._warnGlossConflict();
6948
+ break;
6949
+ case 'gloss-map-channel':
6950
+ this.glossMapChannel = parseEnum(newValue, scalarChannels, 'g', name);
6951
+ break;
6952
+ case 'gloss-map-offset':
6953
+ this.glossMapOffset = parseVec2(newValue, new playcanvas.Vec2(0, 0), name);
6954
+ break;
6955
+ case 'gloss-map-rotation':
6956
+ this.glossMapRotation = parseNumber(newValue, 0, name);
6957
+ break;
6958
+ case 'gloss-map-tiling':
6959
+ this.glossMapTiling = parseVec2(newValue, new playcanvas.Vec2(1, 1), name);
6960
+ break;
6961
+ case 'gloss-map-uv':
6962
+ this.glossMapUv = parseNumber(newValue, 0, name);
6963
+ break;
6964
+ case 'height-map':
6965
+ this.heightMap = newValue ?? '';
6966
+ break;
6967
+ case 'height-map-channel':
6968
+ this.heightMapChannel = parseEnum(newValue, scalarChannels, 'g', name);
6969
+ break;
6970
+ case 'height-map-factor':
6971
+ this.heightMapFactor = parseNumber(newValue, 1, name);
6972
+ break;
6973
+ case 'height-map-offset':
6974
+ this.heightMapOffset = parseVec2(newValue, new playcanvas.Vec2(0, 0), name);
6975
+ break;
6976
+ case 'height-map-rotation':
6977
+ this.heightMapRotation = parseNumber(newValue, 0, name);
6978
+ break;
6979
+ case 'height-map-tiling':
6980
+ this.heightMapTiling = parseVec2(newValue, new playcanvas.Vec2(1, 1), name);
6981
+ break;
6982
+ case 'height-map-uv':
6983
+ this.heightMapUv = parseNumber(newValue, 0, name);
6984
+ break;
6985
+ case 'metalness':
6986
+ this.metalness = parseNumber(newValue, 0, name);
4742
6987
  break;
4743
6988
  case 'metalness-map':
4744
- this.metalnessMap = newValue;
6989
+ this.metalnessMap = newValue ?? '';
6990
+ break;
6991
+ case 'metalness-map-channel':
6992
+ this.metalnessMapChannel = parseEnum(newValue, scalarChannels, 'g', name);
6993
+ break;
6994
+ case 'metalness-map-offset':
6995
+ this.metalnessMapOffset = parseVec2(newValue, new playcanvas.Vec2(0, 0), name);
6996
+ break;
6997
+ case 'metalness-map-rotation':
6998
+ this.metalnessMapRotation = parseNumber(newValue, 0, name);
6999
+ break;
7000
+ case 'metalness-map-tiling':
7001
+ this.metalnessMapTiling = parseVec2(newValue, new playcanvas.Vec2(1, 1), name);
7002
+ break;
7003
+ case 'metalness-map-uv':
7004
+ this.metalnessMapUv = parseNumber(newValue, 0, name);
4745
7005
  break;
4746
7006
  case 'normal-map':
4747
- this.normalMap = newValue;
7007
+ this.normalMap = newValue ?? '';
7008
+ break;
7009
+ case 'normal-map-offset':
7010
+ this.normalMapOffset = parseVec2(newValue, new playcanvas.Vec2(0, 0), name);
7011
+ break;
7012
+ case 'normal-map-rotation':
7013
+ this.normalMapRotation = parseNumber(newValue, 0, name);
7014
+ break;
7015
+ case 'normal-map-tiling':
7016
+ this.normalMapTiling = parseVec2(newValue, new playcanvas.Vec2(1, 1), name);
7017
+ break;
7018
+ case 'normal-map-uv':
7019
+ this.normalMapUv = parseNumber(newValue, 0, name);
7020
+ break;
7021
+ case 'occlude-direct':
7022
+ this.occludeDirect = parseBool(newValue, false);
7023
+ break;
7024
+ case 'occlude-specular':
7025
+ this.occludeSpecular = parseEnum(newValue, occludeSpeculars, 'ao', name);
7026
+ break;
7027
+ case 'opacity':
7028
+ this.opacity = parseNumber(newValue, 1, name);
7029
+ break;
7030
+ case 'opacity-dither':
7031
+ this.opacityDither = parseEnum(newValue, opacityDithers, 'none', name);
7032
+ break;
7033
+ case 'opacity-fades-specular':
7034
+ this.opacityFadesSpecular = parseBool(newValue, true);
7035
+ break;
7036
+ case 'opacity-map':
7037
+ this.opacityMap = newValue ?? '';
7038
+ break;
7039
+ case 'opacity-map-channel':
7040
+ this.opacityMapChannel = parseEnum(newValue, scalarChannels, 'a', name);
7041
+ break;
7042
+ case 'opacity-map-offset':
7043
+ this.opacityMapOffset = parseVec2(newValue, new playcanvas.Vec2(0, 0), name);
7044
+ break;
7045
+ case 'opacity-map-rotation':
7046
+ this.opacityMapRotation = parseNumber(newValue, 0, name);
7047
+ break;
7048
+ case 'opacity-map-tiling':
7049
+ this.opacityMapTiling = parseVec2(newValue, new playcanvas.Vec2(1, 1), name);
7050
+ break;
7051
+ case 'opacity-map-uv':
7052
+ this.opacityMapUv = parseNumber(newValue, 0, name);
7053
+ break;
7054
+ case 'roughness':
7055
+ // Aliases gloss, and inverts it so the value reads as roughness. Removing the
7056
+ // attribute restores the engine's uninverted interpretation.
7057
+ this.gloss = parseNumber(newValue, 0.25, name);
7058
+ this.glossInvert = newValue !== null;
7059
+ this._warnGlossConflict();
4748
7060
  break;
4749
7061
  case 'roughness-map':
4750
- this.roughnessMap = newValue;
7062
+ this.glossMap = newValue ?? '';
7063
+ this.glossInvert = newValue !== null;
7064
+ this._warnGlossConflict();
7065
+ break;
7066
+ case 'slope-depth-bias':
7067
+ this.slopeDepthBias = parseNumber(newValue, 0, name);
7068
+ break;
7069
+ case 'specular':
7070
+ this.specular = parseColor(newValue, new playcanvas.Color(0, 0, 0), name);
7071
+ break;
7072
+ case 'specularity-factor':
7073
+ this.specularityFactor = parseNumber(newValue, 1, name);
7074
+ break;
7075
+ case 'two-sided-lighting':
7076
+ this.twoSidedLighting = parseBool(newValue, false);
7077
+ break;
7078
+ case 'use-fog':
7079
+ this.useFog = parseBool(newValue, true);
7080
+ break;
7081
+ case 'use-lighting':
7082
+ this.useLighting = parseBool(newValue, true);
7083
+ break;
7084
+ case 'use-metalness':
7085
+ this.useMetalness = parseBool(newValue, true);
7086
+ break;
7087
+ case 'use-metalness-specular-color':
7088
+ this.useMetalnessSpecularColor = parseBool(newValue, false);
7089
+ break;
7090
+ case 'use-skybox':
7091
+ this.useSkybox = parseBool(newValue, true);
7092
+ break;
7093
+ case 'use-tonemap':
7094
+ this.useTonemap = parseBool(newValue, true);
4751
7095
  break;
4752
7096
  }
4753
7097
  }
@@ -4767,13 +7111,13 @@
4767
7111
  * @category Components
4768
7112
  */
4769
7113
  class RenderComponentElement extends ComponentElement {
7114
+ _castShadows = true;
7115
+ _material = '';
7116
+ _receiveShadows = true;
7117
+ _type = 'box';
4770
7118
  /** @ignore */
4771
7119
  constructor() {
4772
7120
  super('render');
4773
- this._castShadows = true;
4774
- this._material = '';
4775
- this._receiveShadows = true;
4776
- this._type = 'box';
4777
7121
  }
4778
7122
  getInitialComponentData() {
4779
7123
  return {
@@ -4830,8 +7174,13 @@
4830
7174
  */
4831
7175
  set material(value) {
4832
7176
  this._material = value;
4833
- if (this.component) {
4834
- this.component.material = MaterialElement.get(value);
7177
+ const material = MaterialElement.get(value);
7178
+ // Guarded like every other reference attribute in the library. Assigning an unresolved
7179
+ // lookup used to write `undefined` straight through to every mesh instance, and the
7180
+ // engine's MeshInstance setter takes that literally - it clears the material and skips
7181
+ // the ref/transparency/key bookkeeping, leaving the mesh with no material at all.
7182
+ if (this.component && material) {
7183
+ this.component.material = material;
4835
7184
  }
4836
7185
  }
4837
7186
  /**
@@ -4868,7 +7217,7 @@
4868
7217
  this.castShadows = parseBool(newValue, true);
4869
7218
  break;
4870
7219
  case 'material':
4871
- this.material = newValue;
7220
+ this.material = newValue ?? '';
4872
7221
  break;
4873
7222
  case 'receive-shadows':
4874
7223
  this.receiveShadows = parseBool(newValue, true);
@@ -4890,45 +7239,45 @@
4890
7239
  * @category Components
4891
7240
  */
4892
7241
  class RigidBodyComponentElement extends ComponentElement {
7242
+ /**
7243
+ * The angular damping of the rigidbody.
7244
+ */
7245
+ _angularDamping = 0;
7246
+ /**
7247
+ * The angular factor of the rigidbody.
7248
+ */
7249
+ _angularFactor = new playcanvas.Vec3(1, 1, 1);
7250
+ /**
7251
+ * The friction of the rigidbody.
7252
+ */
7253
+ _friction = 0.5;
7254
+ /**
7255
+ * The linear damping of the rigidbody.
7256
+ */
7257
+ _linearDamping = 0;
7258
+ /**
7259
+ * The linear factor of the rigidbody.
7260
+ */
7261
+ _linearFactor = new playcanvas.Vec3(1, 1, 1);
7262
+ /**
7263
+ * The mass of the rigidbody.
7264
+ */
7265
+ _mass = 1;
7266
+ /**
7267
+ * The restitution of the rigidbody.
7268
+ */
7269
+ _restitution = 0;
7270
+ /**
7271
+ * The rolling friction of the rigidbody.
7272
+ */
7273
+ _rollingFriction = 0;
7274
+ /**
7275
+ * The type of the rigidbody.
7276
+ */
7277
+ _type = 'static';
4893
7278
  /** @ignore */
4894
7279
  constructor() {
4895
7280
  super('rigidbody');
4896
- /**
4897
- * The angular damping of the rigidbody.
4898
- */
4899
- this._angularDamping = 0;
4900
- /**
4901
- * The angular factor of the rigidbody.
4902
- */
4903
- this._angularFactor = new playcanvas.Vec3(1, 1, 1);
4904
- /**
4905
- * The friction of the rigidbody.
4906
- */
4907
- this._friction = 0.5;
4908
- /**
4909
- * The linear damping of the rigidbody.
4910
- */
4911
- this._linearDamping = 0;
4912
- /**
4913
- * The linear factor of the rigidbody.
4914
- */
4915
- this._linearFactor = new playcanvas.Vec3(1, 1, 1);
4916
- /**
4917
- * The mass of the rigidbody.
4918
- */
4919
- this._mass = 1;
4920
- /**
4921
- * The restitution of the rigidbody.
4922
- */
4923
- this._restitution = 0;
4924
- /**
4925
- * The rolling friction of the rigidbody.
4926
- */
4927
- this._rollingFriction = 0;
4928
- /**
4929
- * The type of the rigidbody.
4930
- */
4931
- this._type = 'static';
4932
7281
  }
4933
7282
  getInitialComponentData() {
4934
7283
  return {
@@ -5078,15 +7427,15 @@
5078
7427
  * @category Components
5079
7428
  */
5080
7429
  class ScreenComponentElement extends ComponentElement {
7430
+ _screenSpace = false;
7431
+ _resolution = new playcanvas.Vec2(640, 320);
7432
+ _referenceResolution = new playcanvas.Vec2(640, 320);
7433
+ _priority = 0;
7434
+ _blend = false;
7435
+ _scaleBlend = 0.5;
5081
7436
  /** @ignore */
5082
7437
  constructor() {
5083
7438
  super('screen');
5084
- this._screenSpace = false;
5085
- this._resolution = new playcanvas.Vec2(640, 320);
5086
- this._referenceResolution = new playcanvas.Vec2(640, 320);
5087
- this._priority = 0;
5088
- this._blend = false;
5089
- this._scaleBlend = 0.5;
5090
7439
  }
5091
7440
  getInitialComponentData() {
5092
7441
  return {
@@ -5209,13 +7558,13 @@
5209
7558
  * @category Components
5210
7559
  */
5211
7560
  class ScrollbarComponentElement extends ComponentElement {
7561
+ _orientation = 'horizontal';
7562
+ _value = 0;
7563
+ _handleSize = 0.5;
7564
+ _handle = '';
5212
7565
  /** @ignore */
5213
7566
  constructor() {
5214
7567
  super('scrollbar');
5215
- this._orientation = 'horizontal';
5216
- this._value = 0;
5217
- this._handleSize = 0.5;
5218
- this._handle = '';
5219
7568
  }
5220
7569
  getInitialComponentData() {
5221
7570
  const data = {
@@ -5242,10 +7591,9 @@
5242
7591
  * @param value - The orientation.
5243
7592
  */
5244
7593
  set orientation(value) {
5245
- var _a;
5246
7594
  this._orientation = value;
5247
7595
  if (this.component) {
5248
- this.component.orientation = (_a = orientations.get(value)) !== null && _a !== void 0 ? _a : playcanvas.ORIENTATION_HORIZONTAL;
7596
+ this.component.orientation = orientations.get(value) ?? playcanvas.ORIENTATION_HORIZONTAL;
5249
7597
  }
5250
7598
  }
5251
7599
  /**
@@ -5330,7 +7678,7 @@
5330
7678
  this.handleSize = parseNumber(newValue, 0.5, name);
5331
7679
  break;
5332
7680
  case 'handle':
5333
- this.handle = newValue;
7681
+ this.handle = newValue ?? '';
5334
7682
  break;
5335
7683
  }
5336
7684
  }
@@ -5355,22 +7703,22 @@
5355
7703
  * @category Components
5356
7704
  */
5357
7705
  class ScrollViewComponentElement extends ComponentElement {
7706
+ _horizontal = true;
7707
+ _vertical = true;
7708
+ _scrollMode = 'bounce';
7709
+ _bounceAmount = 0.1;
7710
+ _friction = 0.05;
7711
+ _useMouseWheel = true;
7712
+ _mouseWheelSensitivity = new playcanvas.Vec2(1, 1);
7713
+ _horizontalScrollbarVisibility = 'when-required';
7714
+ _verticalScrollbarVisibility = 'when-required';
7715
+ _viewport = '';
7716
+ _content = '';
7717
+ _horizontalScrollbar = '';
7718
+ _verticalScrollbar = '';
5358
7719
  /** @ignore */
5359
7720
  constructor() {
5360
7721
  super('scrollview');
5361
- this._horizontal = true;
5362
- this._vertical = true;
5363
- this._scrollMode = 'bounce';
5364
- this._bounceAmount = 0.1;
5365
- this._friction = 0.05;
5366
- this._useMouseWheel = true;
5367
- this._mouseWheelSensitivity = new playcanvas.Vec2(1, 1);
5368
- this._horizontalScrollbarVisibility = 'when-required';
5369
- this._verticalScrollbarVisibility = 'when-required';
5370
- this._viewport = '';
5371
- this._content = '';
5372
- this._horizontalScrollbar = '';
5373
- this._verticalScrollbar = '';
5374
7722
  }
5375
7723
  getInitialComponentData() {
5376
7724
  const data = {
@@ -5449,10 +7797,9 @@
5449
7797
  * @param value - The scroll mode.
5450
7798
  */
5451
7799
  set scrollMode(value) {
5452
- var _a;
5453
7800
  this._scrollMode = value;
5454
7801
  if (this.component) {
5455
- this.component.scrollMode = (_a = scrollModes.get(value)) !== null && _a !== void 0 ? _a : playcanvas.SCROLL_MODE_BOUNCE;
7802
+ this.component.scrollMode = scrollModes.get(value) ?? playcanvas.SCROLL_MODE_BOUNCE;
5456
7803
  }
5457
7804
  }
5458
7805
  /**
@@ -5538,10 +7885,9 @@
5538
7885
  * @param value - The horizontal scrollbar visibility.
5539
7886
  */
5540
7887
  set horizontalScrollbarVisibility(value) {
5541
- var _a;
5542
7888
  this._horizontalScrollbarVisibility = value;
5543
7889
  if (this.component) {
5544
- this.component.horizontalScrollbarVisibility = (_a = visibilities.get(value)) !== null && _a !== void 0 ? _a : playcanvas.SCROLLBAR_VISIBILITY_SHOW_WHEN_REQUIRED;
7890
+ this.component.horizontalScrollbarVisibility = visibilities.get(value) ?? playcanvas.SCROLLBAR_VISIBILITY_SHOW_WHEN_REQUIRED;
5545
7891
  }
5546
7892
  }
5547
7893
  /**
@@ -5557,10 +7903,9 @@
5557
7903
  * @param value - The vertical scrollbar visibility.
5558
7904
  */
5559
7905
  set verticalScrollbarVisibility(value) {
5560
- var _a;
5561
7906
  this._verticalScrollbarVisibility = value;
5562
7907
  if (this.component) {
5563
- this.component.verticalScrollbarVisibility = (_a = visibilities.get(value)) !== null && _a !== void 0 ? _a : playcanvas.SCROLLBAR_VISIBILITY_SHOW_WHEN_REQUIRED;
7908
+ this.component.verticalScrollbarVisibility = visibilities.get(value) ?? playcanvas.SCROLLBAR_VISIBILITY_SHOW_WHEN_REQUIRED;
5564
7909
  }
5565
7910
  }
5566
7911
  /**
@@ -5695,16 +8040,16 @@
5695
8040
  this.verticalScrollbarVisibility = parseEnum(newValue, visibilities, 'when-required', name);
5696
8041
  break;
5697
8042
  case 'viewport':
5698
- this.viewport = newValue;
8043
+ this.viewport = newValue ?? '';
5699
8044
  break;
5700
8045
  case 'content':
5701
- this.content = newValue;
8046
+ this.content = newValue ?? '';
5702
8047
  break;
5703
8048
  case 'horizontal-scrollbar':
5704
- this.horizontalScrollbar = newValue;
8049
+ this.horizontalScrollbar = newValue ?? '';
5705
8050
  break;
5706
8051
  case 'vertical-scrollbar':
5707
- this.verticalScrollbar = newValue;
8052
+ this.verticalScrollbar = newValue ?? '';
5708
8053
  break;
5709
8054
  }
5710
8055
  }
@@ -5736,24 +8081,28 @@
5736
8081
  *
5737
8082
  * The element becomes ready once its script instance has been created by the parent
5738
8083
  * `<pc-scripts>` element.
8084
+ *
8085
+ * @fires {CustomEvent} scriptattributeschange - Fired when the script's attributes change. The
8086
+ * `detail` carries the new `attributes` object. Bubbles.
8087
+ * @fires {CustomEvent} scriptenablechange - Fired when the script's enabled state changes. The
8088
+ * `detail` carries the new `enabled` state. Bubbles.
8089
+ * @fires {CustomEvent} scriptnamechange - Fired when the script is renamed on a live element. The
8090
+ * `detail` carries `oldName` and `newName`. Bubbles.
5739
8091
  */
5740
8092
  class ScriptElement extends AsyncElement {
5741
- constructor() {
5742
- super(...arguments);
5743
- this._attributes = {};
5744
- this._enabled = true;
5745
- /**
5746
- * Whether readiness has been signalled. Creation can happen more than once over an
5747
- * element's life (a runtime `name` change recreates the instance), but `ready` is a
5748
- * one-shot signal, so only the first successful creation fires it.
5749
- */
5750
- this._readySignalled = false;
5751
- /**
5752
- * The Script instance created for this element by its parent `<pc-scripts>` element.
5753
- * @ignore
5754
- */
5755
- this._script = null;
5756
- }
8093
+ _attributes = {};
8094
+ _enabled = true;
8095
+ /**
8096
+ * Whether readiness has been signalled. Creation can happen more than once over an
8097
+ * element's life (a runtime `name` change recreates the instance), but `ready` is a
8098
+ * one-shot signal, so only the first successful creation fires it.
8099
+ */
8100
+ _readySignalled = false;
8101
+ /**
8102
+ * The Script instance created for this element by its parent `<pc-scripts>` element.
8103
+ * @ignore
8104
+ */
8105
+ _script = null;
5757
8106
  /**
5758
8107
  * Sets the attributes of the script as an object. Values are converted with the same rules
5759
8108
  * as the `attributes` attribute: `asset:`/`entity:` references and `vec2:`/`vec3:`/`vec4:`/
@@ -5763,7 +8112,7 @@
5763
8112
  * @param value - The attributes of the script.
5764
8113
  */
5765
8114
  set scriptAttributes(value) {
5766
- this._attributes = value !== null && value !== void 0 ? value : {};
8115
+ this._attributes = value ?? {};
5767
8116
  this.dispatchEvent(new CustomEvent('scriptattributeschange', {
5768
8117
  detail: { attributes: this._attributes },
5769
8118
  bubbles: true
@@ -5815,8 +8164,7 @@
5815
8164
  * @returns The name.
5816
8165
  */
5817
8166
  get name() {
5818
- var _a;
5819
- return (_a = this.getAttribute('name')) !== null && _a !== void 0 ? _a : '';
8167
+ return this.getAttribute('name') ?? '';
5820
8168
  }
5821
8169
  /**
5822
8170
  * Gets the {@link Script} instance created for this element. Returns `null` until the
@@ -5828,10 +8176,9 @@
5828
8176
  return this._script;
5829
8177
  }
5830
8178
  connectedCallback() {
5831
- var _a;
5832
8179
  // Script instances are created by the parent pc-scripts element, so an element placed
5833
8180
  // anywhere else is inert and never becomes ready - warn rather than hang silently
5834
- if (((_a = this.parentElement) === null || _a === void 0 ? void 0 : _a.tagName) !== 'PC-SCRIPTS') {
8181
+ if (this.parentElement?.tagName !== 'PC-SCRIPTS') {
5835
8182
  console.warn(`pc-script '${this.getAttribute('name')}' must be a direct child of pc-scripts - script not created`);
5836
8183
  }
5837
8184
  }
@@ -5983,8 +8330,7 @@
5983
8330
  * @returns The color, or `raw`.
5984
8331
  */
5985
8332
  const colorConversion = (rest, raw) => {
5986
- var _a;
5987
- const components = (_a = parseComponents(rest, 4)) !== null && _a !== void 0 ? _a : parseComponents(rest, 3);
8333
+ const components = parseComponents(rest, 4) ?? parseComponents(rest, 3);
5988
8334
  if (components) {
5989
8335
  return new playcanvas.Color(components);
5990
8336
  }
@@ -5992,7 +8338,7 @@
5992
8338
  return raw;
5993
8339
  };
5994
8340
  /**
5995
- * The conversion prefixes recognised in script attribute values, mapped to the conversion each
8341
+ * The conversion prefixes recognized in script attribute values, mapped to the conversion each
5996
8342
  * performs. These keys are the single source of truth for the prefix vocabulary: they drive both
5997
8343
  * the conversion in `convertAttributes` and the has-a-prefix test in `setScriptProperty`, so a
5998
8344
  * prefix added here is automatically known to both.
@@ -6008,10 +8354,10 @@
6008
8354
  /**
6009
8355
  * Matches a value against the conversion prefixes. A prefix is the text before the first colon,
6010
8356
  * so a value whose remainder itself contains colons (`asset:a:b`) still resolves, and a value
6011
- * with an unrecognised prefix (`https://...`) or no colon does not match.
8357
+ * with an unrecognized prefix (`https://...`) or no colon does not match.
6012
8358
  * @param value - The value to inspect.
6013
8359
  * @returns The matching converter and the text after the prefix, or `null` if the value carries
6014
- * no recognised prefix.
8360
+ * no recognized prefix.
6015
8361
  */
6016
8362
  const matchConversion = (value) => {
6017
8363
  const index = value.indexOf(':');
@@ -6050,6 +8396,7 @@
6050
8396
  * @category Components
6051
8397
  */
6052
8398
  class ScriptComponentElement extends ComponentElement {
8399
+ observer;
6053
8400
  /** @ignore */
6054
8401
  constructor() {
6055
8402
  super('script');
@@ -6304,8 +8651,7 @@
6304
8651
  * @param scriptElement - The `pc-script` element holding the attributes.
6305
8652
  */
6306
8653
  applyInlineAttributes(script, scriptElement) {
6307
- var _a;
6308
- const scriptName = (_a = scriptElement.getAttribute('name')) !== null && _a !== void 0 ? _a : '';
8654
+ const scriptName = scriptElement.getAttribute('name') ?? '';
6309
8655
  for (const attr of Array.from(scriptElement.attributes)) {
6310
8656
  if (!isReservedAttribute(attr.name)) {
6311
8657
  this.setScriptProperty(script, scriptName, attr.name, attr.value);
@@ -6320,7 +8666,6 @@
6320
8666
  * @param attributeName - The name of the changed attribute.
6321
8667
  */
6322
8668
  applyScriptProperty(scriptElement, attributeName) {
6323
- var _a;
6324
8669
  const script = this.scriptFor(scriptElement);
6325
8670
  if (!script)
6326
8671
  return;
@@ -6333,7 +8678,7 @@
6333
8678
  }
6334
8679
  return;
6335
8680
  }
6336
- this.setScriptProperty(script, (_a = scriptElement.getAttribute('name')) !== null && _a !== void 0 ? _a : '', attributeName, value);
8681
+ this.setScriptProperty(script, scriptElement.getAttribute('name') ?? '', attributeName, value);
6337
8682
  }
6338
8683
  /**
6339
8684
  * Applies one attribute string to a script property. A string-typed attribute takes the
@@ -6448,9 +8793,8 @@
6448
8793
  }
6449
8794
  }
6450
8795
  disconnectedCallback() {
6451
- var _a;
6452
8796
  this.observer.disconnect();
6453
- (_a = super.disconnectedCallback) === null || _a === void 0 ? void 0 : _a.call(this);
8797
+ super.disconnectedCallback?.();
6454
8798
  }
6455
8799
  /**
6456
8800
  * Gets the underlying PlayCanvas script component.
@@ -6471,16 +8815,16 @@
6471
8815
  * @category Components
6472
8816
  */
6473
8817
  class SoundComponentElement extends ComponentElement {
8818
+ _distanceModel = 'linear';
8819
+ _maxDistance = 10000;
8820
+ _pitch = 1;
8821
+ _positional = false;
8822
+ _refDistance = 1;
8823
+ _rollOffFactor = 1;
8824
+ _volume = 1;
6474
8825
  /** @ignore */
6475
8826
  constructor() {
6476
8827
  super('sound');
6477
- this._distanceModel = 'linear';
6478
- this._maxDistance = 10000;
6479
- this._pitch = 1;
6480
- this._positional = false;
6481
- this._refDistance = 1;
6482
- this._rollOffFactor = 1;
6483
- this._volume = 1;
6484
8828
  }
6485
8829
  getInitialComponentData() {
6486
8830
  return {
@@ -6666,25 +9010,37 @@
6666
9010
  * methods of the {@link AsyncElement} interface.
6667
9011
  */
6668
9012
  class SoundSlotElement extends AsyncElement {
6669
- constructor() {
6670
- super(...arguments);
6671
- this._asset = '';
6672
- this._autoPlay = false;
6673
- this._duration = null;
6674
- this._loop = false;
6675
- this._name = '';
6676
- this._overlap = false;
6677
- this._pitch = 1;
6678
- this._startTime = 0;
6679
- this._volume = 1;
6680
- /**
6681
- * The sound slot.
6682
- */
6683
- this.soundSlot = null;
6684
- }
9013
+ _asset = '';
9014
+ _autoPlay = false;
9015
+ _duration = null;
9016
+ _loop = false;
9017
+ _name = '';
9018
+ _overlap = false;
9019
+ _pitch = 1;
9020
+ _startTime = 0;
9021
+ _volume = 1;
9022
+ /**
9023
+ * The `<pc-sounds>` this slot was added to, captured at connect time.
9024
+ *
9025
+ * `disconnectedCallback` cannot rediscover it: by the time the element is disconnected its
9026
+ * `parentElement` is already `null`, so a lookup would both fail to find the component and
9027
+ * emit a misleading "must be a direct child" warning for what is an ordinary removal.
9028
+ */
9029
+ _soundElement = null;
9030
+ /**
9031
+ * The sound slot.
9032
+ */
9033
+ soundSlot = null;
6685
9034
  async connectedCallback() {
6686
- var _a;
6687
- await ((_a = this.soundElement) === null || _a === void 0 ? void 0 : _a.ready());
9035
+ const soundElement = this.soundElement;
9036
+ await soundElement?.ready();
9037
+ // The element may have been removed, or its parent torn down, while we were waiting. A
9038
+ // <pc-app> disconnects before its children, so by the time we resume the component can
9039
+ // already be gone - see the matching guard in disconnectedCallback below.
9040
+ const component = soundElement?.component;
9041
+ if (!this.isConnected || !component) {
9042
+ return;
9043
+ }
6688
9044
  const options = {
6689
9045
  autoPlay: this._autoPlay,
6690
9046
  loop: this._loop,
@@ -6696,7 +9052,8 @@
6696
9052
  if (this._duration) {
6697
9053
  options.duration = this._duration;
6698
9054
  }
6699
- this.soundSlot = this.soundElement.component.addSlot(this._name, options);
9055
+ this._soundElement = soundElement;
9056
+ this.soundSlot = component.addSlot(this._name, options);
6700
9057
  this.asset = this._asset;
6701
9058
  if (this._autoPlay) {
6702
9059
  this.soundSlot.play();
@@ -6704,10 +9061,12 @@
6704
9061
  this._onReady();
6705
9062
  }
6706
9063
  disconnectedCallback() {
6707
- var _a, _b;
6708
- // The component is null if the parent <pc-sound> (or the whole <pc-app>) is being
6709
- // torn down — parents disconnect first and have already removed the component.
6710
- (_b = (_a = this.soundElement) === null || _a === void 0 ? void 0 : _a.component) === null || _b === void 0 ? void 0 : _b.removeSlot(this._name);
9064
+ // Uses the cached parent rather than a fresh lookup, since parentElement is already null
9065
+ // by now. The component itself is null if the parent <pc-sound> (or the whole <pc-app>) is
9066
+ // being torn down — parents disconnect first and have already removed the component.
9067
+ this._soundElement?.component?.removeSlot(this._name);
9068
+ this._soundElement = null;
9069
+ this.soundSlot = null;
6711
9070
  }
6712
9071
  get soundElement() {
6713
9072
  const soundElement = this.parentElement;
@@ -6722,10 +9081,9 @@
6722
9081
  * @param value - The asset.
6723
9082
  */
6724
9083
  set asset(value) {
6725
- var _a;
6726
9084
  this._asset = value;
6727
9085
  if (this.soundSlot) {
6728
- const id = (_a = AssetElement.get(value)) === null || _a === void 0 ? void 0 : _a.id;
9086
+ const id = AssetElement.get(value)?.id;
6729
9087
  if (id) {
6730
9088
  this.soundSlot.asset = id;
6731
9089
  }
@@ -6880,7 +9238,7 @@
6880
9238
  attributeChangedCallback(name, _oldValue, newValue) {
6881
9239
  switch (name) {
6882
9240
  case 'asset':
6883
- this.asset = newValue;
9241
+ this.asset = newValue ?? '';
6884
9242
  break;
6885
9243
  case 'auto-play':
6886
9244
  this.autoPlay = parseBool(newValue, false);
@@ -6892,7 +9250,7 @@
6892
9250
  this.loop = parseBool(newValue, false);
6893
9251
  break;
6894
9252
  case 'name':
6895
- this.name = newValue;
9253
+ this.name = newValue ?? '';
6896
9254
  break;
6897
9255
  case 'overlap':
6898
9256
  this.overlap = parseBool(newValue, false);
@@ -6920,15 +9278,15 @@
6920
9278
  * @category Components
6921
9279
  */
6922
9280
  class GSplatComponentElement extends ComponentElement {
9281
+ _asset = '';
9282
+ _castShadows = false;
9283
+ _lodBaseDistance = 5;
9284
+ _lodMultiplier = 3;
9285
+ _lodRangeMin = 0;
9286
+ _lodRangeMax = 99;
6923
9287
  /** @ignore */
6924
9288
  constructor() {
6925
9289
  super('gsplat');
6926
- this._asset = '';
6927
- this._castShadows = false;
6928
- this._lodBaseDistance = 5;
6929
- this._lodMultiplier = 3;
6930
- this._lodRangeMin = 0;
6931
- this._lodRangeMax = 99;
6932
9290
  }
6933
9291
  getInitialComponentData() {
6934
9292
  return {
@@ -7077,7 +9435,7 @@
7077
9435
  super.attributeChangedCallback(name, _oldValue, newValue);
7078
9436
  switch (name) {
7079
9437
  case 'asset':
7080
- this.asset = newValue;
9438
+ this.asset = newValue ?? '';
7081
9439
  break;
7082
9440
  case 'cast-shadows':
7083
9441
  this.castShadows = parseBool(newValue, false);
@@ -7106,11 +9464,8 @@
7106
9464
  * {@link HTMLElement} interface.
7107
9465
  */
7108
9466
  class ModelElement extends AsyncElement {
7109
- constructor() {
7110
- super(...arguments);
7111
- this._asset = '';
7112
- this._entity = null;
7113
- }
9467
+ _asset = '';
9468
+ _entity = null;
7114
9469
  connectedCallback() {
7115
9470
  this._loadModel();
7116
9471
  this._onReady();
@@ -7142,10 +9497,9 @@
7142
9497
  }
7143
9498
  }
7144
9499
  async _loadModel() {
7145
- var _a;
7146
9500
  this._unloadModel();
7147
- const appElement = await ((_a = this.closestApp) === null || _a === void 0 ? void 0 : _a.ready());
7148
- const app = appElement === null || appElement === void 0 ? void 0 : appElement.app;
9501
+ const appElement = await this.closestApp?.ready();
9502
+ const app = appElement?.app;
7149
9503
  const asset = AssetElement.get(this._asset);
7150
9504
  if (!asset) {
7151
9505
  return;
@@ -7161,8 +9515,7 @@
7161
9515
  }
7162
9516
  }
7163
9517
  _unloadModel() {
7164
- var _a;
7165
- (_a = this._entity) === null || _a === void 0 ? void 0 : _a.destroy();
9518
+ this._entity?.destroy();
7166
9519
  this._entity = null;
7167
9520
  }
7168
9521
  /**
@@ -7188,7 +9541,7 @@
7188
9541
  attributeChangedCallback(name, _oldValue, newValue) {
7189
9542
  switch (name) {
7190
9543
  case 'asset':
7191
- this.asset = newValue;
9544
+ this.asset = newValue ?? '';
7192
9545
  break;
7193
9546
  }
7194
9547
  }
@@ -7202,60 +9555,84 @@
7202
9555
  * {@link HTMLElement} interface.
7203
9556
  */
7204
9557
  class SceneElement extends AsyncElement {
7205
- constructor() {
7206
- super(...arguments);
7207
- /**
7208
- * The fog type of the scene.
7209
- */
7210
- this._fog = 'none';
7211
- /**
7212
- * The color of the fog.
7213
- */
7214
- this._fogColor = new playcanvas.Color(1, 1, 1);
7215
- /**
7216
- * The density of the fog.
7217
- */
7218
- this._fogDensity = 0;
7219
- /**
7220
- * The start distance of the fog.
7221
- */
7222
- this._fogStart = 0;
7223
- /**
7224
- * The end distance of the fog.
7225
- */
7226
- this._fogEnd = 1000;
7227
- /**
7228
- * The gravity of the scene.
7229
- */
7230
- this._gravity = new playcanvas.Vec3(0, -9.81, 0);
7231
- this._scene = null;
7232
- }
7233
9558
  /**
7234
- * The PlayCanvas scene instance. Available once the element is ready — await
9559
+ * The fog type of the scene.
9560
+ */
9561
+ _fog = 'none';
9562
+ /**
9563
+ * The color of the fog.
9564
+ */
9565
+ _fogColor = new playcanvas.Color(1, 1, 1);
9566
+ /**
9567
+ * The density of the fog.
9568
+ */
9569
+ _fogDensity = 0;
9570
+ /**
9571
+ * The start distance of the fog.
9572
+ */
9573
+ _fogStart = 0;
9574
+ /**
9575
+ * The end distance of the fog.
9576
+ */
9577
+ _fogEnd = 1000;
9578
+ /**
9579
+ * The gravity of the scene.
9580
+ */
9581
+ _gravity = new playcanvas.Vec3(0, -9.81, 0);
9582
+ _scene = null;
9583
+ /**
9584
+ * The PlayCanvas scene instance. `null` until the element is ready — await
7235
9585
  * {@link whenReady} or the element's `ready()` promise before accessing it.
7236
- * @returns The scene instance.
9586
+ * @returns The scene instance, or `null`.
7237
9587
  */
7238
9588
  get scene() {
7239
9589
  return this._scene;
7240
9590
  }
7241
9591
  async connectedCallback() {
7242
- var _a;
7243
- await ((_a = this.closestApp) === null || _a === void 0 ? void 0 : _a.ready());
7244
- this._scene = this.closestApp.app.scene;
9592
+ const appElement = this.closestApp;
9593
+ if (!appElement) {
9594
+ console.warn('pc-scene must be a descendant of pc-app - scene settings not applied');
9595
+ return;
9596
+ }
9597
+ await appElement.ready();
9598
+ // The element may have been removed or re-parented while waiting for the app. Matches the
9599
+ // guard in AssetElement and MaterialElement, but compares closestApp rather than
9600
+ // parentElement because pc-scene resolves its app by ancestor rather than direct child.
9601
+ // Without this, a scene re-parented mid-await would take its Scene from the app it started
9602
+ // under while _applyGravity resolved the app it ended up under, splitting the two.
9603
+ if (!this.isConnected || this.closestApp !== appElement) {
9604
+ return;
9605
+ }
9606
+ // The application is gone if the tree was torn down while we awaited readiness. There is
9607
+ // nothing to configure and nothing the author can act on, so this stays silent.
9608
+ const app = appElement.app;
9609
+ if (!app) {
9610
+ return;
9611
+ }
9612
+ this._scene = app.scene;
7245
9613
  this.updateSceneSettings();
7246
9614
  this._onReady();
7247
9615
  }
7248
9616
  updateSceneSettings() {
7249
- if (this.scene) {
7250
- this.scene.fog.type = this._fog;
7251
- this.scene.fog.color = this._fogColor;
7252
- this.scene.fog.density = this._fogDensity;
7253
- this.scene.fog.start = this._fogStart;
7254
- this.scene.fog.end = this._fogEnd;
7255
- const appElement = this.parentElement;
7256
- appElement.app.systems.rigidbody.gravity.copy(this._gravity);
9617
+ if (this._scene) {
9618
+ this._scene.fog.type = this._fog;
9619
+ this._scene.fog.color = this._fogColor;
9620
+ this._scene.fog.density = this._fogDensity;
9621
+ this._scene.fog.start = this._fogStart;
9622
+ this._scene.fog.end = this._fogEnd;
9623
+ this._applyGravity(this._gravity);
7257
9624
  }
7258
9625
  }
9626
+ /**
9627
+ * Applies gravity to the rigid body system. Resolved through `closestApp` rather than
9628
+ * `parentElement` so that a `<pc-scene>` nested inside a wrapper element behaves the same as
9629
+ * a direct child, matching how `connectedCallback` resolves the application.
9630
+ *
9631
+ * @param value - The gravity to apply.
9632
+ */
9633
+ _applyGravity(value) {
9634
+ this.closestApp?.app?.systems.rigidbody?.gravity.copy(value);
9635
+ }
7259
9636
  /**
7260
9637
  * Sets the fog type of the scene. Can be `none`, `linear`, `exp` or `exp2`. Defaults to
7261
9638
  * `none`.
@@ -7348,9 +9725,8 @@
7348
9725
  */
7349
9726
  set gravity(value) {
7350
9727
  this._gravity = value;
7351
- if (this.scene) {
7352
- const appElement = this.parentElement;
7353
- appElement.app.systems.rigidbody.gravity.copy(value);
9728
+ if (this._scene) {
9729
+ this._applyGravity(value);
7354
9730
  }
7355
9731
  }
7356
9732
  /**
@@ -7395,19 +9771,16 @@
7395
9771
  * methods of the {@link HTMLElement} interface.
7396
9772
  */
7397
9773
  class SkyElement extends AsyncElement {
7398
- constructor() {
7399
- super(...arguments);
7400
- this._asset = '';
7401
- this._center = new playcanvas.Vec3(0, 0.01, 0);
7402
- this._intensity = 1;
7403
- this._rotation = new playcanvas.Vec3();
7404
- this._level = 0;
7405
- this._lighting = false;
7406
- this._scale = new playcanvas.Vec3(100, 100, 100);
7407
- this._type = 'infinite';
7408
- this._scene = null;
7409
- this._appElement = null;
7410
- }
9774
+ _asset = '';
9775
+ _center = new playcanvas.Vec3(0, 0.01, 0);
9776
+ _intensity = 1;
9777
+ _rotation = new playcanvas.Vec3();
9778
+ _level = 0;
9779
+ _lighting = false;
9780
+ _scale = new playcanvas.Vec3(100, 100, 100);
9781
+ _type = 'infinite';
9782
+ _scene = null;
9783
+ _appElement = null;
7411
9784
  connectedCallback() {
7412
9785
  this._loadSkybox();
7413
9786
  this._onReady();
@@ -7439,9 +9812,8 @@
7439
9812
  this._scene.skyboxMip = this._level;
7440
9813
  }
7441
9814
  async _loadSkybox() {
7442
- var _a;
7443
- const appElement = await ((_a = this.closestApp) === null || _a === void 0 ? void 0 : _a.ready());
7444
- const app = appElement === null || appElement === void 0 ? void 0 : appElement.app;
9815
+ const appElement = await this.closestApp?.ready();
9816
+ const app = appElement?.app;
7445
9817
  if (!appElement || !app) {
7446
9818
  return;
7447
9819
  }
@@ -7462,7 +9834,6 @@
7462
9834
  }
7463
9835
  }
7464
9836
  _unloadSkybox() {
7465
- var _a, _b, _c;
7466
9837
  const scene = this._scene;
7467
9838
  if (!scene)
7468
9839
  return;
@@ -7470,12 +9841,12 @@
7470
9841
  // If the owning application has already been destroyed (removing a <pc-app>
7471
9842
  // disconnects it before its children), the scene, graphics device and skybox
7472
9843
  // textures have all been destroyed along with it — nothing left to clean up.
7473
- if (!((_a = this._appElement) === null || _a === void 0 ? void 0 : _a.app))
9844
+ if (!this._appElement?.app)
7474
9845
  return;
7475
- (_b = scene.skybox) === null || _b === void 0 ? void 0 : _b.destroy();
9846
+ scene.skybox?.destroy();
7476
9847
  // @ts-ignore
7477
9848
  scene.skybox = null;
7478
- (_c = scene.envAtlas) === null || _c === void 0 ? void 0 : _c.destroy();
9849
+ scene.envAtlas?.destroy();
7479
9850
  // @ts-ignore
7480
9851
  scene.envAtlas = null;
7481
9852
  }
@@ -7622,7 +9993,7 @@
7622
9993
  attributeChangedCallback(name, _oldValue, newValue) {
7623
9994
  switch (name) {
7624
9995
  case 'asset':
7625
- this.asset = newValue;
9996
+ this.asset = newValue ?? '';
7626
9997
  break;
7627
9998
  case 'center':
7628
9999
  this.center = parseVec3(newValue, new playcanvas.Vec3(0, 0.01, 0), name);