@playcanvas/web-components 0.10.1 → 0.11.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 (78) hide show
  1. package/README.md +1 -1
  2. package/dist/app.d.ts +102 -51
  3. package/dist/asset.d.ts +8 -7
  4. package/dist/async-element.d.ts +21 -5
  5. package/dist/components/button-component.d.ts +3 -7
  6. package/dist/components/camera-component.d.ts +16 -20
  7. package/dist/components/collision-component.d.ts +3 -7
  8. package/dist/components/component.d.ts +22 -4
  9. package/dist/components/element-component.d.ts +4 -8
  10. package/dist/components/gsplat-component.d.ts +2 -7
  11. package/dist/components/layoutchild-component.d.ts +2 -7
  12. package/dist/components/layoutgroup-component.d.ts +3 -7
  13. package/dist/components/light-component.d.ts +3 -7
  14. package/dist/components/listener-component.d.ts +1 -6
  15. package/dist/components/particlesystem-component.d.ts +2 -7
  16. package/dist/components/render-component.d.ts +2 -7
  17. package/dist/components/rigidbody-component.d.ts +3 -7
  18. package/dist/components/screen-component.d.ts +28 -11
  19. package/dist/components/script-component.d.ts +8 -20
  20. package/dist/components/script.d.ts +2 -22
  21. package/dist/components/scrollbar-component.d.ts +2 -7
  22. package/dist/components/scrollview-component.d.ts +9 -11
  23. package/dist/components/sound-component.d.ts +2 -7
  24. package/dist/components/sound-slot.d.ts +8 -6
  25. package/dist/custom-elements.json +5191 -10695
  26. package/dist/entity.d.ts +16 -9
  27. package/dist/index.d.ts +37 -0
  28. package/dist/material.d.ts +12 -12
  29. package/dist/model.d.ts +21 -5
  30. package/dist/module.d.ts +0 -6
  31. package/dist/parse.d.ts +6 -3
  32. package/dist/pwc.cjs +799 -287
  33. package/dist/pwc.cjs.map +1 -1
  34. package/dist/pwc.js +799 -287
  35. package/dist/pwc.js.map +1 -1
  36. package/dist/pwc.min.js +1 -1
  37. package/dist/pwc.min.js.map +1 -1
  38. package/dist/pwc.min.mjs +1 -1
  39. package/dist/pwc.min.mjs.map +1 -1
  40. package/dist/pwc.mjs +800 -288
  41. package/dist/pwc.mjs.map +1 -1
  42. package/dist/scene.d.ts +4 -7
  43. package/dist/sky.d.ts +25 -16
  44. package/dist/vscode.html-custom-data.json +65 -45
  45. package/dist/web-types.json +585 -557
  46. package/package.json +8 -7
  47. package/src/app.ts +326 -144
  48. package/src/asset.ts +34 -28
  49. package/src/async-element.ts +34 -8
  50. package/src/components/button-component.ts +5 -9
  51. package/src/components/camera-component.ts +55 -36
  52. package/src/components/collision-component.ts +26 -15
  53. package/src/components/component.ts +58 -8
  54. package/src/components/element-component.ts +26 -30
  55. package/src/components/gsplat-component.ts +4 -9
  56. package/src/components/layoutchild-component.ts +4 -9
  57. package/src/components/layoutgroup-component.ts +14 -9
  58. package/src/components/light-component.ts +42 -12
  59. package/src/components/listener-component.ts +1 -7
  60. package/src/components/particlesystem-component.ts +7 -15
  61. package/src/components/render-component.ts +5 -10
  62. package/src/components/rigidbody-component.ts +23 -16
  63. package/src/components/screen-component.ts +46 -20
  64. package/src/components/script-component.ts +108 -46
  65. package/src/components/script.ts +38 -33
  66. package/src/components/scrollbar-component.ts +6 -16
  67. package/src/components/scrollview-component.ts +22 -15
  68. package/src/components/sound-component.ts +10 -15
  69. package/src/components/sound-slot.ts +30 -20
  70. package/src/entity.ts +75 -34
  71. package/src/index.ts +45 -1
  72. package/src/loading-bar.ts +8 -8
  73. package/src/material.ts +63 -37
  74. package/src/model.ts +69 -14
  75. package/src/module.ts +8 -7
  76. package/src/parse.ts +67 -20
  77. package/src/scene.ts +12 -9
  78. package/src/sky.ts +76 -34
package/dist/pwc.cjs CHANGED
@@ -5,12 +5,14 @@ var playcanvas = require('playcanvas');
5
5
  /**
6
6
  * Base class for all PlayCanvas Web Components that initialize asynchronously.
7
7
  *
8
- * @fires {CustomEvent} ready - Fired once the element is fully initialized. Bubbles and is
9
- * composed.
8
+ * @fires {CustomEvent} ready - Fired when the element is fully initialized once per readiness
9
+ * cycle, so an element that is torn down and re-initialized (for example by removing and
10
+ * re-inserting it) fires it again. Bubbles and is composed.
10
11
  */
