@playcanvas/web-components 0.11.0 → 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 +18 -38
  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 +3 -7
  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 +3 -7
  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 +3 -7
  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 +5049 -10696
  26. package/dist/entity.d.ts +5 -11
  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 +2 -1
  32. package/dist/pwc.cjs +496 -136
  33. package/dist/pwc.cjs.map +1 -1
  34. package/dist/pwc.js +496 -136
  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 +496 -136
  41. package/dist/pwc.mjs.map +1 -1
  42. package/dist/scene.d.ts +4 -7
  43. package/dist/sky.d.ts +13 -5
  44. package/dist/vscode.html-custom-data.json +26 -26
  45. package/dist/web-types.json +545 -536
  46. package/package.json +8 -7
  47. package/src/app.ts +142 -70
  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 +24 -10
  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 +5 -9
  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 +16 -11
  68. package/src/components/sound-component.ts +10 -15
  69. package/src/components/sound-slot.ts +30 -20
  70. package/src/entity.ts +42 -19
  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 +62 -17
  77. package/src/scene.ts +12 -9
  78. package/src/sky.ts +50 -9
package/dist/pwc.js CHANGED
@@ -7,12 +7,14 @@
7
7
  /**
8
8
  * Base class for all PlayCanvas Web Components that initialize asynchronously.
9
9
  *
10
- * @fires {CustomEvent} ready - Fired once the element is fully initialized. Bubbles and is
11
- * composed.
10
+ * @fires {CustomEvent} ready - Fired when the element is fully initialized once per readiness
11
+ * cycle, so an element that is torn down and re-initialized (for example by removing and
12
+ * re-inserting it) fires it again. Bubbles and is composed.
12
13
  */
13
14
  class AsyncElement extends HTMLElement {
14
15
  _readyPromise;
15
16
  _readyResolve;
17
+ _readyResolved = false;
16
18
  /** @ignore */
17
19
  constructor() {
18
20
  super();
@@ -39,15 +41,39 @@
39
41
  /**
40
42
  * Called when the element is fully initialized and ready. Subclasses should call this when
41
43
  * they're ready. Resolves the ready promise and dispatches a bubbling, composed `ready`
42
- * event.
44
+ * event. Signals at most once per readiness cycle: a repeat call before {@link _resetReady}
45
+ * has re-armed the promise does nothing.
43
46
  */
44
47
  _onReady() {
48
+ if (this._readyResolved)
49
+ return;
50
+ this._readyResolved = true;
45
51
  this._readyResolve();
46
52
  this.dispatchEvent(new CustomEvent('ready', { bubbles: true, composed: true }));
47
53
  }
54
+ /**
55
+ * Returns the ready promise to its pending state. Subclasses should call this when the
56
+ * resource their readiness announced is torn down (typically from `disconnectedCallback`),
57
+ * so that a later re-initialization can signal readiness again. Does nothing while the
58
+ * promise is still pending — an in-flight waiter carries over to the next readiness cycle
59
+ * rather than being stranded on a promise nothing will ever resolve.
60
+ */
61
+ _resetReady() {
62
+ if (!this._readyResolved)
63
+ return;
64
+ this._readyResolved = false;
65
+ this._readyPromise = new Promise((resolve) => {
66
+ this._readyResolve = resolve;
67
+ });
68
+ }
48
69
  /**
49
70
  * Returns a promise that resolves with this element when it's ready. This is the low-level
50
71
  * primitive underlying {@link whenReady}, which is the recommended way to wait for elements.
72
+ *
73
+ * Readiness tracks the element's current lifecycle: once a ready element is torn down (for
74
+ * example by removing it from the document), this returns a fresh promise that resolves when
75
+ * the element is next ready. A promise obtained earlier stays resolved — call this again
76
+ * after re-inserting an element rather than reusing a promise from before its removal.
51
77
  * @returns A promise that resolves with this element when it's ready.
52
78
  */
53
79
  ready() {
@@ -122,7 +148,14 @@
122
148
  });
123
149
  }
124
150
  }
125
- getLoadPromise() {
151
+ /**
152
+ * Returns the promise that settles when the module has loaded. Awaited by the containing
153
+ * `<pc-app>` element before it creates its graphics device.
154
+ *
155
+ * @returns The load promise.
156
+ * @internal
157
+ */
158
+ _getLoadPromise() {
126
159
  return this.loadPromise;
127
160
  }
128
161
  }
@@ -181,10 +214,7 @@
181
214
  // aria-valuenow is set, which is what marks a progressbar indeterminate. jsdom has no Web
182
215
  // Animations API, so the guard degrades to a static bar there rather than crashing boot.
