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