11
12
  class AsyncElement extends HTMLElement {
12
13
  _readyPromise;
13
14
  _readyResolve;
15
+ _readyResolved = false;
14
16
  /** @ignore */
15
17
  constructor() {
16
18
  super();
@@ -37,15 +39,39 @@ class AsyncElement extends HTMLElement {
37
39
  /**
38
40
  * Called when the element is fully initialized and ready. Subclasses should call this when
39
41
  * they're ready. Resolves the ready promise and dispatches a bubbling, composed `ready`
40
- * event.
42
+ * event. Signals at most once per readiness cycle: a repeat call before {@link _resetReady}
43
+ * has re-armed the promise does nothing.
41
44
  */
42
45
  _onReady() {
46
+ if (this._readyResolved)
47
+ return;
48
+ this._readyResolved = true;
43
49
  this._readyResolve();
44
50
  this.dispatchEvent(new CustomEvent('ready', { bubbles: true, composed: true }));
45
51
  }
52
+ /**
53
+ * Returns the ready promise to its pending state. Subclasses should call this when the
54
+ * resource their readiness announced is torn down (typically from `disconnectedCallback`),
55
+ * so that a later re-initialization can signal readiness again. Does nothing while the
56
+ * promise is still pending — an in-flight waiter carries over to the next readiness cycle
57
+ * rather than being stranded on a promise nothing will ever resolve.
58
+ */
59
+ _resetReady() {
60
+ if (!this._readyResolved)
61
+ return;
62
+ this._readyResolved = false;
63
+ this._readyPromise = new Promise((resolve) => {
64
+ this._readyResolve = resolve;
65
+ });
66
+ }
46
67
  /**
47
68
  * Returns a promise that resolves with this element when it's ready. This is the low-level
48
69
  * primitive underlying {@link whenReady}, which is the recommended way to wait for elements.
70
+ *
71
+ * Readiness tracks the element's current lifecycle: once a ready element is torn down (for
72
+ * example by removing it from the document), this returns a fresh promise that resolves when
73
+ * the element is next ready. A promise obtained earlier stays resolved — call this again
74
+ * after re-inserting an element rather than reusing a promise from before its removal.
49
75
  * @returns A promise that resolves with this element when it's ready.
50
76
  */
51
77
  ready() {
@@ -120,7 +146,14 @@ class ModuleElement extends HTMLElement {
120
146
  });
121
147
  }
122
148
  }
123
- getLoadPromise() {
149
+ /**
150
+ * Returns the promise that settles when the module has loaded. Awaited by the containing
151
+ * `<pc-app>` element before it creates its graphics device.
152
+ *
153
+ * @returns The load promise.
154
+ * @internal
155
+ */
156
+ _getLoadPromise() {
124
157
  return this.loadPromise;
125
158
  }
126
159
  }
@@ -179,10 +212,7 @@ class LoadingBar {
179
212
  // aria-valuenow is set, which is what marks a progressbar indeterminate. jsdom has no Web
180
213
  // Animations API, so the guard degrades to a static bar there rather than crashing boot.
181
214
  if (typeof this._fill.animate === 'function') {
182
- this._sweep = this._fill.animate([
183
- { transform: 'scaleX(0.25) translateX(-100%)' },
184
- { transform: 'scaleX(0.25) translateX(500%)' }
185
- ], {
215
+ this._sweep = this._fill.animate([{ transform: 'scaleX(0.25) translateX(-100%)' }, { transform: 'scaleX(0.25) translateX(500%)' }], {
186
216
  duration: 1000,
187
217
  iterations: Infinity,
188
218
  easing: 'ease-in-out'
@@ -415,7 +445,7 @@ const CSS_COLORS = {
415
445
  */
416
446
  const parseComponents = (value, count) => {
417
447
  const components = value.trim().split(/\s+/).map(Number);
418
- if (components.length !== count || components.some(component => !Number.isFinite(component))) {
448
+ if (components.length !== count || components.some((component) => !Number.isFinite(component))) {
419
449
  return null;
420
450
  }
421
451
  return components;
@@ -471,7 +501,10 @@ const parseColor = (value, defaultValue, attribute) => {
471
501
  if (/^#(?:[0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(value)) {
472
502
  let hex = value.slice(1);
473
503
  if (hex.length === 3 || hex.length === 4) {
474
- hex = hex.split('').map(char => char + char).join('');
504
+ hex = hex
505
+ .split('')
506
+ .map((char) => char + char)
507
+ .join('');
475
508
  }
476
509
  return new playcanvas.Color().fromString(`#${hex}`);
477
510
  }
@@ -489,7 +522,9 @@ const parseColor = (value, defaultValue, attribute) => {
489
522
  * the value is invalid — the latter also logs a warning listing the valid names.
490
523
  *
491
524
  * @param value - The attribute value to parse (`null` when the attribute is absent).
492
- * @param valid - The valid names: an array, or a map whose keys are the valid names.
525
+ * @param valid - The valid names: an array, or a map whose keys are the valid names. Only the keys
526
+ * are read, so the map's value type is unconstrained - engine enums are mostly numeric constants,
527
+ * but some (e.g. `SCALEMODE_BLEND`) are strings.
493
528
  * @param defaultValue - The value to use when the attribute is absent or invalid.
494
529
  * @param attribute - The attribute name, used in the warning message.
495
530
  * @returns The resolved enum name.
@@ -566,7 +601,10 @@ const parseTags = (value, defaultValue = []) => {
566
601
  // caller's default, or a later mutation would write back through it.
567
602
  return [...defaultValue];
568
603
  }
569
- return value.split(',').map(tag => tag.trim()).filter(tag => tag !== '');
604
+ return value
605
+ .split(',')
606
+ .map((tag) => tag.trim())
607
+ .filter((tag) => tag !== '');
570
608
  };
571
609
  /**
572
610
  * Parse a Vec2 attribute value. The expected format is 2 space-separated numbers (e.g. '1 2').
@@ -658,6 +696,8 @@ const getEntity = (ref) => {
658
696
  return element?.entity ?? null;
659
697
  };
660
698
 
699
+ /** The pointer event types the application synthesizes on `<pc-entity>` elements via picking. */
700
+ const pointerEventTypes = ['pointermove', 'pointerdown', 'pointerup', 'pointerenter', 'pointerleave'];
661
701
  /**
662
702
  * The AppElement interface provides properties and methods for manipulating
663
703
  * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-app/ | `<pc-app>`} elements.
@@ -677,12 +717,37 @@ class AppElement extends AsyncElement {
677
717
  _alpha = true;
678
718
  _backend = 'webgpu';
679
719
  _antialias = true;
680
- _depth = true;
681
- _stencil = true;
682
- _highResolution = true;
720
+ _depthBuffer = true;
721
+ _stencilBuffer = true;
722
+ _maxPixelRatio = Infinity;
683
723
  _loadingBar = true;
724
+ /**
725
+ * Set once the graphics options above have been handed to `createGraphicsDevice`, after which
726
+ * writing any of them changes nothing. Guards the warning in {@link _warnIfBooted}, and is
727
+ * cleared on disconnect so a re-connected element boots from its current attributes.
728
+ */
729
+ _optionsLocked = false;
684
730
  _bar = null;
731
+ /**
732
+ * Whether the application has created its initial entity hierarchy. Read by EntityElement to
733
+ * decide whether a newly connected element must create its entity itself or leave it to the
734
+ * boot sweep.
735
+ * @internal
736
+ */
685
737
  _hierarchyReady = false;
738
+ /**
739
+ * Incremented on every connect and disconnect. Boot captures the value on entry and abandons
740
+ * itself wherever it resumes from an await if the value has moved on — so a boot whose
741
+ * element was removed cannot complete against a torn-down element, and a boot whose element
742
+ * was removed and re-inserted (which starts a boot of its own) cannot race the newer one.
743
+ */
744
+ _bootGeneration = 0;
745
+ /**
746
+ * The elements backing this application's entities, keyed by the entity itself. Registered
747
+ * by EntityElement at creation and removed when an entity is destroyed, this joins engine
748
+ * scene nodes back to their owning elements by identity - never by name.
749
+ */
750
+ _entityElements = new Map();
686
751
  _picker = null;
687
752
  _hasPointerListeners = {
688
753
  pointerenter: false,
@@ -729,8 +794,16 @@ class AppElement extends AsyncElement {
729
794
  super();
730
795
  // Bind methods to maintain 'this' context
731
796
  this._onWindowResize = this._onWindowResize.bind(this);
797
+ // Track pointer listeners being added to and removed from descendant entities.
798
+ // Registered once here rather than on every boot - the handlers no-op while there is no
799
+ // canvas, and a re-booted element must not stack a second set.
800
+ pointerEventTypes.forEach((type) => {
801
+ this.addEventListener(`${type}:connect`, () => this._onPointerListenerAdded(type));
802
+ this.addEventListener(`${type}:disconnect`, () => this._onPointerListenerRemoved(type));
803
+ });
732
804
  }
733
805
  async connectedCallback() {
806
+ const generation = ++this._bootGeneration;
734
807
  // Created before the first await, so the bar is visible while modules and the graphics
735
808
  // device are created, and exists before any disconnect could need to clean it up
736
809
  if (this._loadingBar && !this._bar) {
@@ -739,7 +812,12 @@ class AppElement extends AsyncElement {
739
812
  // Get all pc-module elements that are direct children of the pc-app element
740
813
  const moduleElements = this.querySelectorAll(':scope > pc-module');
741
814
  // Wait for all modules to load
742
- await Promise.all(Array.from(moduleElements).map(module => module.getLoadPromise()));
815
+ await Promise.all(Array.from(moduleElements).map((module) => module._getLoadPromise()));
816
+ // The element may have been removed while the modules loaded. Nothing beyond the loading
817
+ // bar exists yet, and disconnectedCallback has already destroyed that.
818
+ if (generation !== this._bootGeneration) {
819
+ return;
820
+ }
743
821
  // Create and append the canvas to the element
744
822
  this._canvas = document.createElement('canvas');
745
823
  this.appendChild(this._canvas);
@@ -750,15 +828,26 @@ class AppElement extends AsyncElement {
750
828
  null: ['null']
751
829
  };
752
830
  const deviceTypes = backendToDeviceTypes[this._backend] || [];
831
+ this._optionsLocked = true;
753
832
  const device = await playcanvas.createGraphicsDevice(this._canvas, {
754
833
  // @ts-ignore - alpha needs to be documented
755
834
  alpha: this._alpha,
756
835
  antialias: this._antialias,
757
- depth: this._depth,
836
+ depth: this._depthBuffer,
758
837
  deviceTypes: deviceTypes,
759
- stencil: this._stencil
838
+ stencil: this._stencilBuffer
760
839
  });
761
- device.maxPixelRatio = this._highResolution ? window.devicePixelRatio : 1;
840
+ // The element may have been removed while the device was created. disconnectedCallback
841
+ // has already cleaned up the canvas; the device was created inside the await, so it is
842
+ // this boot's to release.
843
+ if (generation !== this._bootGeneration) {
844
+ device.destroy();
845
+ return;
846
+ }
847
+ // Assigned rather than resolved to a number here: the engine caps against the live
848
+ // window.devicePixelRatio on every resize, so an uncapped Infinity keeps following the
849
+ // display when a window moves between monitors of differing density.
850
+ device.maxPixelRatio = this._maxPixelRatio;
762
851
  const createOptions = new playcanvas.AppOptions();
763
852
  createOptions.graphicsDevice = device;
764
853
  createOptions.keyboard = new playcanvas.Keyboard(window);
@@ -831,27 +920,41 @@ class AppElement extends AsyncElement {
831
920
  this._pickerCreate();
832
921
  // Get all pc-asset elements that are direct children of the pc-app element
833
922
  const assetElements = this.querySelectorAll(':scope > pc-asset');
834
- Array.from(assetElements).forEach((assetElement) => {
835
- assetElement.createAsset();
923
+ for (const assetElement of Array.from(assetElements)) {
924
+ assetElement._createAsset();
836
925
  const asset = assetElement.asset;
837
926
  if (asset) {
838
927
  app.assets.add(asset);
928
+ // Adding a fileless asset (one built purely from data, such as a sprite)
929
+ // completes it synchronously, dispatching the element's load event - whose
930
+ // listeners may have removed this element. Stop before the next addition
931
+ // reaches the destroyed registry, and before orphan entities are created.
932
+ if (generation !== this._bootGeneration) {
933
+ return;
934
+ }
839
935
  }
840
- });
936
+ }
841
937
  // Get all pc-material elements that are direct children of the pc-app element
842
938
  const materialElements = this.querySelectorAll(':scope > pc-material');
843
939
  Array.from(materialElements).forEach((materialElement) => {
844
- materialElement.createMaterial();
940
+ materialElement._createMaterial();
845
941
  });
846
942
  // Create all entities
847
943
  const entityElements = this.querySelectorAll('pc-entity');
848
944
  Array.from(entityElements).forEach((entityElement) => {
849
- entityElement.createEntity(app);
945
+ entityElement._createEntity(app);
850
946
  });
851
947
  // Build hierarchy
852
948
  entityElements.forEach((entityElement) => {
853
- entityElement.buildHierarchy(app);
949
+ entityElement._buildHierarchy(app);
854
950
  });
951
+ // Building the hierarchy dispatched each entity's ready event synchronously, and a
952
+ // listener may have removed the element. The sweep itself degrades safely - destroying
953
+ // the application nulls every element's entity, so the remaining builds no-op - but the
954
+ // teardown's reset must not be overwritten here.
955
+ if (generation !== this._bootGeneration) {
956
+ return;
957
+ }
855
958
  this._hierarchyReady = true;
856
959
  // Forward the engine's preload lifecycle as DOM ProgressEvents on this element. The
857
960
  // listener must be attached before preload() is called: an asset that is already loaded
@@ -868,8 +971,19 @@ class AppElement extends AsyncElement {
868
971
  this._loadProgress = total === 0 ? 1 : 0;
869
972
  this._bar?.progress(0, total);
870
973
  this.dispatchEvent(new ProgressEvent('progress', { lengthComputable: true, loaded: 0, total }));
974
+ // The progress dispatch above ran listeners synchronously, and one may have removed the
975
+ // element. The application is already destroyed - it must not be asked to preload.
976
+ if (generation !== this._bootGeneration) {
977
+ return;
978
+ }
871
979
  // Load assets before starting the application
872
980
  app.preload(() => {
981
+ // The element may have been removed while assets loaded. The application is already
982
+ // destroyed, so it must not be started — and readiness must not be signaled for a
983
+ // boot that no longer owns the element.
984
+ if (generation !== this._bootGeneration) {
985
+ return;
986
+ }
873
987
  // Scope the counter to this preload pass, so a later app.preload() call by user code
874
988
  // cannot push `loaded` past `total`
875
989
  app.off('preload:progress', onPreloadProgress);
@@ -885,15 +999,26 @@ class AppElement extends AsyncElement {
885
999
  });
886
1000
  }
887
1001
  disconnectedCallback() {
1002
+ // Invalidate any boot still in flight, so it abandons itself when it next resumes
1003
+ // instead of completing against a torn-down element.
1004
+ this._bootGeneration++;
1005
+ this._optionsLocked = false;
888
1006
  this._pickerDestroy();
889
- // Clean up the application
1007
+ // Clean up the application. Destroying it destroys every entity, whose destroy hooks
1008
+ // unregister them - clear() covers any entity the engine no longer reached.
890
1009
  if (this._app) {
891
1010
  this._app.destroy();
892
1011
  this._app = null;
893
1012
  }
1013
+ this._entityElements.clear();
894
1014
  this._loadProgress = 0;
895
1015
  this._bar?.destroy();
896
1016
  this._bar = null;
1017
+ // Return the element to its pre-boot state, so re-inserting it boots afresh: descendants
1018
+ // must neither see a hierarchy that no longer exists nor resume against a readiness that
1019
+ // no longer holds.
1020
+ this._hierarchyReady = false;
1021
+ this._resetReady();
897
1022
  // Remove event listeners
898
1023
  window.removeEventListener('resize', this._onWindowResize);
899
1024
  // Remove the canvas
@@ -921,14 +1046,11 @@ class AppElement extends AsyncElement {
921
1046
  this._pointerHandlers.pointermove = listener(this._onPointerMove);
922
1047
  this._pointerHandlers.pointerdown = listener(this._onPointerDown);
923
1048
  this._pointerHandlers.pointerup = listener(this._onPointerUp);
924
- // Listen for pointer listeners being added/removed
925
- ['pointermove', 'pointerdown', 'pointerup', 'pointerenter', 'pointerleave'].forEach((type) => {
926
- this.addEventListener(`${type}:connect`, () => this._onPointerListenerAdded(type));
927
- this.addEventListener(`${type}:disconnect`, () => this._onPointerListenerRemoved(type));
928
- // Attach canvas handlers for listeners registered before this point (e.g. handlers
929
- // created from onpointer* attributes when their elements were first upgraded)
930
- const anyListeners = Array.from(this.querySelectorAll('pc-entity'))
931
- .some(entity => entity.hasListeners(type));
1049
+ // Attach canvas handlers for listeners registered before this boot (e.g. handlers
1050
+ // created from onpointer* attributes when their elements were first upgraded, or
1051
+ // listeners carried over from before a re-boot)
1052
+ pointerEventTypes.forEach((type) => {
1053
+ const anyListeners = Array.from(this.querySelectorAll('pc-entity')).some((entity) => entity._hasListeners(type));
932
1054
  if (anyListeners) {
933
1055
  this._onPointerListenerAdded(type);
934
1056
  }
@@ -943,6 +1065,7 @@ class AppElement extends AsyncElement {
943
1065
  });
944
1066
  }
945
1067
  this._picker = null;
1068
+ this._hoveredEntity = null;
946
1069
  this._pointerHandlers = {
947
1070
  pointermove: null,
948
1071
  pointerdown: null,
@@ -956,6 +1079,74 @@ class AppElement extends AsyncElement {
956
1079
  pointermove: false
957
1080
  };
958
1081
  }
1082
+ /**
1083
+ * Registers the element that created an entity. Called by EntityElement when it creates its
1084
+ * entity.
1085
+ *
1086
+ * @param entity - The entity.
1087
+ * @param element - The element that created it.
1088
+ * @internal
1089
+ */
1090
+ _registerEntityElement(entity, element) {
1091
+ this._entityElements.set(entity, element);
1092
+ }
1093
+ /**
1094
+ * Removes the registration for a destroyed entity. Called by EntityElement.
1095
+ *
1096
+ * @param entity - The entity.
1097
+ * @internal
1098
+ */
1099
+ _unregisterEntityElement(entity) {
1100
+ this._entityElements.delete(entity);
1101
+ }
1102
+ /**
1103
+ * Returns the `<pc-entity>` element whose backing entity is `entity`, or `null` if the
1104
+ * entity was not created by an element of this application - for example, a node inside a
1105
+ * model's instantiated hierarchy, or an entity created through the engine API.
1106
+ *
1107
+ * @param entity - The entity to look up.
1108
+ * @returns The element backing the entity, or `null`.
1109
+ */
1110
+ elementFromEntity(entity) {
1111
+ return this._entityElements.get(entity) ?? null;
1112
+ }
1113
+ /**
1114
+ * Resolves the element that owns a picked node: the nearest node up the parent chain -
1115
+ * starting with the node itself - that was created by a `<pc-entity>` of this application.
1116
+ * A hit inside a model's instantiated hierarchy therefore resolves to the element hosting
1117
+ * the model.
1118
+ *
1119
+ * @param node - The picked node, or `null`.
1120
+ * @returns The owning element, or `null`.
1121
+ */
1122
+ _elementFromNode(node) {
1123
+ while (node !== null) {
1124
+ const element = this._entityElements.get(node);
1125
+ if (element) {
1126
+ return element;
1127
+ }
1128
+ node = node.parent;
1129
+ }
1130
+ return null;
1131
+ }
1132
+ /**
1133
+ * Like {@link _elementFromNode}, but skips elements without a listener for `type`, so a hit
1134
+ * on an unlistened child still reaches a listening ancestor.
1135
+ *
1136
+ * @param node - The picked node, or `null`.
1137
+ * @param type - The pointer event type a listener is required for.
1138
+ * @returns The nearest listening element, or `null`.
1139
+ */
1140
+ _elementWithListener(node, type) {
1141
+ while (node !== null) {
1142
+ const element = this._entityElements.get(node);
1143
+ if (element?._hasListeners(type)) {
1144
+ return element;
1145
+ }
1146
+ node = node.parent;
1147
+ }
1148
+ return null;
1149
+ }
959
1150
  // New helper to convert CSS coordinates to canvas (picker) coordinates
960
1151
  _getPickerCoordinates(event) {
961
1152
  // Get the canvas' bounding rectangle in CSS pixels.
@@ -1001,56 +1192,44 @@ class AppElement extends AsyncElement {
1001
1192
  const node = await this._pickNode(event);
1002
1193
  if (token !== this._pickToken || !this._picker)
1003
1194
  return;
1004
- // Get the currently hovered entity by walking up the hierarchy
1005
- let newHoverEntity = null;
1006
- let currentNode = node;
1007
- while (currentNode !== null) {
1008
- const entityElement = this.querySelector(`pc-entity[name="${currentNode.name}"]`);
1009
- if (entityElement) {
1010
- newHoverEntity = entityElement;
1011
- break;
1012
- }
1013
- currentNode = currentNode.parent;
1014
- }
1195
+ // The hovered element is the nearest one up the node's parent chain, listening or not -
1196
+ // dispatch is gated per event type below
1197
+ const newHoverEntity = this._elementFromNode(node);
1015
1198
  // Handle enter/leave events
1016
1199
  if (this._hoveredEntity !== newHoverEntity) {
1017
- if (this._hoveredEntity && this._hoveredEntity.hasListeners('pointerleave')) {
1200
+ if (this._hoveredEntity && this._hoveredEntity._hasListeners('pointerleave')) {
1018
1201
  this._hoveredEntity.dispatchEvent(new PointerEvent('pointerleave', event));
1019
1202
  }
1020
- if (newHoverEntity && newHoverEntity.hasListeners('pointerenter')) {
1203
+ if (newHoverEntity && newHoverEntity._hasListeners('pointerenter')) {
1021
1204
  newHoverEntity.dispatchEvent(new PointerEvent('pointerenter', event));
1022
1205
  }
1023
1206
  }
1024
1207
  // Update hover state
1025
1208
  this._hoveredEntity = newHoverEntity;
1026
1209
  // Handle pointermove event
1027
- if (newHoverEntity && newHoverEntity.hasListeners('pointermove')) {
1210
+ if (newHoverEntity && newHoverEntity._hasListeners('pointermove')) {
1028
1211
  newHoverEntity.dispatchEvent(new PointerEvent('pointermove', event));
1029
1212
  }
1030
1213
  }
1031
1214
  async _onPointerDown(event) {
1032
1215
  if (!this._picker || !this.app)
1033
1216
  return;
1034
- let currentNode = await this._pickNode(event);
1217
+ const node = await this._pickNode(event);
1035
1218
  if (!this._picker)
1036
1219
  return; // the element disconnected while the pick was in flight
1037
- while (currentNode !== null) {
1038
- const entityElement = this.querySelector(`pc-entity[name="${currentNode.name}"]`);
1039
- if (entityElement && entityElement.hasListeners('pointerdown')) {
1040
- entityElement.dispatchEvent(new PointerEvent('pointerdown', event));
1041
- break;
1042
- }
1043
- currentNode = currentNode.parent;
1220
+ const entityElement = this._elementWithListener(node, 'pointerdown');
1221
+ if (entityElement) {
1222
+ entityElement.dispatchEvent(new PointerEvent('pointerdown', event));
1044
1223
  }
1045
1224
  }
1046
1225
  async _onPointerUp(event) {
1047
1226
  if (!this._picker || !this.app)
1048
1227
  return;
1049
1228
  const node = await this._pickNode(event);
1050
- if (!node || !this._picker)
1051
- return;
1052
- const entityElement = this.querySelector(`pc-entity[name="${node.name}"]`);
1053
- if (entityElement && entityElement.hasListeners('pointerup')) {
1229
+ if (!this._picker)
1230
+ return; // the element disconnected while the pick was in flight
1231
+ const entityElement = this._elementWithListener(node, 'pointerup');
1232
+ if (entityElement) {
1054
1233
  entityElement.dispatchEvent(new PointerEvent('pointerup', event));
1055
1234
  }
1056
1235
  }
@@ -1058,50 +1237,64 @@ class AppElement extends AsyncElement {
1058
1237
  if (!this._hasPointerListeners[type] && this._canvas) {
1059
1238
  this._hasPointerListeners[type] = true;
1060
1239
  // For enter/leave events, we need the move handler
1061
- const handler = (type === 'pointerenter' || type === 'pointerleave') ?
1062
- this._pointerHandlers.pointermove :
1063
- this._pointerHandlers[type];
1240
+ const handler = type === 'pointerenter' || type === 'pointerleave'
1241
+ ? this._pointerHandlers.pointermove
1242
+ : this._pointerHandlers[type];
1064
1243
  if (handler) {
1065
1244
  this._canvas.addEventListener(type === 'pointerenter' || type === 'pointerleave' ? 'pointermove' : type, handler);
1066
1245
  }
1067
1246
  }
1068
1247
  }
1069
1248
  _onPointerListenerRemoved(type) {
1070
- const hasListeners = Array.from(this.querySelectorAll('pc-entity'))
1071
- .some(entity => entity.hasListeners(type));
1249
+ const hasListeners = Array.from(this.querySelectorAll('pc-entity')).some((entity) => entity._hasListeners(type));
1072
1250
  if (!hasListeners && this._canvas) {
1073
1251
  this._hasPointerListeners[type] = false;
1074
- const handler = (type === 'pointerenter' || type === 'pointerleave') ?
1075
- this._pointerHandlers.pointermove :
1076
- this._pointerHandlers[type];
1252
+ const handler = type === 'pointerenter' || type === 'pointerleave'
1253
+ ? this._pointerHandlers.pointermove
1254
+ : this._pointerHandlers[type];
1077
1255
  if (handler) {
1078
1256
  this._canvas.removeEventListener(type === 'pointerenter' || type === 'pointerleave' ? 'pointermove' : type, handler);
1079
1257
  }
1080
1258
  }
1081
1259
  }
1082
1260
  /**
1083
- * Sets the alpha flag.
1261
+ * Warns that a graphics option was written too late to have any effect. These options are read
1262
+ * once, when the element connects and creates its graphics device, so a later write updates
1263
+ * only the element's own property - silently, without this.
1264
+ *
1265
+ * @param name - The name of the option, as its attribute.
1266
+ */
1267
+ _warnIfBooted(name) {
1268
+ if (this._optionsLocked) {
1269
+ console.warn(`Attribute '${name}' on <pc-app> is only read when the application boots, so this change has no effect. Set it before the element is connected, or remove and re-insert the element to reboot with the new value.`);
1270
+ }
1271
+ }
1272
+ /**
1273
+ * Sets whether the frame buffer has an alpha channel, which is what lets the page show through
1274
+ * wherever the scene has not drawn. Read only when the application boots.
1084
1275
  * @param value - The alpha flag.
1085
1276
  */
1086
1277
  set alpha(value) {
1278
+ this._warnIfBooted('alpha');
1087
1279
  this._alpha = value;
1088
1280
  }
1089
1281
  /**
1090
- * Gets the alpha flag.
1282
+ * Gets whether the frame buffer has an alpha channel.
1091
1283
  * @returns The alpha flag.
1092
1284
  */
1093
1285
  get alpha() {
1094
1286
  return this._alpha;
1095
1287
  }
1096
1288
  /**
1097
- * Sets the antialias flag.
1289
+ * Sets whether the frame buffer is anti-aliased. Read only when the application boots.
1098
1290
  * @param value - The antialias flag.
1099
1291
  */
1100
1292
  set antialias(value) {
1293
+ this._warnIfBooted('antialias');
1101
1294
  this._antialias = value;
1102
1295
  }
1103
1296
  /**
1104
- * Gets the antialias flag.
1297
+ * Gets whether the frame buffer is anti-aliased.
1105
1298
  * @returns The antialias flag.
1106
1299
  */
1107
1300
  get antialias() {
@@ -1109,10 +1302,11 @@ class AppElement extends AsyncElement {
1109
1302
  }
1110
1303
  /**
1111
1304
  * Sets the graphics backend. Defaults to 'webgpu', which falls back to 'webgl2' if WebGPU
1112
- * is not supported by the browser.
1305
+ * is not supported by the browser. Read only when the application boots.
1113
1306
  * @param value - The graphics backend ('webgpu', 'webgl2', or 'null').
1114
1307
  */
1115
1308
  set backend(value) {
1309
+ this._warnIfBooted('backend');
1116
1310
  this._backend = value;
1117
1311
  }
1118
1312
  /**
@@ -1123,44 +1317,20 @@ class AppElement extends AsyncElement {
1123
1317
  return this._backend;
1124
1318
  }
1125
1319
  /**
1126
- * Sets the depth flag.
1127
- * @param value - The depth flag.
1128
- */
1129
- set depth(value) {
1130
- this._depth = value;
1131
- }
1132
- /**
1133
- * Gets the depth flag.
1134
- * @returns The depth flag.
1135
- */
1136
- get depth() {
1137
- return this._depth;
1138
- }
1139
- /**
1140
- * Gets the hierarchy ready flag.
1141
- * @returns The hierarchy ready flag.
1142
- * @ignore
1143
- */
1144
- get hierarchyReady() {
1145
- return this._hierarchyReady;
1146
- }
1147
- /**
1148
- * Sets the high resolution flag. When true, the application will render at the device's
1149
- * physical resolution. When false, the application will render at CSS resolution.
1150
- * @param value - The high resolution flag.
1320
+ * Sets whether the frame buffer has a depth buffer, which the renderer needs to resolve which
1321
+ * surface is nearest the camera. Read only when the application boots.
1322
+ * @param value - The depth buffer flag.
1151
1323
  */
1152
- set highResolution(value) {
1153
- this._highResolution = value;
1154
- if (this.app) {
1155
- this.app.graphicsDevice.maxPixelRatio = value ? window.devicePixelRatio : 1;
1156
- }
1324
+ set depthBuffer(value) {
1325
+ this._warnIfBooted('depth-buffer');
1326
+ this._depthBuffer = value;
1157
1327
  }
1158
1328
  /**
1159
- * Gets the high resolution flag.
1160
- * @returns The high resolution flag.
1329
+ * Gets whether the frame buffer has a depth buffer.
1330
+ * @returns The depth buffer flag.
1161
1331
  */
1162
- get highResolution() {
1163
- return this._highResolution;
1332
+ get depthBuffer() {
1333
+ return this._depthBuffer;
1164
1334
  }
1165
1335
  /**
1166
1336
  * Sets whether the application shows its built-in loading bar while it boots and preloads its
@@ -1186,21 +1356,45 @@ class AppElement extends AsyncElement {
1186
1356
  return this._loadingBar;
1187
1357
  }
1188
1358
  /**
1189
- * Sets the stencil flag.
1190
- * @param value - The stencil flag.
1359
+ * Sets the cap on the pixel ratio the application renders at. The canvas is sized by the
1360
+ * smaller of this value and the display's own device pixel ratio, so the default of `Infinity`
1361
+ * renders at full physical resolution, `1` renders at CSS resolution, and an intermediate
1362
+ * value such as `2` keeps a dense display sharp without paying for every one of its pixels.
1363
+ * Must be greater than 0. Unlike the other graphics options, this applies immediately.
1364
+ * @param value - The maximum pixel ratio.
1365
+ */
1366
+ set maxPixelRatio(value) {
1367
+ this._maxPixelRatio = value;
1368
+ if (this.app) {
1369
+ this.app.graphicsDevice.maxPixelRatio = value;
1370
+ this.app.resizeCanvas();
1371
+ }
1372
+ }
1373
+ /**
1374
+ * Gets the cap on the pixel ratio the application renders at.
1375
+ * @returns The maximum pixel ratio.
1376
+ */
1377
+ get maxPixelRatio() {
1378
+ return this._maxPixelRatio;
1379
+ }
1380
+ /**
1381
+ * Sets whether the frame buffer has a stencil buffer, which stencil-based effects and UI
1382
+ * masking need. Read only when the application boots.
1383
+ * @param value - The stencil buffer flag.
1191
1384
  */
1192
- set stencil(value) {
1193
- this._stencil = value;
1385
+ set stencilBuffer(value) {
1386
+ this._warnIfBooted('stencil-buffer');
1387
+ this._stencilBuffer = value;
1194
1388
  }
1195
1389
  /**
1196
- * Gets the stencil flag.
1197
- * @returns The stencil flag.
1390
+ * Gets whether the frame buffer has a stencil buffer.
1391
+ * @returns The stencil buffer flag.
1198
1392
  */
1199
- get stencil() {
1200
- return this._stencil;
1393
+ get stencilBuffer() {
1394
+ return this._stencilBuffer;
1201
1395
  }
1202
1396
  static get observedAttributes() {
1203
- return ['alpha', 'antialias', 'backend', 'depth', 'stencil', 'high-resolution', 'loading-bar'];
1397
+ return ['alpha', 'antialias', 'backend', 'depth-buffer', 'loading-bar', 'max-pixel-ratio', 'stencil-buffer'];
1204
1398
  }
1205
1399
  attributeChangedCallback(name, _oldValue, newValue) {
1206
1400
  switch (name) {
@@ -1213,17 +1407,17 @@ class AppElement extends AsyncElement {
1213
1407
  case 'backend':
1214
1408
  this.backend = parseEnum(newValue, ['webgpu', 'webgl2', 'null'], 'webgpu', name);
1215
1409
  break;
1216
- case 'depth':
1217
- this.depth = parseBool(newValue, true);
1218
- break;
1219
- case 'high-resolution':
1220
- this.highResolution = parseBool(newValue, true);
1410
+ case 'depth-buffer':
1411
+ this.depthBuffer = parseBool(newValue, true);
1221
1412
  break;
1222
1413
  case 'loading-bar':
1223
1414
  this.loadingBar = parseBool(newValue, true);
1224
1415
  break;
1225
- case 'stencil':
1226
- this.stencil = parseBool(newValue, true);
1416
+ case 'max-pixel-ratio':
1417
+ this.maxPixelRatio = parseNumber(newValue, Infinity, name);
1418
+ break;
1419
+ case 'stencil-buffer':
1420
+ this.stencilBuffer = parseBool(newValue, true);
1227
1421
  break;
1228
1422
  }
1229
1423
  }
@@ -1292,6 +1486,11 @@ class EntityElement extends AsyncElement {
1292
1486
  */
1293
1487
  _built = false;
1294
1488
  _entity = null;
1489
+ /**
1490
+ * The application element this entity is registered with, cached at creation time so the
1491
+ * entity can be unregistered even once this element has left the DOM.
1492
+ */
1493
+ _appElement = null;
1295
1494
  /**
1296
1495
  * The PlayCanvas entity instance. `null` until the element is ready, and again once it has
1297
1496
  * been removed from the document — await {@link whenReady} or the element's `ready()`
@@ -1301,7 +1500,14 @@ class EntityElement extends AsyncElement {
1301
1500
  get entity() {
1302
1501
  return this._entity;
1303
1502
  }
1304
- createEntity(app) {
1503
+ /**
1504
+ * Creates the backing entity. Called by the containing `<pc-app>` element during its boot
1505
+ * sweep, and on connection for elements inserted while the application is already running.
1506
+ *
1507
+ * @param app - The application to create the entity in.
1508
+ * @internal
1509
+ */
1510
+ _createEntity(app) {
1305
1511
  // Guard against double creation. When a subtree is inserted at runtime (e.g. cloning a
1306
1512
  // `<template>`), an ancestor's connectedCallback eagerly creates descendant entities; the
1307
1513
  // descendants' own connectedCallbacks would otherwise create them a second time.
@@ -1321,8 +1527,41 @@ class EntityElement extends AsyncElement {
1321
1527
  if (this._tags.length > 0) {
1322
1528
  entity.tags.add(this._tags);
1323
1529
  }
1530
+ // Register with the owning application, which joins engine nodes back to elements by
1531
+ // identity (never by name), and hook the entity's destruction. The engine fires 'destroy'
1532
+ // for every entity in a destroyed subtree, so the element learns of its entity's death no
1533
+ // matter who causes it: this element, an ancestor, the whole application, or a user
1534
+ // script calling entity.destroy().
1535
+ this._appElement = this.closestApp;
1536
+ this._appElement?._registerEntityElement(entity, this);
1537
+ entity.once('destroy', this._onEntityDestroy, this);
1538
+ }
1539
+ /**
1540
+ * Handles the destruction of the backing entity. Resets the element so a later re-insertion
1541
+ * starts clean: `_built` must be cleared alongside `_entity`, or _buildHierarchy would bail
1542
+ * and a re-created entity would never be parented. Readiness is re-armed for the same
1543
+ * reason — with the entity gone, a resolved ready promise would resume its awaiters against
1544
+ * a null `entity`.
1545
+ *
1546
+ * @param entity - The entity that was destroyed.
1547
+ */
1548
+ _onEntityDestroy(entity) {
1549
+ this._appElement?._unregisterEntityElement(entity);
1550
+ this._appElement = null;
1551
+ this._entity = null;
1552
+ this._built = false;
1553
+ this._resetReady();
1324
1554
  }
1325
- buildHierarchy(app) {
1555
+ /**
1556
+ * Parents the backing entity: under the entity of the nearest ancestor `<pc-entity>` when
1557
+ * there is one, and under the application root otherwise. Called by the containing `<pc-app>`
1558
+ * element once a sweep has created every entity, so a parent's existence never depends on
1559
+ * document order.
1560
+ *
1561
+ * @param app - The application whose root adopts parentless entities.
1562
+ * @internal
1563
+ */
1564
+ _buildHierarchy(app) {
1326
1565
  if (!this.entity || this._built)
1327
1566
  return;
1328
1567
  this._built = true;
@@ -1348,37 +1587,26 @@ class EntityElement extends AsyncElement {
1348
1587
  return;
1349
1588
  }
1350
1589
  // If app is already running, create entity immediately
1351
- if (closestApp.hierarchyReady) {
1590
+ if (closestApp._hierarchyReady) {
1352
1591
  const app = closestApp.app;
1353
- this.createEntity(app);
1354
- this.buildHierarchy(app);
1592
+ this._createEntity(app);
1593
+ this._buildHierarchy(app);
1355
1594
  // Handle any child entities that might exist
1356
1595
  const childEntities = this.querySelectorAll('pc-entity');
1357
1596
  childEntities.forEach((child) => {
1358
- child.createEntity(app);
1597
+ child._createEntity(app);
1359
1598
  });
1360
1599
  childEntities.forEach((child) => {
1361
- child.buildHierarchy(app);
1600
+ child._buildHierarchy(app);
1362
1601
  });
1363
1602
  }
1364
1603
  }
1365
1604
  disconnectedCallback() {
1366
- if (this.entity) {
1367
- // Notify all children that their entities are about to become invalid. Both fields have
1368
- // to be reset here, not just _entity: a descendant's own disconnectedCallback runs after
1369
- // this one and skips its reset behind the `if (this.entity)` guard, because we have
1370
- // already nulled the entity it tests. Leaving _built set would make buildHierarchy bail
1371
- // on re-insertion, so the descendant would get a fresh entity that is never parented.
1372
- const children = this.querySelectorAll('pc-entity');
1373
- children.forEach((child) => {
1374
- child._entity = null;
1375
- child._built = false;
1376
- });
1377
- // Destroy the entity
1378
- this.entity.destroy();
1379
- this._entity = null;
1380
- this._built = false;
1381
- }
1605
+ // Destroying the entity destroys its whole subtree, and the engine fires 'destroy' for
1606
+ // every entity in it - so _onEntityDestroy resets this element AND every descendant
1607
+ // element before the descendants' own disconnectedCallbacks run. Their entities are null
1608
+ // by then, making this call a no-op for them.
1609
+ this._entity?.destroy();
1382
1610
  }
1383
1611
  /**
1384
1612
  * Sets the enabled state of the entity.
@@ -1563,14 +1791,23 @@ class EntityElement extends AsyncElement {
1563
1791
  }
1564
1792
  removeEventListener(type, listener, options) {
1565
1793
  if (this._listeners[type]) {
1566
- this._listeners[type] = this._listeners[type].filter(l => l !== listener);
1794
+ this._listeners[type] = this._listeners[type].filter((l) => l !== listener);
1567
1795
  }
1568
1796
  super.removeEventListener(type, listener, options);
1569
1797
  if (type.startsWith('pointer')) {
1570
1798
  this.dispatchEvent(new CustomEvent(`${type}:disconnect`, { bubbles: true }));
1571
1799
  }
1572
1800
  }
1573
- hasListeners(type) {
1801
+ /**
1802
+ * Whether the element has a listener for an event type, registered either with
1803
+ * {@link addEventListener} or with the matching inline `onpointer*` attribute. Read by the
1804
+ * containing `<pc-app>` element to gate pointer event synthesis.
1805
+ *
1806
+ * @param type - The event type.
1807
+ * @returns Whether a listener is registered.
1808
+ * @internal
1809
+ */
1810
+ _hasListeners(type) {
1574
1811
  return Boolean(this._listeners[type]?.length) || this._inlineHandlerTypes.has(type);
1575
1812
  }
1576
1813
  }
@@ -1861,7 +2098,7 @@ class AssetElement extends AsyncElement {
1861
2098
  const app = appElement.app;
1862
2099
  if (!app)
1863
2100
  return; // pc-app is re-connecting; its own boot will create this asset
1864
- this.createAsset();
2101
+ this._createAsset();
1865
2102
  if (this.asset) {
1866
2103
  app.assets.add(this.asset); // add() auto-loads when preload is true
1867
2104
  if (!this.lazy) {
@@ -1869,13 +2106,15 @@ class AssetElement extends AsyncElement {
1869
2106
  }
1870
2107
  }
1871
2108
  }
1872
- // Never ready if createAsset failed (unsupported asset type)
2109
+ // Never ready if _createAsset failed (unsupported asset type)
1873
2110
  if (this.asset) {
1874
2111
  this._onReady();
1875
2112
  }
1876
2113
  }
1877
2114
  disconnectedCallback() {
1878
- this.destroyAsset();
2115
+ this._destroyAsset();
2116
+ // Re-arm readiness so a re-inserted element announces the asset it creates then
2117
+ this._resetReady();
1879
2118
  }
1880
2119
  _onAssetLoad() {
1881
2120
  this.dispatchEvent(new Event('load'));
@@ -1885,7 +2124,14 @@ class AssetElement extends AsyncElement {
1885
2124
  message: err instanceof Error ? err.message : String(err)
1886
2125
  }));
1887
2126
  }
1888
- createAsset() {
2127
+ /**
2128
+ * Creates the asset from the element's attributes. Called by the containing `<pc-app>`
2129
+ * element during its boot sweep, and on connection for elements inserted while the
2130
+ * application is already running.
2131
+ *
2132
+ * @internal
2133
+ */
2134
+ _createAsset() {
1889
2135
  const id = this.getAttribute('id') || '';
1890
2136
  const src = this.getAttribute('src') || '';
1891
2137
  let type = this.getAttribute('type');
@@ -1975,7 +2221,7 @@ class AssetElement extends AsyncElement {
1975
2221
  }
1976
2222
  return data;
1977
2223
  }
1978
- destroyAsset() {
2224
+ _destroyAsset() {
1979
2225
  if (this.asset) {
1980
2226
  // A caller that keeps the Asset alive must not dispatch on a removed element
1981
2227
  this.asset.off('load', this._onAssetLoad, this);
@@ -2003,6 +2249,13 @@ class AssetElement extends AsyncElement {
2003
2249
  get lazy() {
2004
2250
  return this._lazy;
2005
2251
  }
2252
+ /**
2253
+ * Returns the {@link Asset} created by the `<pc-asset>` element with the given `id`, or
2254
+ * `undefined` if there is no such element or its asset has not been created yet.
2255
+ *
2256
+ * @param id - The `id` of the `<pc-asset>` element.
2257
+ * @returns The asset, or `undefined`.
2258
+ */
2006
2259
  static get(id) {
2007
2260
  const assetElement = document.querySelector(`pc-asset[id="${id}"]`);
2008
2261
  return assetElement?.asset;
@@ -2028,6 +2281,14 @@ class ComponentElement extends AsyncElement {
2028
2281
  _enabled = true;
2029
2282
  _component = null;
2030
2283
  _appElement = null;
2284
+ /**
2285
+ * Incremented on every connect and disconnect. connectedCallback captures the value on entry
2286
+ * and abandons itself wherever it resumes from an await if the value has moved on — so a
2287
+ * callback whose element was removed cannot act on a torn-down tree, and one whose element
2288
+ * was removed and re-inserted (which runs a callback of its own) cannot add the component a
2289
+ * second time.
2290
+ */
2291
+ _connectionGeneration = 0;
2031
2292
  /**
2032
2293
  * Creates a new ComponentElement instance.
2033
2294
  *
@@ -2038,11 +2299,17 @@ class ComponentElement extends AsyncElement {
2038
2299
  super();
2039
2300
  this._componentName = componentName;
2040
2301
  }
2041
- // Method to be overridden by subclasses to provide initial component data
2302
+ /**
2303
+ * Returns the data the component is created with. Overridden by subclasses to supply the
2304
+ * initial values of their cached properties.
2305
+ *
2306
+ * @returns The initial component data.
2307
+ */
2042
2308
  getInitialComponentData() {
2043
2309
  return {};
2044
2310
  }
2045
- async addComponent() {
2311
+ async _addComponent() {
2312
+ const generation = this._connectionGeneration;
2046
2313
  const entityElement = this.closestEntity;
2047
2314
  if (!entityElement) {
2048
2315
  // A component can only exist on an entity, so an element placed outside one is inert.
@@ -2052,19 +2319,42 @@ class ComponentElement extends AsyncElement {
2052
2319
  return;
2053
2320
  }
2054
2321
  await entityElement.ready();
2322
+ // The element may have been removed, or removed and re-inserted, while the entity became
2323
+ // ready — the component belongs to the connection that owns the current generation.
2324
+ if (generation !== this._connectionGeneration) {
2325
+ return;
2326
+ }
2055
2327
  // Add the component to the entity
2056
2328
  const data = this.getInitialComponentData();
2057
2329
  this._component = entityElement.entity.addComponent(this._componentName, data);
2058
2330
  }
2059
- initComponent() { }
2331
+ /**
2332
+ * Configures the newly added component. Overridden by subclasses whose setup goes beyond
2333
+ * the initial data — child-element handling, asset resolution and the like.
2334
+ */
2335
+ initComponent() {
2336
+ // optional hook
2337
+ }
2060
2338
  async connectedCallback() {
2339
+ const generation = ++this._connectionGeneration;
2061
2340
  this._appElement = this.closestApp ?? null;
2062
2341
  await this._appElement?.ready();
2063
- await this.addComponent();
2342
+ // The element may have been removed, or removed and re-inserted, while the application
2343
+ // became ready. A re-insertion runs a connectedCallback of its own, so a stale resume
2344
+ // must not add the component alongside it.
2345
+ if (generation !== this._connectionGeneration) {
2346
+ return;
2347
+ }
2348
+ await this._addComponent();
2349
+ if (generation !== this._connectionGeneration) {
2350
+ return;
2351
+ }
2064
2352
  this.initComponent();
2065
2353
  this._onReady();
2066
2354
  }
2067
2355
  disconnectedCallback() {
2356
+ // Invalidate any connectedCallback still suspended on an await
2357
+ this._connectionGeneration++;
2068
2358
  // Remove the component when the element is disconnected. Skip this when the owning
2069
2359
  // application has already been destroyed — removing a <pc-app> disconnects it before
2070
2360
  // its children, taking the component systems with it.
@@ -2073,6 +2363,7 @@ class ComponentElement extends AsyncElement {
2073
2363
  }
2074
2364
  this._component = null;
2075
2365
  this._appElement = null;
2366
+ this._resetReady();
2076
2367
  }
2077
2368
  /**
2078
2369
  * The PlayCanvas component instance. `null` until the element is ready, and also for an
@@ -2525,6 +2816,10 @@ class ButtonComponentElement extends ComponentElement {
2525
2816
  }
2526
2817
  customElements.define('pc-button', ButtonComponentElement);
2527
2818
 
2819
+ const projections = new Map([
2820
+ ['perspective', playcanvas.PROJECTION_PERSPECTIVE],
2821
+ ['orthographic', playcanvas.PROJECTION_ORTHOGRAPHIC]
2822
+ ]);
2528
2823
  const tonemaps = new Map([
2529
2824
  ['none', playcanvas.TONEMAP_NONE],
2530
2825
  ['linear', playcanvas.TONEMAP_LINEAR],
@@ -2555,7 +2850,7 @@ class CameraComponentElement extends ComponentElement {
2555
2850
  _gamma = 'srgb';
2556
2851
  _horizontalFov = false;
2557
2852
  _nearClip = 0.1;
2558
- _orthographic = false;
2853
+ _projection = 'perspective';
2559
2854
  _orthoHeight = 10;
2560
2855
  _priority = 0;
2561
2856
  _rect = new playcanvas.Vec4(0, 0, 1, 1);
@@ -2579,12 +2874,12 @@ class CameraComponentElement extends ComponentElement {
2579
2874
  gammaCorrection: this._gamma === 'srgb' ? playcanvas.GAMMA_SRGB : playcanvas.GAMMA_NONE,
2580
2875
  horizontalFov: this._horizontalFov,
2581
2876
  nearClip: this._nearClip,
2582
- projection: this._orthographic ? playcanvas.PROJECTION_ORTHOGRAPHIC : playcanvas.PROJECTION_PERSPECTIVE,
2877
+ projection: projections.get(this._projection) ?? playcanvas.PROJECTION_PERSPECTIVE,
2583
2878
  orthoHeight: this._orthoHeight,
2584
2879
  priority: this._priority,
2585
2880
  rect: this._rect,
2586
2881
  scissorRect: this._scissorRect,
2587
- toneMapping: tonemaps.get(this._tonemap)
2882
+ toneMapping: tonemaps.get(this._tonemap) ?? playcanvas.TONEMAP_NONE
2588
2883
  };
2589
2884
  }
2590
2885
  get xrAvailable() {
@@ -2826,23 +3121,6 @@ class CameraComponentElement extends ComponentElement {
2826
3121
  get nearClip() {
2827
3122
  return this._nearClip;
2828
3123
  }
2829
- /**
2830
- * Sets the orthographic projection of the camera.
2831
- * @param value - The orthographic projection.
2832
- */
2833
- set orthographic(value) {
2834
- this._orthographic = value;
2835
- if (this.component) {
2836
- this.component.projection = value ? playcanvas.PROJECTION_ORTHOGRAPHIC : playcanvas.PROJECTION_PERSPECTIVE;
2837
- }
2838
- }
2839
- /**
2840
- * Gets the orthographic projection of the camera.
2841
- * @returns The orthographic projection.
2842
- */
2843
- get orthographic() {
2844
- return this._orthographic;
2845
- }
2846
3124
  /**
2847
3125
  * Sets the orthographic height of the camera.
2848
3126
  * @param value - The orthographic height.
@@ -2877,6 +3155,23 @@ class CameraComponentElement extends ComponentElement {
2877
3155
  get priority() {
2878
3156
  return this._priority;
2879
3157
  }
3158
+ /**
3159
+ * Sets the projection of the camera. Use `orthoHeight` to size an orthographic projection.
3160
+ * @param value - The projection ('perspective' or 'orthographic').
3161
+ */
3162
+ set projection(value) {
3163
+ this._projection = value;
3164
+ if (this.component) {
3165
+ this.component.projection = projections.get(value) ?? playcanvas.PROJECTION_PERSPECTIVE;
3166
+ }
3167
+ }
3168
+ /**
3169
+ * Gets the projection of the camera.
3170
+ * @returns The projection.
3171
+ */
3172
+ get projection() {
3173
+ return this._projection;
3174
+ }
2880
3175
  /**
2881
3176
  * Sets the rect of the camera.
2882
3177
  * @param value - The rect.
@@ -2943,9 +3238,9 @@ class CameraComponentElement extends ComponentElement {
2943
3238
  'gamma',
2944
3239
  'horizontal-fov',
2945
3240
  'near-clip',
2946
- 'orthographic',
2947
3241
  'ortho-height',
2948
3242
  'priority',
3243
+ 'projection',
2949
3244
  'rect',
2950
3245
  'scissor-rect',
2951
3246
  'tonemap'
@@ -2990,15 +3285,15 @@ class CameraComponentElement extends ComponentElement {
2990
3285
  case 'near-clip':
2991
3286
  this.nearClip = parseNumber(newValue, 0.1, name);
2992
3287
  break;
2993
- case 'orthographic':
2994
- this.orthographic = parseBool(newValue, false);
2995
- break;
2996
3288
  case 'ortho-height':
2997
3289
  this.orthoHeight = parseNumber(newValue, 10, name);
2998
3290
  break;
2999
3291
  case 'priority':
3000
3292
  this.priority = parseNumber(newValue, 0, name);
3001
3293
  break;
3294
+ case 'projection':
3295
+ this.projection = parseEnum(newValue, projections, 'perspective', name);
3296
+ break;
3002
3297
  case 'rect':
3003
3298
  this.rect = parseVec4(newValue, new playcanvas.Vec4(0, 0, 1, 1), name);
3004
3299
  break;
@@ -3126,7 +3421,17 @@ class CollisionComponentElement extends ComponentElement {
3126
3421
  return this._type;
3127
3422
  }
3128
3423
  static get observedAttributes() {
3129
- return [...super.observedAttributes, 'angular-offset', 'axis', 'convex-hull', 'half-extents', 'height', 'linear-offset', 'radius', 'type'];
3424
+ return [
3425
+ ...super.observedAttributes,
3426
+ 'angular-offset',
3427
+ 'axis',
3428
+ 'convex-hull',
3429
+ 'half-extents',
3430
+ 'height',
3431
+ 'linear-offset',
3432
+ 'radius',
3433
+ 'type'
3434
+ ];
3130
3435
  }
3131
3436
  attributeChangedCallback(name, _oldValue, newValue) {
3132
3437
  super.attributeChangedCallback(name, _oldValue, newValue);
@@ -4831,7 +5136,7 @@ class ParticleSystemComponentElement extends ComponentElement {
4831
5136
  }
4832
5137
  // Set all the config properties on the component
4833
5138
  for (const key in resource) {
4834
- if (resource.hasOwnProperty(key)) {
5139
+ if (Object.hasOwn(resource, key)) {
4835
5140
  this.component[key] = resource[key];
4836
5141
  }
4837
5142
  }
@@ -4904,10 +5209,7 @@ class ParticleSystemComponentElement extends ComponentElement {
4904
5209
  }
4905
5210
  }
4906
5211
  static get observedAttributes() {
4907
- return [
4908
- ...super.observedAttributes,
4909
- 'asset'
4910
- ];
5212
+ return [...super.observedAttributes, 'asset'];
4911
5213
  }
4912
5214
  attributeChangedCallback(name, _oldValue, newValue) {
4913
5215
  super.attributeChangedCallback(name, _oldValue, newValue);
@@ -5067,7 +5369,7 @@ class MaterialElement extends HTMLElement {
5067
5369
  _twoSidedLighting = false;
5068
5370
  _useFog = true;
5069
5371
  _useLighting = true;
5070
- // Diverges from the engine default of false - see the class docblock and createMaterial()
5372
+ // Diverges from the engine default of false - see the class docblock and _createMaterial()
5071
5373
  _useMetalness = true;
5072
5374
  _useMetalnessSpecularColor = false;
5073
5375
  _useSkybox = true;
@@ -5080,6 +5382,10 @@ class MaterialElement extends HTMLElement {
5080
5382
  _mapHandles = new Map();
5081
5383
  _updateScheduled = false;
5082
5384
  _glossConflictWarned = false;
5385
+ /**
5386
+ * The material. `null` until the containing application has created it — an element present
5387
+ * at startup has its material once the application is ready.
5388
+ */
5083
5389
  material = null;
5084
5390
  async connectedCallback() {
5085
5391
  const appElement = this.parentElement?.closest('pc-app') ?? null;
@@ -5097,10 +5403,17 @@ class MaterialElement extends HTMLElement {
5097
5403
  if (!this.material) {
5098
5404
  if (!appElement.app)
5099
5405
  return; // pc-app is re-connecting; its own boot will create this
5100
- this.createMaterial();
5406
+ this._createMaterial();
5101
5407
  }
5102
5408
  }
5103
- createMaterial() {
5409
+ /**
5410
+ * Creates the material from the element's cached properties. Called by the containing
5411
+ * `<pc-app>` element during its boot sweep, and on connection for elements inserted while
5412
+ * the application is already running.
5413
+ *
5414
+ * @internal
5415
+ */
5416
+ _createMaterial() {
5104
5417
  const material = new playcanvas.StandardMaterial();
5105
5418
  this.material = material;
5106
5419
  material.alphaTest = this._alphaTest;
@@ -5227,9 +5540,9 @@ class MaterialElement extends HTMLElement {
5227
5540
  * warning latches and reports once per episode, clearing when the clash is resolved.
5228
5541
  */
5229
5542
  _warnGlossConflict() {
5230
- const quote = (names) => `'${names.join('\', \'')}'`;
5231
- const roughness = roughnessAliases.filter(name => this.hasAttribute(name));
5232
- const gloss = glossConflicts.filter(name => this.hasAttribute(name));
5543
+ const quote = (names) => `'${names.join("', '")}'`;
5544
+ const roughness = roughnessAliases.filter((name) => this.hasAttribute(name));
5545
+ const gloss = glossConflicts.filter((name) => this.hasAttribute(name));
5233
5546
  if (roughness.length === 0 || gloss.length === 0) {
5234
5547
  this._glossConflictWarned = false;
5235
5548
  return;
@@ -5247,7 +5560,7 @@ class MaterialElement extends HTMLElement {
5247
5560
  * @param id - The id of the `pc-asset`, or an empty string to clear the slot.
5248
5561
  * @param slot - The material property to write.
5249
5562
  */
5250
- setMap(id, slot) {
5563
+ _setMap(id, slot) {
5251
5564
  // Drop any load still pending for this slot - its texture is no longer the one we want
5252
5565
  this._mapHandles.get(slot)?.off();
5253
5566
  this._mapHandles.delete(slot);
@@ -5341,7 +5654,7 @@ class MaterialElement extends HTMLElement {
5341
5654
  */
5342
5655
  set aoMap(value) {
5343
5656
  this._aoMap = value;
5344
- this.setMap(value, 'aoMap');
5657
+ this._setMap(value, 'aoMap');
5345
5658
  }
5346
5659
  /**
5347
5660
  * Gets the id of the `pc-asset` used as the ambient occlusion map.
@@ -5573,7 +5886,7 @@ class MaterialElement extends HTMLElement {
5573
5886
  */
5574
5887
  set diffuseMap(value) {
5575
5888
  this._diffuseMap = value;
5576
- this.setMap(value, 'diffuseMap');
5889
+ this._setMap(value, 'diffuseMap');
5577
5890
  }
5578
5891
  /**
5579
5892
  * Gets the id of the `pc-asset` used as the diffuse map.
@@ -5714,7 +6027,7 @@ class MaterialElement extends HTMLElement {
5714
6027
  */
5715
6028
  set emissiveMap(value) {
5716
6029
  this._emissiveMap = value;
5717
- this.setMap(value, 'emissiveMap');
6030
+ this._setMap(value, 'emissiveMap');
5718
6031
  }
5719
6032
  /**
5720
6033
  * Gets the id of the `pc-asset` used as the emissive map.
@@ -5892,7 +6205,7 @@ class MaterialElement extends HTMLElement {
5892
6205
  */
5893
6206
  set glossMap(value) {
5894
6207
  this._glossMap = value;
5895
- this.setMap(value, 'glossMap');
6208
+ this._setMap(value, 'glossMap');
5896
6209
  }
5897
6210
  /**
5898
6211
  * Gets the id of the `pc-asset` used as the gloss map.
@@ -5997,7 +6310,7 @@ class MaterialElement extends HTMLElement {
5997
6310
  */
5998
6311
  set heightMap(value) {
5999
6312
  this._heightMap = value;
6000
- this.setMap(value, 'heightMap');
6313
+ this._setMap(value, 'heightMap');
6001
6314
  }
6002
6315
  /**
6003
6316
  * Gets the id of the `pc-asset` used as the height map.
@@ -6138,7 +6451,7 @@ class MaterialElement extends HTMLElement {
6138
6451
  */
6139
6452
  set metalnessMap(value) {
6140
6453
  this._metalnessMap = value;
6141
- this.setMap(value, 'metalnessMap');
6454
+ this._setMap(value, 'metalnessMap');
6142
6455
  }
6143
6456
  /**
6144
6457
  * Gets the id of the `pc-asset` used as the metalness map.
@@ -6243,7 +6556,7 @@ class MaterialElement extends HTMLElement {
6243
6556
  */
6244
6557
  set normalMap(value) {
6245
6558
  this._normalMap = value;
6246
- this.setMap(value, 'normalMap');
6559
+ this._setMap(value, 'normalMap');
6247
6560
  }
6248
6561
  /**
6249
6562
  * Gets the id of the `pc-asset` used as the normal map.
@@ -6331,7 +6644,7 @@ class MaterialElement extends HTMLElement {
6331
6644
  set occludeDirect(value) {
6332
6645
  this._occludeDirect = value;
6333
6646
  if (this.material) {
6334
- // @ts-ignore see createMaterial() - the engine mistypes occludeDirect as a number
6647
+ // @ts-ignore see _createMaterial() - the engine mistypes occludeDirect as a number
6335
6648
  this.material.occludeDirect = value;
6336
6649
  this._scheduleUpdate();
6337
6650
  }
@@ -6423,7 +6736,7 @@ class MaterialElement extends HTMLElement {
6423
6736
  */
6424
6737
  set opacityMap(value) {
6425
6738
  this._opacityMap = value;
6426
- this.setMap(value, 'opacityMap');
6739
+ this._setMap(value, 'opacityMap');
6427
6740
  }
6428
6741
  /**
6429
6742
  * Gets the id of the `pc-asset` used as the opacity map.
@@ -6741,6 +7054,13 @@ class MaterialElement extends HTMLElement {
6741
7054
  get useTonemap() {
6742
7055
  return this._useTonemap;
6743
7056
  }
7057
+ /**
7058
+ * Returns the {@link StandardMaterial} created by the `<pc-material>` element with the given
7059
+ * `id`, or `undefined` if there is no such element or its material has not been created yet.
7060
+ *
7061
+ * @param id - The `id` of the `<pc-material>` element.
7062
+ * @returns The material, or `undefined`.
7063
+ */
6744
7064
  static get(id) {
6745
7065
  const materialElement = document.querySelector(`pc-material[id="${id}"]`);
6746
7066
  return materialElement?.material;
@@ -7379,7 +7699,18 @@ class RigidBodyComponentElement extends ComponentElement {
7379
7699
  return this._type;
7380
7700
  }
7381
7701
  static get observedAttributes() {
7382
- return [...super.observedAttributes, 'angular-damping', 'angular-factor', 'friction', 'linear-damping', 'linear-factor', 'mass', 'restitution', 'rolling-friction', 'type'];
7702
+ return [
7703
+ ...super.observedAttributes,
7704
+ 'angular-damping',
7705
+ 'angular-factor',
7706
+ 'friction',
7707
+ 'linear-damping',
7708
+ 'linear-factor',
7709
+ 'mass',
7710
+ 'restitution',
7711
+ 'rolling-friction',
7712
+ 'type'
7713
+ ];
7383
7714
  }
7384
7715
  attributeChangedCallback(name, _oldValue, newValue) {
7385
7716
  super.attributeChangedCallback(name, _oldValue, newValue);
@@ -7416,6 +7747,14 @@ class RigidBodyComponentElement extends ComponentElement {
7416
7747
  }
7417
7748
  customElements.define('pc-rigidbody', RigidBodyComponentElement);
7418
7749
 
7750
+ // The engine's SCALEMODE_* constants are the strings 'none' and 'blend', so this map happens to be
7751
+ // an identity. It is still the right shape: it supplies parseEnum's valid-name list, it is what the
7752
+ // manifest generator reads the enum values from, and it keeps the attribute vocabulary independent
7753
+ // of constants the engine is free to change.
7754
+ const scaleModes = new Map([
7755
+ ['none', playcanvas.SCALEMODE_NONE],
7756
+ ['blend', playcanvas.SCALEMODE_BLEND]
7757
+ ]);
7419
7758
  /**
7420
7759
  * The ScreenComponentElement interface provides properties and methods for manipulating
7421
7760
  * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-screen/ | `<pc-screen>`} elements.
@@ -7429,7 +7768,7 @@ class ScreenComponentElement extends ComponentElement {
7429
7768
  _resolution = new playcanvas.Vec2(640, 320);
7430
7769
  _referenceResolution = new playcanvas.Vec2(640, 320);
7431
7770
  _priority = 0;
7432
- _blend = false;
7771
+ _scaleMode = 'none';
7433
7772
  _scaleBlend = 0.5;
7434
7773
  /** @ignore */
7435
7774
  constructor() {
@@ -7441,7 +7780,7 @@ class ScreenComponentElement extends ComponentElement {
7441
7780
  referenceResolution: this._referenceResolution,
7442
7781
  resolution: this._resolution,
7443
7782
  scaleBlend: this._scaleBlend,
7444
- scaleMode: this._blend ? playcanvas.SCALEMODE_BLEND : playcanvas.SCALEMODE_NONE,
7783
+ scaleMode: scaleModes.get(this._scaleMode) ?? playcanvas.SCALEMODE_NONE,
7445
7784
  screenSpace: this._screenSpace
7446
7785
  };
7447
7786
  }
@@ -7479,23 +7818,44 @@ class ScreenComponentElement extends ComponentElement {
7479
7818
  get resolution() {
7480
7819
  return this._resolution;
7481
7820
  }
7821
+ /**
7822
+ * Sets how the screen's `resolution` and `referenceResolution` are weighted against each other
7823
+ * when `scaleMode` is `blend`, from 0 (follow the resolution) to 1 (follow the reference
7824
+ * resolution). Ignored while `scaleMode` is `none`.
7825
+ * @param value - The scale blend factor.
7826
+ */
7482
7827
  set scaleBlend(value) {
7483
7828
  this._scaleBlend = value;
7484
7829
  if (this.component) {
7485
7830
  this.component.scaleBlend = this._scaleBlend;
7486
7831
  }
7487
7832
  }
7833
+ /**
7834
+ * Gets how the screen's resolutions are weighted against each other.
7835
+ * @returns The scale blend factor.
7836
+ */
7488
7837
  get scaleBlend() {
7489
7838
  return this._scaleBlend;
7490
7839
  }
7491
- set blend(value) {
7492
- this._blend = value;
7840
+ /**
7841
+ * Sets how the screen scales its contents. `none` renders at `resolution` and ignores
7842
+ * `referenceResolution`; `blend` scales between the two, weighted by `scaleBlend`, which is what
7843
+ * keeps a UI laid out at one resolution usable at another. Requires `screenSpace` - the engine
7844
+ * forces `none` on a world-space screen, which does not support scaling.
7845
+ * @param value - The scale mode ('none' or 'blend').
7846
+ */
7847
+ set scaleMode(value) {
7848
+ this._scaleMode = value;
7493
7849
  if (this.component) {
7494
- this.component.scaleMode = this._blend ? playcanvas.SCALEMODE_BLEND : playcanvas.SCALEMODE_NONE;
7850
+ this.component.scaleMode = scaleModes.get(value) ?? playcanvas.SCALEMODE_NONE;
7495
7851
  }
7496
7852
  }
7497
- get blend() {
7498
- return this._blend;
7853
+ /**
7854
+ * Gets how the screen scales its contents.
7855
+ * @returns The scale mode.
7856
+ */
7857
+ get scaleMode() {
7858
+ return this._scaleMode;
7499
7859
  }
7500
7860
  set screenSpace(value) {
7501
7861
  this._screenSpace = value;
@@ -7509,12 +7869,12 @@ class ScreenComponentElement extends ComponentElement {
7509
7869
  static get observedAttributes() {
7510
7870
  return [
7511
7871
  ...super.observedAttributes,
7512
- 'blend',
7513
7872
  'screen-space',
7514
7873
  'resolution',
7515
7874
  'reference-resolution',
7516
7875
  'priority',
7517
- 'scale-blend'
7876
+ 'scale-blend',
7877
+ 'scale-mode'
7518
7878
  ];
7519
7879
  }
7520
7880
  attributeChangedCallback(name, _oldValue, newValue) {
@@ -7532,8 +7892,8 @@ class ScreenComponentElement extends ComponentElement {
7532
7892
  case 'scale-blend':
7533
7893
  this.scaleBlend = parseNumber(newValue, 0.5, name);
7534
7894
  break;
7535
- case 'blend':
7536
- this.blend = parseBool(newValue, false);
7895
+ case 'scale-mode':
7896
+ this.scaleMode = parseEnum(newValue, scaleModes, 'none', name);
7537
7897
  break;
7538
7898
  case 'screen-space':
7539
7899
  this.screenSpace = parseBool(newValue, false);
@@ -7655,13 +8015,7 @@ class ScrollbarComponentElement extends ComponentElement {
7655
8015
  return this._handle;
7656
8016
  }
7657
8017
  static get observedAttributes() {
7658
- return [
7659
- ...super.observedAttributes,
7660
- 'orientation',
7661
- 'value',
7662
- 'handle-size',
7663
- 'handle'
7664
- ];
8018
+ return [...super.observedAttributes, 'orientation', 'value', 'handle-size', 'handle'];
7665
8019
  }
7666
8020
  attributeChangedCallback(name, _oldValue, newValue) {
7667
8021
  super.attributeChangedCallback(name, _oldValue, newValue);
@@ -7756,7 +8110,8 @@ class ScrollViewComponentElement extends ComponentElement {
7756
8110
  return super.component;
7757
8111
  }
7758
8112
  /**
7759
- * Sets whether horizontal scrolling is enabled.
8113
+ * Sets whether scrolling along the horizontal axis is enabled. This is a toggle, unlike the
8114
+ * `orientation` of a `<pc-scrollbar>`, for which `horizontal` is one of the accepted values.
7760
8115
  * @param value - Whether horizontal scrolling is enabled.
7761
8116
  */
7762
8117
  set horizontal(value) {
@@ -7766,14 +8121,15 @@ class ScrollViewComponentElement extends ComponentElement {
7766
8121
  }
7767
8122
  }
7768
8123
  /**
7769
- * Gets whether horizontal scrolling is enabled.
8124
+ * Gets whether scrolling along the horizontal axis is enabled.
7770
8125
  * @returns Whether horizontal scrolling is enabled.
7771
8126
  */
7772
8127
  get horizontal() {
7773
8128
  return this._horizontal;
7774
8129
  }
7775
8130
  /**
7776
- * Sets whether vertical scrolling is enabled.
8131
+ * Sets whether scrolling along the vertical axis is enabled. This is a toggle, unlike the
8132
+ * `orientation` of a `<pc-scrollbar>`, for which `vertical` is one of the accepted values.
7777
8133
  * @param value - Whether vertical scrolling is enabled.
7778
8134
  */
7779
8135
  set vertical(value) {
@@ -7783,7 +8139,7 @@ class ScrollViewComponentElement extends ComponentElement {
7783
8139
  }
7784
8140
  }
7785
8141
  /**
7786
- * Gets whether vertical scrolling is enabled.
8142
+ * Gets whether scrolling along the vertical axis is enabled.
7787
8143
  * @returns Whether vertical scrolling is enabled.
7788
8144
  */
7789
8145
  get vertical() {
@@ -7885,7 +8241,8 @@ class ScrollViewComponentElement extends ComponentElement {
7885
8241
  set horizontalScrollbarVisibility(value) {
7886
8242
  this._horizontalScrollbarVisibility = value;
7887
8243
  if (this.component) {
7888
- this.component.horizontalScrollbarVisibility = visibilities.get(value) ?? playcanvas.SCROLLBAR_VISIBILITY_SHOW_WHEN_REQUIRED;
8244
+ this.component.horizontalScrollbarVisibility =
8245
+ visibilities.get(value) ?? playcanvas.SCROLLBAR_VISIBILITY_SHOW_WHEN_REQUIRED;
7889
8246
  }
7890
8247
  }
7891
8248
  /**
@@ -7903,7 +8260,8 @@ class ScrollViewComponentElement extends ComponentElement {
7903
8260
  set verticalScrollbarVisibility(value) {
7904
8261
  this._verticalScrollbarVisibility = value;
7905
8262
  if (this.component) {
7906
- this.component.verticalScrollbarVisibility = visibilities.get(value) ?? playcanvas.SCROLLBAR_VISIBILITY_SHOW_WHEN_REQUIRED;
8263
+ this.component.verticalScrollbarVisibility =
8264
+ visibilities.get(value) ?? playcanvas.SCROLLBAR_VISIBILITY_SHOW_WHEN_REQUIRED;
7907
8265
  }
7908
8266
  }
7909
8267
  /**
@@ -8090,15 +8448,9 @@ customElements.define('pc-scrollview', ScrollViewComponentElement);
8090
8448
  class ScriptElement extends AsyncElement {
8091
8449
  _attributes = {};
8092
8450
  _enabled = true;
8093
- /**
8094
- * Whether readiness has been signalled. Creation can happen more than once over an
8095
- * element's life (a runtime `name` change recreates the instance), but `ready` is a
8096
- * one-shot signal, so only the first successful creation fires it.
8097
- */
8098
- _readySignalled = false;
8099
8451
  /**
8100
8452
  * The Script instance created for this element by its parent `<pc-scripts>` element.
8101
- * @ignore
8453
+ * @internal
8102
8454
  */
8103
8455
  _script = null;
8104
8456
  /**
@@ -8180,14 +8532,20 @@ class ScriptElement extends AsyncElement {
8180
8532
  console.warn(`pc-script '${this.getAttribute('name')}' must be a direct child of pc-scripts - script not created`);
8181
8533
  }
8182
8534
  }
8535
+ disconnectedCallback() {
8536
+ // Re-arm readiness so a re-inserted element announces the instance created for it then.
8537
+ // `_script` is deliberately NOT cleared here: the parent's mutation observer processes
8538
+ // this removal afterwards and reads it to establish which engine script this element
8539
+ // owned - the parent is what clears it.
8540
+ this._resetReady();
8541
+ }
8183
8542
  /**
8184
8543
  * Called by the parent `<pc-scripts>` element when the script instance has been created.
8185
- * @ignore
8544
+ * Creation can happen more than once per connection (a runtime `name` change recreates the
8545
+ * instance), but `_onReady` signals readiness at most once per cycle.
8546
+ * @internal
8186
8547
  */
8187
8548
  _onScriptCreated() {
8188
- if (this._readySignalled)
8189
- return;
8190
- this._readySignalled = true;
8191
8549
  this._onReady();
8192
8550
  }
8193
8551
  static get observedAttributes() {
@@ -8232,10 +8590,34 @@ customElements.define('pc-script', ScriptElement);
8232
8590
  */
8233
8591
  const RESERVED_ATTRIBUTES = new Set([
8234
8592
  ...ScriptElement.observedAttributes,
8235
- 'accesskey', 'autocapitalize', 'autofocus', 'class', 'contenteditable', 'dir', 'draggable',
8236
- 'exportparts', 'hidden', 'id', 'inert', 'is', 'itemid', 'itemprop', 'itemref', 'itemscope',
8237
- 'itemtype', 'lang', 'nonce', 'part', 'popover', 'role', 'slot', 'spellcheck', 'style',
8238
- 'tabindex', 'title', 'translate'
8593
+ 'accesskey',
8594
+ 'autocapitalize',
8595
+ 'autofocus',
8596
+ 'class',
8597
+ 'contenteditable',
8598
+ 'dir',
8599
+ 'draggable',
8600
+ 'exportparts',
8601
+ 'hidden',
8602
+ 'id',
8603
+ 'inert',
8604
+ 'is',
8605
+ 'itemid',
8606
+ 'itemprop',
8607
+ 'itemref',
8608
+ 'itemscope',
8609
+ 'itemtype',
8610
+ 'lang',
8611
+ 'nonce',
8612
+ 'part',
8613
+ 'popover',
8614
+ 'role',
8615
+ 'slot',
8616
+ 'spellcheck',
8617
+ 'style',
8618
+ 'tabindex',
8619
+ 'title',
8620
+ 'translate'
8239
8621
  ]);
8240
8622
  /**
8241
8623
  * Checks whether a `pc-script` attribute name is reserved (and so never maps to a script
@@ -8247,18 +8629,25 @@ const RESERVED_ATTRIBUTES = new Set([
8247
8629
  * @returns Whether the attribute name is reserved.
8248
8630
  */
8249
8631
  const isReservedAttribute = (name) => {
8250
- return RESERVED_ATTRIBUTES.has(name) ||
8632
+ return (RESERVED_ATTRIBUTES.has(name) ||
8251
8633
  name.startsWith('data-') ||
8252
8634
  name.startsWith('aria-') ||
8253
8635
  name.startsWith('_') ||
8254
- (name.startsWith('on') && name in HTMLElement.prototype);
8636
+ (name.startsWith('on') && name in HTMLElement.prototype));
8255
8637
  };
8256
8638
  /**
8257
8639
  * Script API members that per-property attributes must never overwrite: the engine bindings and
8258
8640
  * the (optional, so possibly undefined) lifecycle methods.
8259
8641
  */
8260
8642
  const SCRIPT_API_MEMBERS = new Set([
8261
- 'app', 'entity', 'destroy', 'initialize', 'postInitialize', 'postUpdate', 'swap', 'update'
8643
+ 'app',
8644
+ 'entity',
8645
+ 'destroy',
8646
+ 'initialize',
8647
+ 'postInitialize',
8648
+ 'postUpdate',
8649
+ 'swap',
8650
+ 'update'
8262
8651
  ]);
8263
8652
  /**
8264
8653
  * Converts a kebab-case attribute name to the camelCase script attribute name.
@@ -8274,7 +8663,7 @@ const kebabToCamel = (name) => {
8274
8663
  * @returns The kebab-case name.
8275
8664
  */
8276
8665
  const camelToKebab = (name) => {
8277
- return name.replace(/[A-Z]/g, char => `-${char.toLowerCase()}`);
8666
+ return name.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`);
8278
8667
  };
8279
8668
  /**
8280
8669
  * Resolves an `asset:` prefix to the Asset created by the `pc-asset` element with that id.
@@ -8473,7 +8862,10 @@ class ScriptComponentElement extends ComponentElement {
8473
8862
  // Only recurse into plain objects. Class instances (Vec3, Color, Asset, Entity...)
8474
8863
  // are leaf values assigned whole, so accessor-typed script attributes receive them
8475
8864
  // through their setters instead of having a getter's returned copy mutated.
8476
- if (value && typeof value === 'object' && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype) {
8865
+ if (value &&
8866
+ typeof value === 'object' &&
8867
+ !Array.isArray(value) &&
8868
+ Object.getPrototypeOf(value) === Object.prototype) {
8477
8869
  if (!current || typeof current !== 'object') {
8478
8870
  target[key] = {};
8479
8871
  }
@@ -8491,7 +8883,11 @@ class ScriptComponentElement extends ComponentElement {
8491
8883
  * @returns Whether the value is a math type.
8492
8884
  */
8493
8885
  isMathType(value) {
8494
- return value instanceof playcanvas.Vec2 || value instanceof playcanvas.Vec3 || value instanceof playcanvas.Vec4 || value instanceof playcanvas.Color || value instanceof playcanvas.Quat;
8886
+ return (value instanceof playcanvas.Vec2 ||
8887
+ value instanceof playcanvas.Vec3 ||
8888
+ value instanceof playcanvas.Vec4 ||
8889
+ value instanceof playcanvas.Color ||
8890
+ value instanceof playcanvas.Quat);
8495
8891
  }
8496
8892
  /**
8497
8893
  * Converts a plain numeric array to the math type of `current`. A 3-element array targeting
@@ -8504,7 +8900,7 @@ class ScriptComponentElement extends ComponentElement {
8504
8900
  * @returns The converted value, or `null`.
8505
8901
  */
8506
8902
  arrayToMathType(current, value, key) {
8507
- if (value.every(component => typeof component === 'number' && Number.isFinite(component))) {
8903
+ if (value.every((component) => typeof component === 'number' && Number.isFinite(component))) {
8508
8904
  if (current instanceof playcanvas.Vec2 && value.length === 2)
8509
8905
  return new playcanvas.Vec2(value);
8510
8906
  if (current instanceof playcanvas.Vec3 && value.length === 3)
@@ -8776,7 +9172,10 @@ class ScriptComponentElement extends ComponentElement {
8776
9172
  mutation.removedNodes.forEach((node) => {
8777
9173
  if (node instanceof ScriptElement) {
8778
9174
  const scriptName = node.getAttribute('name');
8779
- if (scriptName && node._script && this.component && this.component.get(scriptName) === node._script) {
9175
+ if (scriptName &&
9176
+ node._script &&
9177
+ this.component &&
9178
+ this.component.get(scriptName) === node._script) {
8780
9179
  this.destroyScript(scriptName);
8781
9180
  }
8782
9181
  node._script = null;
@@ -9025,18 +9424,27 @@ class SoundSlotElement extends AsyncElement {
9025
9424
  * emit a misleading "must be a direct child" warning for what is an ordinary removal.
9026
9425
  */
9027
9426
  _soundElement = null;
9427
+ /**
9428
+ * Incremented on every connect and disconnect, and captured by connectedCallback on entry —
9429
+ * a resume from an await abandons itself if the value has moved on, so a stale callback can
9430
+ * neither act on a torn-down tree nor add its slot alongside a re-inserted element's own
9431
+ * callback.
9432
+ */
9433
+ _connectionGeneration = 0;
9028
9434
  /**
9029
9435
  * The sound slot.
9030
9436
  */
9031
9437
  soundSlot = null;
9032
9438
  async connectedCallback() {
9439
+ const generation = ++this._connectionGeneration;
9033
9440
  const soundElement = this.soundElement;
9034
9441
  await soundElement?.ready();
9035
- // The element may have been removed, or its parent torn down, while we were waiting. A
9036
- // <pc-app> disconnects before its children, so by the time we resume the component can
9037
- // already be gone - see the matching guard in disconnectedCallback below.
9442
+ // The element may have been removed (perhaps re-inserted, which runs a callback of its
9443
+ // own), or its parent torn down, while we were waiting. A <pc-app> disconnects before
9444
+ // its children, so by the time we resume the component can already be gone - see the
9445
+ // matching guard in disconnectedCallback below.
9038
9446
  const component = soundElement?.component;
9039
- if (!this.isConnected || !component) {
9447
+ if (generation !== this._connectionGeneration || !component) {
9040
9448
  return;
9041
9449
  }
9042
9450
  const options = {
@@ -9059,12 +9467,15 @@ class SoundSlotElement extends AsyncElement {
9059
9467
  this._onReady();
9060
9468
  }
9061
9469
  disconnectedCallback() {
9470
+ // Invalidate any connectedCallback still suspended on an await
9471
+ this._connectionGeneration++;
9062
9472
  // Uses the cached parent rather than a fresh lookup, since parentElement is already null
9063
9473
  // by now. The component itself is null if the parent <pc-sound> (or the whole <pc-app>) is
9064
9474
  // being torn down — parents disconnect first and have already removed the component.
9065
9475
  this._soundElement?.component?.removeSlot(this._name);
9066
9476
  this._soundElement = null;
9067
9477
  this.soundSlot = null;
9478
+ this._resetReady();
9068
9479
  }
9069
9480
  get soundElement() {
9070
9481
  const soundElement = this.parentElement;
@@ -9464,39 +9875,85 @@ customElements.define('pc-gsplat', GSplatComponentElement);
9464
9875
  class ModelElement extends AsyncElement {
9465
9876
  _asset = '';
9466
9877
  _entity = null;
9878
+ /**
9879
+ * Incremented on every new load and on disconnect, and captured by a load when it starts. A
9880
+ * load that resumes from an await or a load callback abandons itself if the value has moved
9881
+ * on, so a superseded load can neither instantiate a second entity nor parent one that has
9882
+ * since been destroyed.
9883
+ */
9884
+ _loadGeneration = 0;
9885
+ /**
9886
+ * The pending asset-load subscription of the current load, if it is waiting for its asset.
9887
+ * Held so that whatever supersedes the load can detach the handler from the asset, rather
9888
+ * than leave it registered until the asset loads (or forever, if it never does).
9889
+ */
9890
+ _loadHandle = null;
9891
+ /**
9892
+ * The root entity of the instantiated model. `null` until the container asset has loaded
9893
+ * and been instantiated, and again once the element has been removed from the document.
9894
+ * @returns The model's root entity, or `null`.
9895
+ */
9896
+ get entity() {
9897
+ return this._entity;
9898
+ }
9467
9899
  connectedCallback() {
9468
9900
  this._loadModel();
9469
9901
  this._onReady();
9470
9902
  }
9471
9903
  disconnectedCallback() {
9904
+ this._loadGeneration++;
9905
+ this._detachLoadHandler();
9472
9906
  this._unloadModel();
9907
+ this._resetReady();
9908
+ }
9909
+ _detachLoadHandler() {
9910
+ this._loadHandle?.off();
9911
+ this._loadHandle = null;
9473
9912
  }
9474
9913
  _instantiate(container) {
9475
- this._entity = container.instantiateRenderEntity();
9914
+ const generation = this._loadGeneration;
9915
+ const entity = container.instantiateRenderEntity();
9916
+ this._entity = entity;
9476
9917
  // @ts-ignore
9477
9918
  if (container.animations.length > 0) {
9478
- this._entity.addComponent('anim');
9919
+ entity.addComponent('anim');
9479
9920
  // @ts-ignore
9480
- this._entity.anim.assignAnimation('animation', container.animations[0].resource);
9921
+ entity.anim.assignAnimation('animation', container.animations[0].resource);
9481
9922
  }
9923
+ // The parent's readiness re-arms when it is torn down, so these can resume in a later
9924
+ // connection cycle. The entity is captured above and the generation re-checked, so a
9925
+ // stale resume cannot parent an entity a newer cycle has already destroyed.
9482
9926
  const parentEntityElement = this.closestEntity;
9483
9927
  if (parentEntityElement) {
9484
9928
  parentEntityElement.ready().then(() => {
9485
- parentEntityElement.entity.addChild(this._entity);
9929
+ if (generation !== this._loadGeneration) {
9930
+ return;
9931
+ }
9932
+ parentEntityElement.entity.addChild(entity);
9486
9933
  });
9487
9934
  }
9488
9935
  else {
9489
9936
  const appElement = this.closestApp;
9490
9937
  if (appElement) {
9491
9938
  appElement.ready().then(() => {
9492
- appElement.app.root.addChild(this._entity);
9939
+ if (generation !== this._loadGeneration) {
9940
+ return;
9941
+ }
9942
+ appElement.app.root.addChild(entity);
9493
9943
  });
9494
9944
  }
9495
9945
  }
9496
9946
  }
9497
9947
  async _loadModel() {
9498
9948
  this._unloadModel();
9949
+ // Supersede any load already in flight - only the newest load may instantiate
9950
+ const generation = ++this._loadGeneration;
9951
+ this._detachLoadHandler();
9499
9952
  const appElement = await this.closestApp?.ready();
9953
+ // The element may have been removed, or another load started, while we waited
9954
+ if (generation !== this._loadGeneration) {
9955
+ return;
9956
+ }
9500
9957
  const app = appElement?.app;
9501
9958
  const asset = AssetElement.get(this._asset);
9502
9959
  if (!asset) {
@@ -9506,7 +9963,14 @@ class ModelElement extends AsyncElement {
9506
9963
  this._instantiate(asset.resource);
9507
9964
  }
9508
9965
  else {
9509
- asset.once('load', () => {
9966
+ // The generation is re-checked even though a superseded handler is detached: the
9967
+ // detach relies on how the engine's event emitter treats removal, while the check
9968
+ // holds on its own.
9969
+ this._loadHandle = asset.once('load', () => {
9970
+ this._loadHandle = null;
9971
+ if (generation !== this._loadGeneration) {
9972
+ return;
9973
+ }
9510
9974
  this._instantiate(asset.resource);
9511
9975
  });
9512
9976
  app.assets.load(asset);
@@ -9608,10 +10072,17 @@ class SceneElement extends AsyncElement {
9608
10072
  return;
9609
10073
  }
9610
10074
  this._scene = app.scene;
9611
- this.updateSceneSettings();
10075
+ this._updateSceneSettings();
9612
10076
  this._onReady();
9613
10077
  }
9614
- updateSceneSettings() {
10078
+ disconnectedCallback() {
10079
+ // The scene belongs to the application, and removing this element - or the <pc-app>
10080
+ // above it, which disconnects first - parts the two. Re-arm readiness so a re-inserted
10081
+ // element announces the scene it acquires then, not the one it lost here.
10082
+ this._scene = null;
10083
+ this._resetReady();
10084
+ }
10085
+ _updateSceneSettings() {
9615
10086
  if (this._scene) {
9616
10087
  this._scene.fog.type = this._fog;
9617
10088
  this._scene.fog.color = this._fogColor;
@@ -9773,19 +10244,38 @@ class SkyElement extends AsyncElement {
9773
10244
  _center = new playcanvas.Vec3(0, 0.01, 0);
9774
10245
  _intensity = 1;
9775
10246
  _rotation = new playcanvas.Vec3();
9776
- _level = 0;
10247
+ _mipLevel = 0;
9777
10248
  _lighting = false;
9778
10249
  _scale = new playcanvas.Vec3(100, 100, 100);
9779
10250
  _type = 'infinite';
9780
10251
  _scene = null;
9781
10252
  _appElement = null;
10253
+ /**
10254
+ * Incremented on every new load and on disconnect, and captured by a load when it starts. A
10255
+ * load that resumes from an await or a load callback abandons itself if the value has moved
10256
+ * on, so a superseded load cannot generate a skybox for a scene it no longer configures.
10257
+ */
10258
+ _loadGeneration = 0;
10259
+ /**
10260
+ * The pending asset-load subscription of the current load, if it is waiting for its asset.
10261
+ * Held so that whatever supersedes the load can detach the handler from the asset, rather
10262
+ * than leave it registered until the asset loads (or forever, if it never does).
10263
+ */
10264
+ _loadHandle = null;
9782
10265
  connectedCallback() {
9783
10266
  this._loadSkybox();
9784
10267
  this._onReady();
9785
10268
  }
9786
10269
  disconnectedCallback() {
10270
+ this._loadGeneration++;
10271
+ this._detachLoadHandler();
9787
10272
  this._unloadSkybox();
9788
10273
  this._appElement = null;
10274
+ this._resetReady();
10275
+ }
10276
+ _detachLoadHandler() {
10277
+ this._loadHandle?.off();
10278
+ this._loadHandle = null;
9789
10279
  }
9790
10280
  _generateSkybox(asset) {
9791
10281
  if (!this._scene)
@@ -9793,10 +10283,17 @@ class SkyElement extends AsyncElement {
9793
10283
  const source = asset.resource;
9794
10284
  const skybox = playcanvas.EnvLighting.generateSkyboxCubemap(source);
9795
10285
  skybox.anisotropy = 4;
10286
+ // This element owns what it generated (see _unloadSkybox) - replacing a skybox from an
10287
+ // earlier load must release it, not orphan it on the GPU
10288
+ this._scene.skybox?.destroy();
9796
10289
  this._scene.skybox = skybox;
9797
10290
  if (this._lighting) {
9798
10291
  const lighting = playcanvas.EnvLighting.generateLightingSource(source);
9799
10292
  const envAtlas = playcanvas.EnvLighting.generateAtlas(lighting);
10293
+ // The lighting source is an intermediate: the atlas is rendered from it and it is
10294
+ // not needed afterwards
10295
+ lighting.destroy();
10296
+ this._scene.envAtlas?.destroy();
9800
10297
  this._scene.envAtlas = envAtlas;
9801
10298
  }
9802
10299
  const layer = this._scene.layers.getLayerById(playcanvas.LAYERID_SKYBOX);
@@ -9807,10 +10304,17 @@ class SkyElement extends AsyncElement {
9807
10304
  this._scene.sky.node.setLocalScale(this._scale);
9808
10305
  this._scene.sky.center = this._center;
9809
10306
  this._scene.skyboxIntensity = this._intensity;
9810
- this._scene.skyboxMip = this._level;
10307
+ this._scene.skyboxMip = this._mipLevel;
9811
10308
  }
9812
10309
  async _loadSkybox() {
10310
+ // Supersede any load already in flight - only the newest load may generate the skybox
10311
+ const generation = ++this._loadGeneration;
10312
+ this._detachLoadHandler();
9813
10313
  const appElement = await this.closestApp?.ready();
10314
+ // The element may have been removed, or another load started, while we waited
10315
+ if (generation !== this._loadGeneration) {
10316
+ return;
10317
+ }
9814
10318
  const app = appElement?.app;
9815
10319
  if (!appElement || !app) {
9816
10320
  return;
@@ -9825,7 +10329,14 @@ class SkyElement extends AsyncElement {
9825
10329
  this._generateSkybox(asset);
9826
10330
  }
9827
10331
  else {
9828
- asset.once('load', () => {
10332
+ // The generation is re-checked even though a superseded handler is detached: the
10333
+ // detach relies on how the engine's event emitter treats removal, while the check
10334
+ // holds on its own.
10335
+ this._loadHandle = asset.once('load', () => {
10336
+ this._loadHandle = null;
10337
+ if (generation !== this._loadGeneration) {
10338
+ return;
10339
+ }
9829
10340
  this._generateSkybox(asset);
9830
10341
  });
9831
10342
  app.assets.load(asset);
@@ -9899,23 +10410,6 @@ class SkyElement extends AsyncElement {
9899
10410
  get intensity() {
9900
10411
  return this._intensity;
9901
10412
  }
9902
- /**
9903
- * Sets the mip level of the skybox.
9904
- * @param value - The mip level.
9905
- */
9906
- set level(value) {
9907
- this._level = value;
9908
- if (this._scene) {
9909
- this._scene.skyboxMip = this._level;
9910
- }
9911
- }
9912
- /**
9913
- * Gets the mip level of the skybox.
9914
- * @returns The mip level.
9915
- */
9916
- get level() {
9917
- return this._level;
9918
- }
9919
10413
  /**
9920
10414
  * Sets whether the skybox is used as a light source.
9921
10415
  * @param value - Whether to use lighting.
@@ -9930,6 +10424,24 @@ class SkyElement extends AsyncElement {
9930
10424
  get lighting() {
9931
10425
  return this._lighting;
9932
10426
  }
10427
+ /**
10428
+ * Sets the mip level of the skybox, where 0 is the sharpest. Raising it selects a blurrier mip,
10429
+ * which is how a skybox is softened without blurring the texture itself.
10430
+ * @param value - The mip level.
10431
+ */
10432
+ set mipLevel(value) {
10433
+ this._mipLevel = value;
10434
+ if (this._scene) {
10435
+ this._scene.skyboxMip = this._mipLevel;
10436
+ }
10437
+ }
10438
+ /**
10439
+ * Gets the mip level of the skybox.
10440
+ * @returns The mip level.
10441
+ */
10442
+ get mipLevel() {
10443
+ return this._mipLevel;
10444
+ }
9933
10445
  /**
9934
10446
  * Sets the Euler rotation of the skybox.
9935
10447
  * @param value - The rotation.
@@ -9986,7 +10498,7 @@ class SkyElement extends AsyncElement {
9986
10498
  return this._type;
9987
10499
  }
9988
10500
  static get observedAttributes() {
9989
- return ['asset', 'center', 'intensity', 'level', 'lighting', 'rotation', 'scale', 'type'];
10501
+ return ['asset', 'center', 'intensity', 'lighting', 'mip-level', 'rotation', 'scale', 'type'];
9990
10502
  }
9991
10503
  attributeChangedCallback(name, _oldValue, newValue) {
9992
10504
  switch (name) {
@@ -9999,12 +10511,12 @@ class SkyElement extends AsyncElement {
9999
10511
  case 'intensity':
10000
10512
  this.intensity = parseNumber(newValue, 1, name);
10001
10513
  break;
10002
- case 'level':
10003
- this.level = parseNumber(newValue, 0, name);
10004
- break;
10005
10514
  case 'lighting':
10006
10515
  this.lighting = parseBool(newValue, false);
10007
10516
  break;
10517
+ case 'mip-level':
10518
+ this.mipLevel = parseNumber(newValue, 0, name);
10519
+ break;
10008
10520
  case 'rotation':
10009
10521
  this.rotation = parseVec3(newValue, playcanvas.Vec3.ZERO, name);
10010
10522
  break;