183
216
  if (typeof this._fill.animate === 'function') {
184
- this._sweep = this._fill.animate([
185
- { transform: 'scaleX(0.25) translateX(-100%)' },
186
- { transform: 'scaleX(0.25) translateX(500%)' }
187
- ], {
217
+ this._sweep = this._fill.animate([{ transform: 'scaleX(0.25) translateX(-100%)' }, { transform: 'scaleX(0.25) translateX(500%)' }], {
188
218
  duration: 1000,
189
219
  iterations: Infinity,
190
220
  easing: 'ease-in-out'
@@ -417,7 +447,7 @@
417
447
  */
418
448
  const parseComponents = (value, count) => {
419
449
  const components = value.trim().split(/\s+/).map(Number);
420
- if (components.length !== count || components.some(component => !Number.isFinite(component))) {
450
+ if (components.length !== count || components.some((component) => !Number.isFinite(component))) {
421
451
  return null;
422
452
  }
423
453
  return components;
@@ -473,7 +503,10 @@
473
503
  if (/^#(?:[0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(value)) {
474
504
  let hex = value.slice(1);
475
505
  if (hex.length === 3 || hex.length === 4) {
476
- hex = hex.split('').map(char => char + char).join('');
506
+ hex = hex
507
+ .split('')
508
+ .map((char) => char + char)
509
+ .join('');
477
510
  }
478
511
  return new playcanvas.Color().fromString(`#${hex}`);
479
512
  }
@@ -570,7 +603,10 @@
570
603
  // caller's default, or a later mutation would write back through it.
571
604
  return [...defaultValue];
572
605
  }
573
- return value.split(',').map(tag => tag.trim()).filter(tag => tag !== '');
606
+ return value
607
+ .split(',')
608
+ .map((tag) => tag.trim())
609
+ .filter((tag) => tag !== '');
574
610
  };
575
611
  /**
576
612
  * Parse a Vec2 attribute value. The expected format is 2 space-separated numbers (e.g. '1 2').
@@ -662,6 +698,8 @@
662
698
  return element?.entity ?? null;
663
699
  };
664
700
 
701
+ /** The pointer event types the application synthesizes on `<pc-entity>` elements via picking. */
702
+ const pointerEventTypes = ['pointermove', 'pointerdown', 'pointerup', 'pointerenter', 'pointerleave'];
665
703
  /**
666
704
  * The AppElement interface provides properties and methods for manipulating
667
705
  * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-app/ | `<pc-app>`} elements.
@@ -692,7 +730,20 @@
692
730
  */
693
731
  _optionsLocked = false;
694
732
  _bar = null;
733
+ /**
734
+ * Whether the application has created its initial entity hierarchy. Read by EntityElement to
735
+ * decide whether a newly connected element must create its entity itself or leave it to the
736
+ * boot sweep.
737
+ * @internal
738
+ */
695
739
  _hierarchyReady = false;
740
+ /**
741
+ * Incremented on every connect and disconnect. Boot captures the value on entry and abandons
742
+ * itself wherever it resumes from an await if the value has moved on — so a boot whose
743
+ * element was removed cannot complete against a torn-down element, and a boot whose element
744
+ * was removed and re-inserted (which starts a boot of its own) cannot race the newer one.
745
+ */
746
+ _bootGeneration = 0;
696
747
  /**
697
748
  * The elements backing this application's entities, keyed by the entity itself. Registered
698
749
  * by EntityElement at creation and removed when an entity is destroyed, this joins engine
@@ -745,8 +796,16 @@
745
796
  super();
746
797
  // Bind methods to maintain 'this' context
747
798
  this._onWindowResize = this._onWindowResize.bind(this);
799
+ // Track pointer listeners being added to and removed from descendant entities.
800
+ // Registered once here rather than on every boot - the handlers no-op while there is no
801
+ // canvas, and a re-booted element must not stack a second set.
802
+ pointerEventTypes.forEach((type) => {
803
+ this.addEventListener(`${type}:connect`, () => this._onPointerListenerAdded(type));
804
+ this.addEventListener(`${type}:disconnect`, () => this._onPointerListenerRemoved(type));
805
+ });
748
806
  }
749
807
  async connectedCallback() {
808
+ const generation = ++this._bootGeneration;
750
809
  // Created before the first await, so the bar is visible while modules and the graphics
751
810
  // device are created, and exists before any disconnect could need to clean it up
752
811
  if (this._loadingBar && !this._bar) {
@@ -755,7 +814,12 @@
755
814
  // Get all pc-module elements that are direct children of the pc-app element
756
815
  const moduleElements = this.querySelectorAll(':scope > pc-module');
757
816
  // Wait for all modules to load
758
- await Promise.all(Array.from(moduleElements).map(module => module.getLoadPromise()));
817
+ await Promise.all(Array.from(moduleElements).map((module) => module._getLoadPromise()));
818
+ // The element may have been removed while the modules loaded. Nothing beyond the loading
819
+ // bar exists yet, and disconnectedCallback has already destroyed that.
820
+ if (generation !== this._bootGeneration) {
821
+ return;
822
+ }
759
823
  // Create and append the canvas to the element
760
824
  this._canvas = document.createElement('canvas');
761
825
  this.appendChild(this._canvas);
@@ -775,6 +839,13 @@
775
839
  deviceTypes: deviceTypes,
776
840
  stencil: this._stencilBuffer
777
841
  });
842
+ // The element may have been removed while the device was created. disconnectedCallback
843
+ // has already cleaned up the canvas; the device was created inside the await, so it is
844
+ // this boot's to release.
845
+ if (generation !== this._bootGeneration) {
846
+ device.destroy();
847
+ return;
848
+ }
778
849
  // Assigned rather than resolved to a number here: the engine caps against the live
779
850
  // window.devicePixelRatio on every resize, so an uncapped Infinity keeps following the
780
851
  // display when a window moves between monitors of differing density.
@@ -851,27 +922,41 @@
851
922
  this._pickerCreate();
852
923
  // Get all pc-asset elements that are direct children of the pc-app element
853
924
  const assetElements = this.querySelectorAll(':scope > pc-asset');
854
- Array.from(assetElements).forEach((assetElement) => {
855
- assetElement.createAsset();
925
+ for (const assetElement of Array.from(assetElements)) {
926
+ assetElement._createAsset();
856
927
  const asset = assetElement.asset;
857
928
  if (asset) {
858
929
  app.assets.add(asset);
930
+ // Adding a fileless asset (one built purely from data, such as a sprite)
931
+ // completes it synchronously, dispatching the element's load event - whose
932
+ // listeners may have removed this element. Stop before the next addition
933
+ // reaches the destroyed registry, and before orphan entities are created.
934
+ if (generation !== this._bootGeneration) {
935
+ return;
936
+ }
859
937
  }
860
- });
938
+ }
861
939
  // Get all pc-material elements that are direct children of the pc-app element
862
940
  const materialElements = this.querySelectorAll(':scope > pc-material');
863
941
  Array.from(materialElements).forEach((materialElement) => {
864
- materialElement.createMaterial();
942
+ materialElement._createMaterial();
865
943
  });
866
944
  // Create all entities
867
945
  const entityElements = this.querySelectorAll('pc-entity');
868
946
  Array.from(entityElements).forEach((entityElement) => {
869
- entityElement.createEntity(app);
947
+ entityElement._createEntity(app);
870
948
  });
871
949
  // Build hierarchy
872
950
  entityElements.forEach((entityElement) => {
873
- entityElement.buildHierarchy(app);
951
+ entityElement._buildHierarchy(app);
874
952
  });
953
+ // Building the hierarchy dispatched each entity's ready event synchronously, and a
954
+ // listener may have removed the element. The sweep itself degrades safely - destroying
955
+ // the application nulls every element's entity, so the remaining builds no-op - but the
956
+ // teardown's reset must not be overwritten here.
957
+ if (generation !== this._bootGeneration) {
958
+ return;
959
+ }
875
960
  this._hierarchyReady = true;
876
961
  // Forward the engine's preload lifecycle as DOM ProgressEvents on this element. The
877
962
  // listener must be attached before preload() is called: an asset that is already loaded
@@ -888,8 +973,19 @@
888
973
  this._loadProgress = total === 0 ? 1 : 0;
889
974
  this._bar?.progress(0, total);
890
975
  this.dispatchEvent(new ProgressEvent('progress', { lengthComputable: true, loaded: 0, total }));
976
+ // The progress dispatch above ran listeners synchronously, and one may have removed the
977
+ // element. The application is already destroyed - it must not be asked to preload.
978
+ if (generation !== this._bootGeneration) {
979
+ return;
980
+ }
891
981
  // Load assets before starting the application
892
982
  app.preload(() => {
983
+ // The element may have been removed while assets loaded. The application is already
984
+ // destroyed, so it must not be started — and readiness must not be signaled for a
985
+ // boot that no longer owns the element.
986
+ if (generation !== this._bootGeneration) {
987
+ return;
988
+ }
893
989
  // Scope the counter to this preload pass, so a later app.preload() call by user code
894
990
  // cannot push `loaded` past `total`
895
991
  app.off('preload:progress', onPreloadProgress);
@@ -905,6 +1001,9 @@
905
1001
  });
906
1002
  }
907
1003
  disconnectedCallback() {
1004
+ // Invalidate any boot still in flight, so it abandons itself when it next resumes
1005
+ // instead of completing against a torn-down element.
1006
+ this._bootGeneration++;
908
1007
  this._optionsLocked = false;
909
1008
  this._pickerDestroy();
910
1009
  // Clean up the application. Destroying it destroys every entity, whose destroy hooks
@@ -917,6 +1016,11 @@
917
1016
  this._loadProgress = 0;
918
1017
  this._bar?.destroy();
919
1018
  this._bar = null;
1019
+ // Return the element to its pre-boot state, so re-inserting it boots afresh: descendants
1020
+ // must neither see a hierarchy that no longer exists nor resume against a readiness that
1021
+ // no longer holds.
1022
+ this._hierarchyReady = false;
1023
+ this._resetReady();
920
1024
  // Remove event listeners
921
1025
  window.removeEventListener('resize', this._onWindowResize);
922
1026
  // Remove the canvas
@@ -944,14 +1048,11 @@
944
1048
  this._pointerHandlers.pointermove = listener(this._onPointerMove);
945
1049
  this._pointerHandlers.pointerdown = listener(this._onPointerDown);
946
1050
  this._pointerHandlers.pointerup = listener(this._onPointerUp);
947
- // Listen for pointer listeners being added/removed
948
- ['pointermove', 'pointerdown', 'pointerup', 'pointerenter', 'pointerleave'].forEach((type) => {
949
- this.addEventListener(`${type}:connect`, () => this._onPointerListenerAdded(type));
950
- this.addEventListener(`${type}:disconnect`, () => this._onPointerListenerRemoved(type));
951
- // Attach canvas handlers for listeners registered before this point (e.g. handlers
952
- // created from onpointer* attributes when their elements were first upgraded)
953
- const anyListeners = Array.from(this.querySelectorAll('pc-entity'))
954
- .some(entity => entity.hasListeners(type));
1051
+ // Attach canvas handlers for listeners registered before this boot (e.g. handlers
1052
+ // created from onpointer* attributes when their elements were first upgraded, or
1053
+ // listeners carried over from before a re-boot)
1054
+ pointerEventTypes.forEach((type) => {
1055
+ const anyListeners = Array.from(this.querySelectorAll('pc-entity')).some((entity) => entity._hasListeners(type));
955
1056
  if (anyListeners) {
956
1057
  this._onPointerListenerAdded(type);
957
1058
  }
@@ -966,6 +1067,7 @@
966
1067
  });
967
1068
  }
968
1069
  this._picker = null;
1070
+ this._hoveredEntity = null;
969
1071
  this._pointerHandlers = {
970
1072
  pointermove: null,
971
1073
  pointerdown: null,
@@ -985,7 +1087,7 @@
985
1087
  *
986
1088
  * @param entity - The entity.
987
1089
  * @param element - The element that created it.
988
- * @ignore
1090
+ * @internal
989
1091
  */
990
1092
  _registerEntityElement(entity, element) {
991
1093
  this._entityElements.set(entity, element);
@@ -994,7 +1096,7 @@
994
1096
  * Removes the registration for a destroyed entity. Called by EntityElement.
995
1097
  *
996
1098
  * @param entity - The entity.
997
- * @ignore
1099
+ * @internal
998
1100
  */
999
1101
  _unregisterEntityElement(entity) {
1000
1102
  this._entityElements.delete(entity);
@@ -1040,7 +1142,7 @@
1040
1142
  _elementWithListener(node, type) {
1041
1143
  while (node !== null) {
1042
1144
  const element = this._entityElements.get(node);
1043
- if (element?.hasListeners(type)) {
1145
+ if (element?._hasListeners(type)) {
1044
1146
  return element;
1045
1147
  }
1046
1148
  node = node.parent;
@@ -1097,17 +1199,17 @@
1097
1199
  const newHoverEntity = this._elementFromNode(node);
1098
1200
  // Handle enter/leave events
1099
1201
  if (this._hoveredEntity !== newHoverEntity) {
1100
- if (this._hoveredEntity && this._hoveredEntity.hasListeners('pointerleave')) {
1202
+ if (this._hoveredEntity && this._hoveredEntity._hasListeners('pointerleave')) {
1101
1203
  this._hoveredEntity.dispatchEvent(new PointerEvent('pointerleave', event));
1102
1204
  }
1103
- if (newHoverEntity && newHoverEntity.hasListeners('pointerenter')) {
1205
+ if (newHoverEntity && newHoverEntity._hasListeners('pointerenter')) {
1104
1206
  newHoverEntity.dispatchEvent(new PointerEvent('pointerenter', event));
1105
1207
  }
1106
1208
  }
1107
1209
  // Update hover state
1108
1210
  this._hoveredEntity = newHoverEntity;
1109
1211
  // Handle pointermove event
1110
- if (newHoverEntity && newHoverEntity.hasListeners('pointermove')) {
1212
+ if (newHoverEntity && newHoverEntity._hasListeners('pointermove')) {
1111
1213
  newHoverEntity.dispatchEvent(new PointerEvent('pointermove', event));
1112
1214
  }
1113
1215
  }
@@ -1137,22 +1239,21 @@
1137
1239
  if (!this._hasPointerListeners[type] && this._canvas) {
1138
1240
  this._hasPointerListeners[type] = true;
1139
1241
  // For enter/leave events, we need the move handler
1140
- const handler = (type === 'pointerenter' || type === 'pointerleave') ?
1141
- this._pointerHandlers.pointermove :
1142
- this._pointerHandlers[type];
1242
+ const handler = type === 'pointerenter' || type === 'pointerleave'
1243
+ ? this._pointerHandlers.pointermove
1244
+ : this._pointerHandlers[type];
1143
1245
  if (handler) {
1144
1246
  this._canvas.addEventListener(type === 'pointerenter' || type === 'pointerleave' ? 'pointermove' : type, handler);
1145
1247
  }
1146
1248
  }
1147
1249
  }
1148
1250
  _onPointerListenerRemoved(type) {
1149
- const hasListeners = Array.from(this.querySelectorAll('pc-entity'))
1150
- .some(entity => entity.hasListeners(type));
1251
+ const hasListeners = Array.from(this.querySelectorAll('pc-entity')).some((entity) => entity._hasListeners(type));
1151
1252
  if (!hasListeners && this._canvas) {
1152
1253
  this._hasPointerListeners[type] = false;
1153
- const handler = (type === 'pointerenter' || type === 'pointerleave') ?
1154
- this._pointerHandlers.pointermove :
1155
- this._pointerHandlers[type];
1254
+ const handler = type === 'pointerenter' || type === 'pointerleave'
1255
+ ? this._pointerHandlers.pointermove
1256
+ : this._pointerHandlers[type];
1156
1257
  if (handler) {
1157
1258
  this._canvas.removeEventListener(type === 'pointerenter' || type === 'pointerleave' ? 'pointermove' : type, handler);
1158
1259
  }
@@ -1233,14 +1334,6 @@
1233
1334
  get depthBuffer() {
1234
1335
  return this._depthBuffer;
1235
1336
  }
1236
- /**
1237
- * Gets the hierarchy ready flag.
1238
- * @returns The hierarchy ready flag.
1239
- * @ignore
1240
- */
1241
- get hierarchyReady() {
1242
- return this._hierarchyReady;
1243
- }
1244
1337
  /**
1245
1338
  * Sets whether the application shows its built-in loading bar while it boots and preloads its
1246
1339
  * assets. Enabled by default; setting `false` removes the bar immediately, while setting
@@ -1409,7 +1502,14 @@
1409
1502
  get entity() {
1410
1503
  return this._entity;
1411
1504
  }
1412
- createEntity(app) {
1505
+ /**
1506
+ * Creates the backing entity. Called by the containing `<pc-app>` element during its boot
1507
+ * sweep, and on connection for elements inserted while the application is already running.
1508
+ *
1509
+ * @param app - The application to create the entity in.
1510
+ * @internal
1511
+ */
1512
+ _createEntity(app) {
1413
1513
  // Guard against double creation. When a subtree is inserted at runtime (e.g. cloning a
1414
1514
  // `<template>`), an ancestor's connectedCallback eagerly creates descendant entities; the
1415
1515
  // descendants' own connectedCallbacks would otherwise create them a second time.
@@ -1440,8 +1540,10 @@
1440
1540
  }
1441
1541
  /**
1442
1542
  * Handles the destruction of the backing entity. Resets the element so a later re-insertion
1443
- * starts clean: `_built` must be cleared alongside `_entity`, or buildHierarchy would bail
1444
- * and a re-created entity would never be parented.
1543
+ * starts clean: `_built` must be cleared alongside `_entity`, or _buildHierarchy would bail
1544
+ * and a re-created entity would never be parented. Readiness is re-armed for the same
1545
+ * reason — with the entity gone, a resolved ready promise would resume its awaiters against
1546
+ * a null `entity`.
1445
1547
  *
1446
1548
  * @param entity - The entity that was destroyed.
1447
1549
  */
@@ -1450,8 +1552,18 @@
1450
1552
  this._appElement = null;
1451
1553
  this._entity = null;
1452
1554
  this._built = false;
1555
+ this._resetReady();
1453
1556
  }
1454
- buildHierarchy(app) {
1557
+ /**
1558
+ * Parents the backing entity: under the entity of the nearest ancestor `<pc-entity>` when
1559
+ * there is one, and under the application root otherwise. Called by the containing `<pc-app>`
1560
+ * element once a sweep has created every entity, so a parent's existence never depends on
1561
+ * document order.
1562
+ *
1563
+ * @param app - The application whose root adopts parentless entities.
1564
+ * @internal
1565
+ */
1566
+ _buildHierarchy(app) {
1455
1567
  if (!this.entity || this._built)
1456
1568
  return;
1457
1569
  this._built = true;
@@ -1477,17 +1589,17 @@
1477
1589
  return;
1478
1590
  }
1479
1591
  // If app is already running, create entity immediately
1480
- if (closestApp.hierarchyReady) {
1592
+ if (closestApp._hierarchyReady) {
1481
1593
  const app = closestApp.app;
1482
- this.createEntity(app);
1483
- this.buildHierarchy(app);
1594
+ this._createEntity(app);
1595
+ this._buildHierarchy(app);
1484
1596
  // Handle any child entities that might exist
1485
1597
  const childEntities = this.querySelectorAll('pc-entity');
1486
1598
  childEntities.forEach((child) => {
1487
- child.createEntity(app);
1599
+ child._createEntity(app);
1488
1600
  });
1489
1601
  childEntities.forEach((child) => {
1490
- child.buildHierarchy(app);
1602
+ child._buildHierarchy(app);
1491
1603
  });
1492
1604
  }
1493
1605
  }
@@ -1681,14 +1793,23 @@
1681
1793
  }
1682
1794
  removeEventListener(type, listener, options) {
1683
1795
  if (this._listeners[type]) {
1684
- this._listeners[type] = this._listeners[type].filter(l => l !== listener);
1796
+ this._listeners[type] = this._listeners[type].filter((l) => l !== listener);
1685
1797
  }
1686
1798
  super.removeEventListener(type, listener, options);
1687
1799
  if (type.startsWith('pointer')) {
1688
1800
  this.dispatchEvent(new CustomEvent(`${type}:disconnect`, { bubbles: true }));
1689
1801
  }
1690
1802
  }
1691
- hasListeners(type) {
1803
+ /**
1804
+ * Whether the element has a listener for an event type, registered either with
1805
+ * {@link addEventListener} or with the matching inline `onpointer*` attribute. Read by the
1806
+ * containing `<pc-app>` element to gate pointer event synthesis.
1807
+ *
1808
+ * @param type - The event type.
1809
+ * @returns Whether a listener is registered.
1810
+ * @internal
1811
+ */
1812
+ _hasListeners(type) {
1692
1813
  return Boolean(this._listeners[type]?.length) || this._inlineHandlerTypes.has(type);
1693
1814
  }
1694
1815
  }
@@ -1979,7 +2100,7 @@
1979
2100
  const app = appElement.app;
1980
2101
  if (!app)
1981
2102
  return; // pc-app is re-connecting; its own boot will create this asset
1982
- this.createAsset();
2103
+ this._createAsset();
1983
2104
  if (this.asset) {
1984
2105
  app.assets.add(this.asset); // add() auto-loads when preload is true
1985
2106
  if (!this.lazy) {
@@ -1987,13 +2108,15 @@
1987
2108
  }
1988
2109
  }
1989
2110
  }
1990
- // Never ready if createAsset failed (unsupported asset type)
2111
+ // Never ready if _createAsset failed (unsupported asset type)
1991
2112
  if (this.asset) {
1992
2113
  this._onReady();
1993
2114
  }
1994
2115
  }
1995
2116
  disconnectedCallback() {
1996
- this.destroyAsset();
2117
+ this._destroyAsset();
2118
+ // Re-arm readiness so a re-inserted element announces the asset it creates then
2119
+ this._resetReady();
1997
2120
  }
1998
2121
  _onAssetLoad() {
1999
2122
  this.dispatchEvent(new Event('load'));
@@ -2003,7 +2126,14 @@
2003
2126
  message: err instanceof Error ? err.message : String(err)
2004
2127
  }));
2005
2128
  }
2006
- createAsset() {
2129
+ /**
2130
+ * Creates the asset from the element's attributes. Called by the containing `<pc-app>`
2131
+ * element during its boot sweep, and on connection for elements inserted while the
2132
+ * application is already running.
2133
+ *
2134
+ * @internal
2135
+ */
2136
+ _createAsset() {
2007
2137
  const id = this.getAttribute('id') || '';
2008
2138
  const src = this.getAttribute('src') || '';
2009
2139
  let type = this.getAttribute('type');
@@ -2093,7 +2223,7 @@
2093
2223
  }
2094
2224
  return data;
2095
2225
  }
2096
- destroyAsset() {
2226
+ _destroyAsset() {
2097
2227
  if (this.asset) {
2098
2228
  // A caller that keeps the Asset alive must not dispatch on a removed element
2099
2229
  this.asset.off('load', this._onAssetLoad, this);
@@ -2121,6 +2251,13 @@
2121
2251
  get lazy() {
2122
2252
  return this._lazy;
2123
2253
  }
2254
+ /**
2255
+ * Returns the {@link Asset} created by the `<pc-asset>` element with the given `id`, or
2256
+ * `undefined` if there is no such element or its asset has not been created yet.
2257
+ *
2258
+ * @param id - The `id` of the `<pc-asset>` element.
2259
+ * @returns The asset, or `undefined`.
2260
+ */
2124
2261
  static get(id) {
2125
2262
  const assetElement = document.querySelector(`pc-asset[id="${id}"]`);
2126
2263
  return assetElement?.asset;
@@ -2146,6 +2283,14 @@
2146
2283
  _enabled = true;
2147
2284
  _component = null;
2148
2285
  _appElement = null;
2286
+ /**
2287
+ * Incremented on every connect and disconnect. connectedCallback captures the value on entry
2288
+ * and abandons itself wherever it resumes from an await if the value has moved on — so a
2289
+ * callback whose element was removed cannot act on a torn-down tree, and one whose element
2290
+ * was removed and re-inserted (which runs a callback of its own) cannot add the component a
2291
+ * second time.
2292
+ */
2293
+ _connectionGeneration = 0;
2149
2294
  /**
2150
2295
  * Creates a new ComponentElement instance.
2151
2296
  *
@@ -2156,11 +2301,17 @@
2156
2301
  super();
2157
2302
  this._componentName = componentName;
2158
2303
  }
2159
- // Method to be overridden by subclasses to provide initial component data
2304
+ /**
2305
+ * Returns the data the component is created with. Overridden by subclasses to supply the
2306
+ * initial values of their cached properties.
2307
+ *
2308
+ * @returns The initial component data.
2309
+ */
2160
2310
  getInitialComponentData() {
2161
2311
  return {};
2162
2312
  }
2163
- async addComponent() {
2313
+ async _addComponent() {
2314
+ const generation = this._connectionGeneration;
2164
2315
  const entityElement = this.closestEntity;
2165
2316
  if (!entityElement) {
2166
2317
  // A component can only exist on an entity, so an element placed outside one is inert.
@@ -2170,19 +2321,42 @@
2170
2321
  return;
2171
2322
  }
2172
2323
  await entityElement.ready();
2324
+ // The element may have been removed, or removed and re-inserted, while the entity became
2325
+ // ready — the component belongs to the connection that owns the current generation.
2326
+ if (generation !== this._connectionGeneration) {
2327
+ return;
2328
+ }
2173
2329
  // Add the component to the entity
2174
2330
  const data = this.getInitialComponentData();
2175
2331
  this._component = entityElement.entity.addComponent(this._componentName, data);
2176
2332
  }
2177
- initComponent() { }
2333
+ /**
2334
+ * Configures the newly added component. Overridden by subclasses whose setup goes beyond
2335
+ * the initial data — child-element handling, asset resolution and the like.
2336
+ */
2337
+ initComponent() {
2338
+ // optional hook
2339
+ }
2178
2340
  async connectedCallback() {
2341
+ const generation = ++this._connectionGeneration;
2179
2342
  this._appElement = this.closestApp ?? null;
2180
2343
  await this._appElement?.ready();
2181
- await this.addComponent();
2344
+ // The element may have been removed, or removed and re-inserted, while the application
2345
+ // became ready. A re-insertion runs a connectedCallback of its own, so a stale resume
2346
+ // must not add the component alongside it.
2347
+ if (generation !== this._connectionGeneration) {
2348
+ return;
2349
+ }
2350
+ await this._addComponent();
2351
+ if (generation !== this._connectionGeneration) {
2352
+ return;
2353
+ }
2182
2354
  this.initComponent();
2183
2355
  this._onReady();
2184
2356
  }
2185
2357
  disconnectedCallback() {
2358
+ // Invalidate any connectedCallback still suspended on an await
2359
+ this._connectionGeneration++;
2186
2360
  // Remove the component when the element is disconnected. Skip this when the owning
2187
2361
  // application has already been destroyed — removing a <pc-app> disconnects it before
2188
2362
  // its children, taking the component systems with it.
@@ -2191,6 +2365,7 @@
2191
2365
  }
2192
2366
  this._component = null;
2193
2367
  this._appElement = null;
2368
+ this._resetReady();
2194
2369
  }
2195
2370
  /**
2196
2371
  * The PlayCanvas component instance. `null` until the element is ready, and also for an
@@ -3248,7 +3423,17 @@
3248
3423
  return this._type;
3249
3424
  }
3250
3425
  static get observedAttributes() {
3251
- return [...super.observedAttributes, 'angular-offset', 'axis', 'convex-hull', 'half-extents', 'height', 'linear-offset', 'radius', 'type'];
3426
+ return [
3427
+ ...super.observedAttributes,
3428
+ 'angular-offset',
3429
+ 'axis',
3430
+ 'convex-hull',
3431
+ 'half-extents',
3432
+ 'height',
3433
+ 'linear-offset',
3434
+ 'radius',
3435
+ 'type'
3436
+ ];
3252
3437
  }
3253
3438
  attributeChangedCallback(name, _oldValue, newValue) {
3254
3439
  super.attributeChangedCallback(name, _oldValue, newValue);
@@ -4953,7 +5138,7 @@
4953
5138
  }
4954
5139
  // Set all the config properties on the component
4955
5140
  for (const key in resource) {
4956
- if (resource.hasOwnProperty(key)) {
5141
+ if (Object.hasOwn(resource, key)) {
4957
5142
  this.component[key] = resource[key];
4958
5143
  }
4959
5144
  }
@@ -5026,10 +5211,7 @@
5026
5211
  }
5027
5212
  }
5028
5213
  static get observedAttributes() {
5029
- return [
5030
- ...super.observedAttributes,
5031
- 'asset'
5032
- ];
5214
+ return [...super.observedAttributes, 'asset'];
5033
5215
  }
5034
5216
  attributeChangedCallback(name, _oldValue, newValue) {
5035
5217
  super.attributeChangedCallback(name, _oldValue, newValue);
@@ -5189,7 +5371,7 @@
5189
5371
  _twoSidedLighting = false;
5190
5372
  _useFog = true;
5191
5373
  _useLighting = true;
5192
- // Diverges from the engine default of false - see the class docblock and createMaterial()
5374
+ // Diverges from the engine default of false - see the class docblock and _createMaterial()
5193
5375
  _useMetalness = true;
5194
5376
  _useMetalnessSpecularColor = false;
5195
5377
  _useSkybox = true;
@@ -5202,6 +5384,10 @@
5202
5384
  _mapHandles = new Map();
5203
5385
  _updateScheduled = false;
5204
5386
  _glossConflictWarned = false;
5387
+ /**
5388
+ * The material. `null` until the containing application has created it — an element present
5389
+ * at startup has its material once the application is ready.
5390
+ */
5205
5391
  material = null;
5206
5392
  async connectedCallback() {
5207
5393
  const appElement = this.parentElement?.closest('pc-app') ?? null;
@@ -5219,10 +5405,17 @@
5219
5405
  if (!this.material) {
5220
5406
  if (!appElement.app)
5221
5407
  return; // pc-app is re-connecting; its own boot will create this
5222
- this.createMaterial();
5408
+ this._createMaterial();
5223
5409
  }
5224
5410
  }
5225
- createMaterial() {
5411
+ /**
5412
+ * Creates the material from the element's cached properties. Called by the containing
5413
+ * `<pc-app>` element during its boot sweep, and on connection for elements inserted while
5414
+ * the application is already running.
5415
+ *
5416
+ * @internal
5417
+ */
5418
+ _createMaterial() {
5226
5419
  const material = new playcanvas.StandardMaterial();
5227
5420
  this.material = material;
5228
5421
  material.alphaTest = this._alphaTest;
@@ -5349,9 +5542,9 @@
5349
5542
  * warning latches and reports once per episode, clearing when the clash is resolved.
5350
5543
  */
5351
5544
  _warnGlossConflict() {
5352
- const quote = (names) => `'${names.join('\', \'')}'`;
5353
- const roughness = roughnessAliases.filter(name => this.hasAttribute(name));
5354
- const gloss = glossConflicts.filter(name => this.hasAttribute(name));
5545
+ const quote = (names) => `'${names.join("', '")}'`;
5546
+ const roughness = roughnessAliases.filter((name) => this.hasAttribute(name));
5547
+ const gloss = glossConflicts.filter((name) => this.hasAttribute(name));
5355
5548
  if (roughness.length === 0 || gloss.length === 0) {
5356
5549
  this._glossConflictWarned = false;
5357
5550
  return;
@@ -5369,7 +5562,7 @@
5369
5562
  * @param id - The id of the `pc-asset`, or an empty string to clear the slot.
5370
5563
  * @param slot - The material property to write.
5371
5564
  */
5372
- setMap(id, slot) {
5565
+ _setMap(id, slot) {
5373
5566
  // Drop any load still pending for this slot - its texture is no longer the one we want
5374
5567
  this._mapHandles.get(slot)?.off();
5375
5568
  this._mapHandles.delete(slot);
@@ -5463,7 +5656,7 @@
5463
5656
  */
5464
5657
  set aoMap(value) {
5465
5658
  this._aoMap = value;
5466
- this.setMap(value, 'aoMap');
5659
+ this._setMap(value, 'aoMap');
5467
5660
  }
5468
5661
  /**
5469
5662
  * Gets the id of the `pc-asset` used as the ambient occlusion map.
@@ -5695,7 +5888,7 @@
5695
5888
  */
5696
5889
  set diffuseMap(value) {
5697
5890
  this._diffuseMap = value;
5698
- this.setMap(value, 'diffuseMap');
5891
+ this._setMap(value, 'diffuseMap');
5699
5892
  }
5700
5893
  /**
5701
5894
  * Gets the id of the `pc-asset` used as the diffuse map.
@@ -5836,7 +6029,7 @@
5836
6029
  */
5837
6030
  set emissiveMap(value) {
5838
6031
  this._emissiveMap = value;
5839
- this.setMap(value, 'emissiveMap');
6032
+ this._setMap(value, 'emissiveMap');
5840
6033
  }
5841
6034
  /**
5842
6035
  * Gets the id of the `pc-asset` used as the emissive map.
@@ -6014,7 +6207,7 @@
6014
6207
  */
6015
6208
  set glossMap(value) {
6016
6209
  this._glossMap = value;
6017
- this.setMap(value, 'glossMap');
6210
+ this._setMap(value, 'glossMap');
6018
6211
  }
6019
6212
  /**
6020
6213
  * Gets the id of the `pc-asset` used as the gloss map.
@@ -6119,7 +6312,7 @@
6119
6312
  */
6120
6313
  set heightMap(value) {
6121
6314
  this._heightMap = value;
6122
- this.setMap(value, 'heightMap');
6315
+ this._setMap(value, 'heightMap');
6123
6316
  }
6124
6317
  /**
6125
6318
  * Gets the id of the `pc-asset` used as the height map.
@@ -6260,7 +6453,7 @@
6260
6453
  */
6261
6454
  set metalnessMap(value) {
6262
6455
  this._metalnessMap = value;
6263
- this.setMap(value, 'metalnessMap');
6456
+ this._setMap(value, 'metalnessMap');
6264
6457
  }
6265
6458
  /**
6266
6459
  * Gets the id of the `pc-asset` used as the metalness map.
@@ -6365,7 +6558,7 @@
6365
6558
  */
6366
6559
  set normalMap(value) {
6367
6560
  this._normalMap = value;
6368
- this.setMap(value, 'normalMap');
6561
+ this._setMap(value, 'normalMap');
6369
6562
  }
6370
6563
  /**
6371
6564
  * Gets the id of the `pc-asset` used as the normal map.
@@ -6453,7 +6646,7 @@
6453
6646
  set occludeDirect(value) {
6454
6647
  this._occludeDirect = value;
6455
6648
  if (this.material) {
6456
- // @ts-ignore see createMaterial() - the engine mistypes occludeDirect as a number
6649
+ // @ts-ignore see _createMaterial() - the engine mistypes occludeDirect as a number
6457
6650
  this.material.occludeDirect = value;
6458
6651
  this._scheduleUpdate();
6459
6652
  }
@@ -6545,7 +6738,7 @@
6545
6738
  */
6546
6739
  set opacityMap(value) {
6547
6740
  this._opacityMap = value;
6548
- this.setMap(value, 'opacityMap');
6741
+ this._setMap(value, 'opacityMap');
6549
6742
  }
6550
6743
  /**
6551
6744
  * Gets the id of the `pc-asset` used as the opacity map.
@@ -6863,6 +7056,13 @@
6863
7056
  get useTonemap() {
6864
7057
  return this._useTonemap;
6865
7058
  }
7059
+ /**
7060
+ * Returns the {@link StandardMaterial} created by the `<pc-material>` element with the given
7061
+ * `id`, or `undefined` if there is no such element or its material has not been created yet.
7062
+ *
7063
+ * @param id - The `id` of the `<pc-material>` element.
7064
+ * @returns The material, or `undefined`.
7065
+ */
6866
7066
  static get(id) {
6867
7067
  const materialElement = document.querySelector(`pc-material[id="${id}"]`);
6868
7068
  return materialElement?.material;
@@ -7501,7 +7701,18 @@
7501
7701
  return this._type;
7502
7702
  }
7503
7703
  static get observedAttributes() {
7504
- return [...super.observedAttributes, 'angular-damping', 'angular-factor', 'friction', 'linear-damping', 'linear-factor', 'mass', 'restitution', 'rolling-friction', 'type'];
7704
+ return [
7705
+ ...super.observedAttributes,
7706
+ 'angular-damping',
7707
+ 'angular-factor',
7708
+ 'friction',
7709
+ 'linear-damping',
7710
+ 'linear-factor',
7711
+ 'mass',
7712
+ 'restitution',
7713
+ 'rolling-friction',
7714
+ 'type'
7715
+ ];
7505
7716
  }
7506
7717
  attributeChangedCallback(name, _oldValue, newValue) {
7507
7718
  super.attributeChangedCallback(name, _oldValue, newValue);
@@ -7806,13 +8017,7 @@
7806
8017
  return this._handle;
7807
8018
  }
7808
8019
  static get observedAttributes() {
7809
- return [
7810
- ...super.observedAttributes,
7811
- 'orientation',
7812
- 'value',
7813
- 'handle-size',
7814
- 'handle'
7815
- ];
8020
+ return [...super.observedAttributes, 'orientation', 'value', 'handle-size', 'handle'];
7816
8021
  }
7817
8022
  attributeChangedCallback(name, _oldValue, newValue) {
7818
8023
  super.attributeChangedCallback(name, _oldValue, newValue);
@@ -8038,7 +8243,8 @@
8038
8243
  set horizontalScrollbarVisibility(value) {
8039
8244
  this._horizontalScrollbarVisibility = value;
8040
8245
  if (this.component) {
8041
- this.component.horizontalScrollbarVisibility = visibilities.get(value) ?? playcanvas.SCROLLBAR_VISIBILITY_SHOW_WHEN_REQUIRED;
8246
+ this.component.horizontalScrollbarVisibility =
8247
+ visibilities.get(value) ?? playcanvas.SCROLLBAR_VISIBILITY_SHOW_WHEN_REQUIRED;
8042
8248
  }
8043
8249
  }
8044
8250
  /**
@@ -8056,7 +8262,8 @@
8056
8262
  set verticalScrollbarVisibility(value) {
8057
8263
  this._verticalScrollbarVisibility = value;
8058
8264
  if (this.component) {
8059
- this.component.verticalScrollbarVisibility = visibilities.get(value) ?? playcanvas.SCROLLBAR_VISIBILITY_SHOW_WHEN_REQUIRED;
8265
+ this.component.verticalScrollbarVisibility =
8266
+ visibilities.get(value) ?? playcanvas.SCROLLBAR_VISIBILITY_SHOW_WHEN_REQUIRED;
8060
8267
  }
8061
8268
  }
8062
8269
  /**
@@ -8243,15 +8450,9 @@
8243
8450
  class ScriptElement extends AsyncElement {
8244
8451
  _attributes = {};
8245
8452
  _enabled = true;
8246
- /**
8247
- * Whether readiness has been signalled. Creation can happen more than once over an
8248
- * element's life (a runtime `name` change recreates the instance), but `ready` is a
8249
- * one-shot signal, so only the first successful creation fires it.
8250
- */
8251
- _readySignalled = false;
8252
8453
  /**
8253
8454
  * The Script instance created for this element by its parent `<pc-scripts>` element.
8254
- * @ignore
8455
+ * @internal
8255
8456
  */
8256
8457
  _script = null;
8257
8458
  /**
@@ -8333,14 +8534,20 @@
8333
8534
  console.warn(`pc-script '${this.getAttribute('name')}' must be a direct child of pc-scripts - script not created`);
8334
8535
  }
8335
8536
  }
8537
+ disconnectedCallback() {
8538
+ // Re-arm readiness so a re-inserted element announces the instance created for it then.
8539
+ // `_script` is deliberately NOT cleared here: the parent's mutation observer processes
8540
+ // this removal afterwards and reads it to establish which engine script this element
8541
+ // owned - the parent is what clears it.
8542
+ this._resetReady();
8543
+ }
8336
8544
  /**
8337
8545
  * Called by the parent `<pc-scripts>` element when the script instance has been created.
8338
- * @ignore
8546
+ * Creation can happen more than once per connection (a runtime `name` change recreates the
8547
+ * instance), but `_onReady` signals readiness at most once per cycle.
8548
+ * @internal
8339
8549
  */
8340
8550
  _onScriptCreated() {
8341
- if (this._readySignalled)
8342
- return;
8343
- this._readySignalled = true;
8344
8551
  this._onReady();
8345
8552
  }
8346
8553
  static get observedAttributes() {
@@ -8385,10 +8592,34 @@
8385
8592
  */
8386
8593
  const RESERVED_ATTRIBUTES = new Set([
8387
8594
  ...ScriptElement.observedAttributes,
8388
- 'accesskey', 'autocapitalize', 'autofocus', 'class', 'contenteditable', 'dir', 'draggable',
8389
- 'exportparts', 'hidden', 'id', 'inert', 'is', 'itemid', 'itemprop', 'itemref', 'itemscope',
8390
- 'itemtype', 'lang', 'nonce', 'part', 'popover', 'role', 'slot', 'spellcheck', 'style',
8391
- 'tabindex', 'title', 'translate'
8595
+ 'accesskey',
8596
+ 'autocapitalize',
8597
+ 'autofocus',
8598
+ 'class',
8599
+ 'contenteditable',
8600
+ 'dir',
8601
+ 'draggable',
8602
+ 'exportparts',
8603
+ 'hidden',
8604
+ 'id',
8605
+ 'inert',
8606
+ 'is',
8607
+ 'itemid',
8608
+ 'itemprop',
8609
+ 'itemref',
8610
+ 'itemscope',
8611
+ 'itemtype',
8612
+ 'lang',
8613
+ 'nonce',
8614
+ 'part',
8615
+ 'popover',
8616
+ 'role',
8617
+ 'slot',
8618
+ 'spellcheck',
8619
+ 'style',
8620
+ 'tabindex',
8621
+ 'title',
8622
+ 'translate'
8392
8623
  ]);
8393
8624
  /**
8394
8625
  * Checks whether a `pc-script` attribute name is reserved (and so never maps to a script
@@ -8400,18 +8631,25 @@
8400
8631
  * @returns Whether the attribute name is reserved.
8401
8632
  */
8402
8633
  const isReservedAttribute = (name) => {
8403
- return RESERVED_ATTRIBUTES.has(name) ||
8634
+ return (RESERVED_ATTRIBUTES.has(name) ||
8404
8635
  name.startsWith('data-') ||
8405
8636
  name.startsWith('aria-') ||
8406
8637
  name.startsWith('_') ||
8407
- (name.startsWith('on') && name in HTMLElement.prototype);
8638
+ (name.startsWith('on') && name in HTMLElement.prototype));
8408
8639
  };
8409
8640
  /**
8410
8641
  * Script API members that per-property attributes must never overwrite: the engine bindings and
8411
8642
  * the (optional, so possibly undefined) lifecycle methods.
8412
8643
  */
8413
8644
  const SCRIPT_API_MEMBERS = new Set([
8414
- 'app', 'entity', 'destroy', 'initialize', 'postInitialize', 'postUpdate', 'swap', 'update'
8645
+ 'app',
8646
+ 'entity',
8647
+ 'destroy',
8648
+ 'initialize',
8649
+ 'postInitialize',
8650
+ 'postUpdate',
8651
+ 'swap',
8652
+ 'update'
8415
8653
  ]);
8416
8654
  /**
8417
8655
  * Converts a kebab-case attribute name to the camelCase script attribute name.
@@ -8427,7 +8665,7 @@
8427
8665
  * @returns The kebab-case name.
8428
8666
  */
8429
8667
  const camelToKebab = (name) => {
8430
- return name.replace(/[A-Z]/g, char => `-${char.toLowerCase()}`);
8668
+ return name.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`);
8431
8669
  };
8432
8670
  /**
8433
8671
  * Resolves an `asset:` prefix to the Asset created by the `pc-asset` element with that id.
@@ -8626,7 +8864,10 @@
8626
8864
  // Only recurse into plain objects. Class instances (Vec3, Color, Asset, Entity...)
8627
8865
  // are leaf values assigned whole, so accessor-typed script attributes receive them
8628
8866
  // through their setters instead of having a getter's returned copy mutated.
8629
- if (value && typeof value === 'object' && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype) {
8867
+ if (value &&
8868
+ typeof value === 'object' &&
8869
+ !Array.isArray(value) &&
8870
+ Object.getPrototypeOf(value) === Object.prototype) {
8630
8871
  if (!current || typeof current !== 'object') {
8631
8872
  target[key] = {};
8632
8873
  }
@@ -8644,7 +8885,11 @@
8644
8885
  * @returns Whether the value is a math type.
8645
8886
  */
8646
8887
  isMathType(value) {
8647
- return value instanceof playcanvas.Vec2 || value instanceof playcanvas.Vec3 || value instanceof playcanvas.Vec4 || value instanceof playcanvas.Color || value instanceof playcanvas.Quat;
8888
+ return (value instanceof playcanvas.Vec2 ||
8889
+ value instanceof playcanvas.Vec3 ||
8890
+ value instanceof playcanvas.Vec4 ||
8891
+ value instanceof playcanvas.Color ||
8892
+ value instanceof playcanvas.Quat);
8648
8893
  }
8649
8894
  /**
8650
8895
  * Converts a plain numeric array to the math type of `current`. A 3-element array targeting
@@ -8657,7 +8902,7 @@
8657
8902
  * @returns The converted value, or `null`.
8658
8903
  */
8659
8904
  arrayToMathType(current, value, key) {
8660
- if (value.every(component => typeof component === 'number' && Number.isFinite(component))) {
8905
+ if (value.every((component) => typeof component === 'number' && Number.isFinite(component))) {
8661
8906
  if (current instanceof playcanvas.Vec2 && value.length === 2)
8662
8907
  return new playcanvas.Vec2(value);
8663
8908
  if (current instanceof playcanvas.Vec3 && value.length === 3)
@@ -8929,7 +9174,10 @@
8929
9174
  mutation.removedNodes.forEach((node) => {
8930
9175
  if (node instanceof ScriptElement) {
8931
9176
  const scriptName = node.getAttribute('name');
8932
- if (scriptName && node._script && this.component && this.component.get(scriptName) === node._script) {
9177
+ if (scriptName &&
9178
+ node._script &&
9179
+ this.component &&
9180
+ this.component.get(scriptName) === node._script) {
8933
9181
  this.destroyScript(scriptName);
8934
9182
  }
8935
9183
  node._script = null;
@@ -9178,18 +9426,27 @@
9178
9426
  * emit a misleading "must be a direct child" warning for what is an ordinary removal.
9179
9427
  */
9180
9428
  _soundElement = null;
9429
+ /**
9430
+ * Incremented on every connect and disconnect, and captured by connectedCallback on entry —
9431
+ * a resume from an await abandons itself if the value has moved on, so a stale callback can
9432
+ * neither act on a torn-down tree nor add its slot alongside a re-inserted element's own
9433
+ * callback.
9434
+ */
9435
+ _connectionGeneration = 0;
9181
9436
  /**
9182
9437
  * The sound slot.
9183
9438
  */
9184
9439
  soundSlot = null;
9185
9440
  async connectedCallback() {
9441
+ const generation = ++this._connectionGeneration;
9186
9442
  const soundElement = this.soundElement;
9187
9443
  await soundElement?.ready();
9188
- // The element may have been removed, or its parent torn down, while we were waiting. A
9189
- // <pc-app> disconnects before its children, so by the time we resume the component can
9190
- // already be gone - see the matching guard in disconnectedCallback below.
9444
+ // The element may have been removed (perhaps re-inserted, which runs a callback of its
9445
+ // own), or its parent torn down, while we were waiting. A <pc-app> disconnects before
9446
+ // its children, so by the time we resume the component can already be gone - see the
9447
+ // matching guard in disconnectedCallback below.
9191
9448
  const component = soundElement?.component;
9192
- if (!this.isConnected || !component) {
9449
+ if (generation !== this._connectionGeneration || !component) {
9193
9450
  return;
9194
9451
  }
9195
9452
  const options = {
@@ -9212,12 +9469,15 @@
9212
9469
  this._onReady();
9213
9470
  }
9214
9471
  disconnectedCallback() {
9472
+ // Invalidate any connectedCallback still suspended on an await
9473
+ this._connectionGeneration++;
9215
9474
  // Uses the cached parent rather than a fresh lookup, since parentElement is already null
9216
9475
  // by now. The component itself is null if the parent <pc-sound> (or the whole <pc-app>) is
9217
9476
  // being torn down — parents disconnect first and have already removed the component.
9218
9477
  this._soundElement?.component?.removeSlot(this._name);
9219
9478
  this._soundElement = null;
9220
9479
  this.soundSlot = null;
9480
+ this._resetReady();
9221
9481
  }
9222
9482
  get soundElement() {
9223
9483
  const soundElement = this.parentElement;
@@ -9617,39 +9877,85 @@
9617
9877
  class ModelElement extends AsyncElement {
9618
9878
  _asset = '';
9619
9879
  _entity = null;
9880
+ /**
9881
+ * Incremented on every new load and on disconnect, and captured by a load when it starts. A
9882
+ * load that resumes from an await or a load callback abandons itself if the value has moved
9883
+ * on, so a superseded load can neither instantiate a second entity nor parent one that has
9884
+ * since been destroyed.
9885
+ */
9886
+ _loadGeneration = 0;
9887
+ /**
9888
+ * The pending asset-load subscription of the current load, if it is waiting for its asset.
9889
+ * Held so that whatever supersedes the load can detach the handler from the asset, rather
9890
+ * than leave it registered until the asset loads (or forever, if it never does).
9891
+ */
9892
+ _loadHandle = null;
9893
+ /**
9894
+ * The root entity of the instantiated model. `null` until the container asset has loaded
9895
+ * and been instantiated, and again once the element has been removed from the document.
9896
+ * @returns The model's root entity, or `null`.
9897
+ */
9898
+ get entity() {
9899
+ return this._entity;
9900
+ }
9620
9901
  connectedCallback() {
9621
9902
  this._loadModel();
9622
9903
  this._onReady();
9623
9904
  }
9624
9905
  disconnectedCallback() {
9906
+ this._loadGeneration++;
9907
+ this._detachLoadHandler();
9625
9908
  this._unloadModel();
9909
+ this._resetReady();
9910
+ }
9911
+ _detachLoadHandler() {
9912
+ this._loadHandle?.off();
9913
+ this._loadHandle = null;
9626
9914
  }
9627
9915
  _instantiate(container) {
9628
- this._entity = container.instantiateRenderEntity();
9916
+ const generation = this._loadGeneration;
9917
+ const entity = container.instantiateRenderEntity();
9918
+ this._entity = entity;
9629
9919
  // @ts-ignore
9630
9920
  if (container.animations.length > 0) {
9631
- this._entity.addComponent('anim');
9921
+ entity.addComponent('anim');
9632
9922
  // @ts-ignore
9633
- this._entity.anim.assignAnimation('animation', container.animations[0].resource);
9923
+ entity.anim.assignAnimation('animation', container.animations[0].resource);
9634
9924
  }
9925
+ // The parent's readiness re-arms when it is torn down, so these can resume in a later
9926
+ // connection cycle. The entity is captured above and the generation re-checked, so a
9927
+ // stale resume cannot parent an entity a newer cycle has already destroyed.
9635
9928
  const parentEntityElement = this.closestEntity;
9636
9929
  if (parentEntityElement) {
9637
9930
  parentEntityElement.ready().then(() => {
9638
- parentEntityElement.entity.addChild(this._entity);
9931
+ if (generation !== this._loadGeneration) {
9932
+ return;
9933
+ }
9934
+ parentEntityElement.entity.addChild(entity);
9639
9935
  });
9640
9936
  }
9641
9937
  else {
9642
9938
  const appElement = this.closestApp;
9643
9939
  if (appElement) {
9644
9940
  appElement.ready().then(() => {
9645
- appElement.app.root.addChild(this._entity);
9941
+ if (generation !== this._loadGeneration) {
9942
+ return;
9943
+ }
9944
+ appElement.app.root.addChild(entity);
9646
9945
  });
9647
9946
  }
9648
9947
  }
9649
9948
  }
9650
9949
  async _loadModel() {
9651
9950
  this._unloadModel();
9951
+ // Supersede any load already in flight - only the newest load may instantiate
9952
+ const generation = ++this._loadGeneration;
9953
+ this._detachLoadHandler();
9652
9954
  const appElement = await this.closestApp?.ready();
9955
+ // The element may have been removed, or another load started, while we waited
9956
+ if (generation !== this._loadGeneration) {
9957
+ return;
9958
+ }
9653
9959
  const app = appElement?.app;
9654
9960
  const asset = AssetElement.get(this._asset);
9655
9961
  if (!asset) {
@@ -9659,7 +9965,14 @@
9659
9965
  this._instantiate(asset.resource);
9660
9966
  }
9661
9967
  else {
9662
- asset.once('load', () => {
9968
+ // The generation is re-checked even though a superseded handler is detached: the
9969
+ // detach relies on how the engine's event emitter treats removal, while the check
9970
+ // holds on its own.
9971
+ this._loadHandle = asset.once('load', () => {
9972
+ this._loadHandle = null;
9973
+ if (generation !== this._loadGeneration) {
9974
+ return;
9975
+ }
9663
9976
  this._instantiate(asset.resource);
9664
9977
  });
9665
9978
  app.assets.load(asset);
@@ -9761,10 +10074,17 @@
9761
10074
  return;
9762
10075
  }
9763
10076
  this._scene = app.scene;
9764
- this.updateSceneSettings();
10077
+ this._updateSceneSettings();
9765
10078
  this._onReady();
9766
10079
  }
9767
- updateSceneSettings() {
10080
+ disconnectedCallback() {
10081
+ // The scene belongs to the application, and removing this element - or the <pc-app>
10082
+ // above it, which disconnects first - parts the two. Re-arm readiness so a re-inserted
10083
+ // element announces the scene it acquires then, not the one it lost here.
10084
+ this._scene = null;
10085
+ this._resetReady();
10086
+ }
10087
+ _updateSceneSettings() {
9768
10088
  if (this._scene) {
9769
10089
  this._scene.fog.type = this._fog;
9770
10090
  this._scene.fog.color = this._fogColor;
@@ -9932,13 +10252,32 @@
9932
10252
  _type = 'infinite';
9933
10253
  _scene = null;
9934
10254
  _appElement = null;
10255
+ /**
10256
+ * Incremented on every new load and on disconnect, and captured by a load when it starts. A
10257
+ * load that resumes from an await or a load callback abandons itself if the value has moved
10258
+ * on, so a superseded load cannot generate a skybox for a scene it no longer configures.
10259
+ */
10260
+ _loadGeneration = 0;
10261
+ /**
10262
+ * The pending asset-load subscription of the current load, if it is waiting for its asset.
10263
+ * Held so that whatever supersedes the load can detach the handler from the asset, rather
10264
+ * than leave it registered until the asset loads (or forever, if it never does).
10265
+ */
10266
+ _loadHandle = null;
9935
10267
  connectedCallback() {
9936
10268
  this._loadSkybox();
9937
10269
  this._onReady();
9938
10270
  }
9939
10271
  disconnectedCallback() {
10272
+ this._loadGeneration++;
10273
+ this._detachLoadHandler();
9940
10274
  this._unloadSkybox();
9941
10275
  this._appElement = null;
10276
+ this._resetReady();
10277
+ }
10278
+ _detachLoadHandler() {
10279
+ this._loadHandle?.off();
10280
+ this._loadHandle = null;
9942
10281
  }
9943
10282
  _generateSkybox(asset) {
9944
10283
  if (!this._scene)
@@ -9946,10 +10285,17 @@
9946
10285
  const source = asset.resource;
9947
10286
  const skybox = playcanvas.EnvLighting.generateSkyboxCubemap(source);
9948
10287
  skybox.anisotropy = 4;
10288
+ // This element owns what it generated (see _unloadSkybox) - replacing a skybox from an
10289
+ // earlier load must release it, not orphan it on the GPU
10290
+ this._scene.skybox?.destroy();
9949
10291
  this._scene.skybox = skybox;
9950
10292
  if (this._lighting) {
9951
10293
  const lighting = playcanvas.EnvLighting.generateLightingSource(source);
9952
10294
  const envAtlas = playcanvas.EnvLighting.generateAtlas(lighting);
10295
+ // The lighting source is an intermediate: the atlas is rendered from it and it is
10296
+ // not needed afterwards
10297
+ lighting.destroy();
10298
+ this._scene.envAtlas?.destroy();
9953
10299
  this._scene.envAtlas = envAtlas;
9954
10300
  }
9955
10301
  const layer = this._scene.layers.getLayerById(playcanvas.LAYERID_SKYBOX);
@@ -9963,7 +10309,14 @@
9963
10309
  this._scene.skyboxMip = this._mipLevel;
9964
10310
  }
9965
10311
  async _loadSkybox() {
10312
+ // Supersede any load already in flight - only the newest load may generate the skybox
10313
+ const generation = ++this._loadGeneration;
10314
+ this._detachLoadHandler();
9966
10315
  const appElement = await this.closestApp?.ready();
10316
+ // The element may have been removed, or another load started, while we waited
10317
+ if (generation !== this._loadGeneration) {
10318
+ return;
10319
+ }
9967
10320
  const app = appElement?.app;
9968
10321
  if (!appElement || !app) {
9969
10322
  return;
@@ -9978,7 +10331,14 @@
9978
10331
  this._generateSkybox(asset);
9979
10332
  }
9980
10333
  else {
9981
- asset.once('load', () => {
10334
+ // The generation is re-checked even though a superseded handler is detached: the
10335
+ // detach relies on how the engine's event emitter treats removal, while the check
10336
+ // holds on its own.
10337
+ this._loadHandle = asset.once('load', () => {
10338
+ this._loadHandle = null;
10339
+ if (generation !== this._loadGeneration) {
10340
+ return;
10341
+ }
9982
10342
  this._generateSkybox(asset);
9983
10343
  });
9984
10344
  app.assets.load(asset);