@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.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
  }
@@ -568,7 +601,10 @@ const parseTags = (value, defaultValue = []) => {
568
601
  // caller's default, or a later mutation would write back through it.
569
602
  return [...defaultValue];
570
603
  }
571
- return value.split(',').map(tag => tag.trim()).filter(tag => tag !== '');
604
+ return value
605
+ .split(',')
606
+ .map((tag) => tag.trim())
607
+ .filter((tag) => tag !== '');
572
608
  };
573
609
  /**
574
610
  * Parse a Vec2 attribute value. The expected format is 2 space-separated numbers (e.g. '1 2').
@@ -660,6 +696,8 @@ const getEntity = (ref) => {
660
696
  return element?.entity ?? null;
661
697
  };
662
698
 
699
+ /** The pointer event types the application synthesizes on `<pc-entity>` elements via picking. */
700
+ const pointerEventTypes = ['pointermove', 'pointerdown', 'pointerup', 'pointerenter', 'pointerleave'];
663
701
  /**
664
702
  * The AppElement interface provides properties and methods for manipulating
665
703
  * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-app/ | `<pc-app>`} elements.
@@ -690,7 +728,20 @@ class AppElement extends AsyncElement {
690
728
  */
691
729
  _optionsLocked = false;
692
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
+ */
693
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;
694
745
  /**
695
746
  * The elements backing this application's entities, keyed by the entity itself. Registered
696
747
  * by EntityElement at creation and removed when an entity is destroyed, this joins engine
@@ -743,8 +794,16 @@ class AppElement extends AsyncElement {
743
794
  super();
744
795
  // Bind methods to maintain 'this' context
745
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
+ });
746
804
  }
747
805
  async connectedCallback() {
806
+ const generation = ++this._bootGeneration;
748
807
  // Created before the first await, so the bar is visible while modules and the graphics
749
808
  // device are created, and exists before any disconnect could need to clean it up
750
809
  if (this._loadingBar && !this._bar) {
@@ -753,7 +812,12 @@ class AppElement extends AsyncElement {
753
812
  // Get all pc-module elements that are direct children of the pc-app element
754
813
  const moduleElements = this.querySelectorAll(':scope > pc-module');
755
814
  // Wait for all modules to load
756
- 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
+ }
757
821
  // Create and append the canvas to the element
758
822
  this._canvas = document.createElement('canvas');
759
823
  this.appendChild(this._canvas);
@@ -773,6 +837,13 @@ class AppElement extends AsyncElement {
773
837
  deviceTypes: deviceTypes,
774
838
  stencil: this._stencilBuffer
775
839
  });
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
+ }
776
847
  // Assigned rather than resolved to a number here: the engine caps against the live
777
848
  // window.devicePixelRatio on every resize, so an uncapped Infinity keeps following the
778
849
  // display when a window moves between monitors of differing density.
@@ -849,27 +920,41 @@ class AppElement extends AsyncElement {
849
920
  this._pickerCreate();
850
921
  // Get all pc-asset elements that are direct children of the pc-app element
851
922
  const assetElements = this.querySelectorAll(':scope > pc-asset');
852
- Array.from(assetElements).forEach((assetElement) => {
853
- assetElement.createAsset();
923
+ for (const assetElement of Array.from(assetElements)) {
924
+ assetElement._createAsset();
854
925
  const asset = assetElement.asset;
855
926
  if (asset) {
856
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
+ }
857
935
  }
858
- });
936
+ }
859
937
  // Get all pc-material elements that are direct children of the pc-app element
860
938
  const materialElements = this.querySelectorAll(':scope > pc-material');
861
939
  Array.from(materialElements).forEach((materialElement) => {
862
- materialElement.createMaterial();
940
+ materialElement._createMaterial();
863
941
  });
864
942
  // Create all entities
865
943
  const entityElements = this.querySelectorAll('pc-entity');
866
944
  Array.from(entityElements).forEach((entityElement) => {
867
- entityElement.createEntity(app);
945
+ entityElement._createEntity(app);
868
946
  });
869
947
  // Build hierarchy
870
948
  entityElements.forEach((entityElement) => {
871
- entityElement.buildHierarchy(app);
949
+ entityElement._buildHierarchy(app);
872
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
+ }
873
958
  this._hierarchyReady = true;
874
959
  // Forward the engine's preload lifecycle as DOM ProgressEvents on this element. The
875
960
  // listener must be attached before preload() is called: an asset that is already loaded
@@ -886,8 +971,19 @@ class AppElement extends AsyncElement {
886
971
  this._loadProgress = total === 0 ? 1 : 0;
887
972
  this._bar?.progress(0, total);
888
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
+ }
889
979
  // Load assets before starting the application
890
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
+ }
891
987
  // Scope the counter to this preload pass, so a later app.preload() call by user code
892
988
  // cannot push `loaded` past `total`
893
989
  app.off('preload:progress', onPreloadProgress);
@@ -903,6 +999,9 @@ class AppElement extends AsyncElement {
903
999
  });
904
1000
  }
905
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++;
906
1005
  this._optionsLocked = false;
907
1006
  this._pickerDestroy();
908
1007
  // Clean up the application. Destroying it destroys every entity, whose destroy hooks
@@ -915,6 +1014,11 @@ class AppElement extends AsyncElement {
915
1014
  this._loadProgress = 0;
916
1015
  this._bar?.destroy();
917
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();
918
1022
  // Remove event listeners
919
1023
  window.removeEventListener('resize', this._onWindowResize);
920
1024
  // Remove the canvas
@@ -942,14 +1046,11 @@ class AppElement extends AsyncElement {
942
1046
  this._pointerHandlers.pointermove = listener(this._onPointerMove);
943
1047
  this._pointerHandlers.pointerdown = listener(this._onPointerDown);
944
1048
  this._pointerHandlers.pointerup = listener(this._onPointerUp);
945
- // Listen for pointer listeners being added/removed
946
- ['pointermove', 'pointerdown', 'pointerup', 'pointerenter', 'pointerleave'].forEach((type) => {
947
- this.addEventListener(`${type}:connect`, () => this._onPointerListenerAdded(type));
948
- this.addEventListener(`${type}:disconnect`, () => this._onPointerListenerRemoved(type));
949
- // Attach canvas handlers for listeners registered before this point (e.g. handlers
950
- // created from onpointer* attributes when their elements were first upgraded)
951
- const anyListeners = Array.from(this.querySelectorAll('pc-entity'))
952
- .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));
953
1054
  if (anyListeners) {
954
1055
  this._onPointerListenerAdded(type);
955
1056
  }
@@ -964,6 +1065,7 @@ class AppElement extends AsyncElement {
964
1065
  });
965
1066
  }
966
1067
  this._picker = null;
1068
+ this._hoveredEntity = null;
967
1069
  this._pointerHandlers = {
968
1070
  pointermove: null,
969
1071
  pointerdown: null,
@@ -983,7 +1085,7 @@ class AppElement extends AsyncElement {
983
1085
  *
984
1086
  * @param entity - The entity.
985
1087
  * @param element - The element that created it.
986
- * @ignore
1088
+ * @internal
987
1089
  */
988
1090
  _registerEntityElement(entity, element) {
989
1091
  this._entityElements.set(entity, element);
@@ -992,7 +1094,7 @@ class AppElement extends AsyncElement {
992
1094
  * Removes the registration for a destroyed entity. Called by EntityElement.
993
1095
  *
994
1096
  * @param entity - The entity.
995
- * @ignore
1097
+ * @internal
996
1098
  */
997
1099
  _unregisterEntityElement(entity) {
998
1100
  this._entityElements.delete(entity);
@@ -1038,7 +1140,7 @@ class AppElement extends AsyncElement {
1038
1140
  _elementWithListener(node, type) {
1039
1141
  while (node !== null) {
1040
1142
  const element = this._entityElements.get(node);
1041
- if (element?.hasListeners(type)) {
1143
+ if (element?._hasListeners(type)) {
1042
1144
  return element;
1043
1145
  }
1044
1146
  node = node.parent;
@@ -1095,17 +1197,17 @@ class AppElement extends AsyncElement {
1095
1197
  const newHoverEntity = this._elementFromNode(node);
1096
1198
  // Handle enter/leave events
1097
1199
  if (this._hoveredEntity !== newHoverEntity) {
1098
- if (this._hoveredEntity && this._hoveredEntity.hasListeners('pointerleave')) {
1200
+ if (this._hoveredEntity && this._hoveredEntity._hasListeners('pointerleave')) {
1099
1201
  this._hoveredEntity.dispatchEvent(new PointerEvent('pointerleave', event));
1100
1202
  }
1101
- if (newHoverEntity && newHoverEntity.hasListeners('pointerenter')) {
1203
+ if (newHoverEntity && newHoverEntity._hasListeners('pointerenter')) {
1102
1204
  newHoverEntity.dispatchEvent(new PointerEvent('pointerenter', event));
1103
1205
  }
1104
1206
  }
1105
1207
  // Update hover state
1106
1208
  this._hoveredEntity = newHoverEntity;
1107
1209
  // Handle pointermove event
1108
- if (newHoverEntity && newHoverEntity.hasListeners('pointermove')) {
1210
+ if (newHoverEntity && newHoverEntity._hasListeners('pointermove')) {
1109
1211
  newHoverEntity.dispatchEvent(new PointerEvent('pointermove', event));
1110
1212
  }
1111
1213
  }
@@ -1135,22 +1237,21 @@ class AppElement extends AsyncElement {
1135
1237
  if (!this._hasPointerListeners[type] && this._canvas) {
1136
1238
  this._hasPointerListeners[type] = true;
1137
1239
  // For enter/leave events, we need the move handler
1138
- const handler = (type === 'pointerenter' || type === 'pointerleave') ?
1139
- this._pointerHandlers.pointermove :
1140
- this._pointerHandlers[type];
1240
+ const handler = type === 'pointerenter' || type === 'pointerleave'
1241
+ ? this._pointerHandlers.pointermove
1242
+ : this._pointerHandlers[type];
1141
1243
  if (handler) {
1142
1244
  this._canvas.addEventListener(type === 'pointerenter' || type === 'pointerleave' ? 'pointermove' : type, handler);
1143
1245
  }
1144
1246
  }
1145
1247
  }
1146
1248
  _onPointerListenerRemoved(type) {
1147
- const hasListeners = Array.from(this.querySelectorAll('pc-entity'))
1148
- .some(entity => entity.hasListeners(type));
1249
+ const hasListeners = Array.from(this.querySelectorAll('pc-entity')).some((entity) => entity._hasListeners(type));
1149
1250
  if (!hasListeners && this._canvas) {
1150
1251
  this._hasPointerListeners[type] = false;
1151
- const handler = (type === 'pointerenter' || type === 'pointerleave') ?
1152
- this._pointerHandlers.pointermove :
1153
- this._pointerHandlers[type];
1252
+ const handler = type === 'pointerenter' || type === 'pointerleave'
1253
+ ? this._pointerHandlers.pointermove
1254
+ : this._pointerHandlers[type];
1154
1255
  if (handler) {
1155
1256
  this._canvas.removeEventListener(type === 'pointerenter' || type === 'pointerleave' ? 'pointermove' : type, handler);
1156
1257
  }
@@ -1231,14 +1332,6 @@ class AppElement extends AsyncElement {
1231
1332
  get depthBuffer() {
1232
1333
  return this._depthBuffer;
1233
1334
  }
1234
- /**
1235
- * Gets the hierarchy ready flag.
1236
- * @returns The hierarchy ready flag.
1237
- * @ignore
1238
- */
1239
- get hierarchyReady() {
1240
- return this._hierarchyReady;
1241
- }
1242
1335
  /**
1243
1336
  * Sets whether the application shows its built-in loading bar while it boots and preloads its
1244
1337
  * assets. Enabled by default; setting `false` removes the bar immediately, while setting
@@ -1407,7 +1500,14 @@ class EntityElement extends AsyncElement {
1407
1500
  get entity() {
1408
1501
  return this._entity;
1409
1502
  }
1410
- 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) {
1411
1511
  // Guard against double creation. When a subtree is inserted at runtime (e.g. cloning a
1412
1512
  // `<template>`), an ancestor's connectedCallback eagerly creates descendant entities; the
1413
1513
  // descendants' own connectedCallbacks would otherwise create them a second time.
@@ -1438,8 +1538,10 @@ class EntityElement extends AsyncElement {
1438
1538
  }
1439
1539
  /**
1440
1540
  * Handles the destruction of the backing entity. Resets the element so a later re-insertion
1441
- * starts clean: `_built` must be cleared alongside `_entity`, or buildHierarchy would bail
1442
- * and a re-created entity would never be parented.
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`.
1443
1545
  *
1444
1546
  * @param entity - The entity that was destroyed.
1445
1547
  */
@@ -1448,8 +1550,18 @@ class EntityElement extends AsyncElement {
1448
1550
  this._appElement = null;
1449
1551
  this._entity = null;
1450
1552
  this._built = false;
1553
+ this._resetReady();
1451
1554
  }
1452
- 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) {
1453
1565
  if (!this.entity || this._built)
1454
1566
  return;
1455
1567
  this._built = true;
@@ -1475,17 +1587,17 @@ class EntityElement extends AsyncElement {
1475
1587
  return;
1476
1588
  }
1477
1589
  // If app is already running, create entity immediately
1478
- if (closestApp.hierarchyReady) {
1590
+ if (closestApp._hierarchyReady) {
1479
1591
  const app = closestApp.app;
1480
- this.createEntity(app);
1481
- this.buildHierarchy(app);
1592
+ this._createEntity(app);
1593
+ this._buildHierarchy(app);
1482
1594
  // Handle any child entities that might exist
1483
1595
  const childEntities = this.querySelectorAll('pc-entity');
1484
1596
  childEntities.forEach((child) => {
1485
- child.createEntity(app);
1597
+ child._createEntity(app);
1486
1598
  });
1487
1599
  childEntities.forEach((child) => {
1488
- child.buildHierarchy(app);
1600
+ child._buildHierarchy(app);
1489
1601
  });
1490
1602
  }
1491
1603
  }
@@ -1679,14 +1791,23 @@ class EntityElement extends AsyncElement {
1679
1791
  }
1680
1792
  removeEventListener(type, listener, options) {
1681
1793
  if (this._listeners[type]) {
1682
- this._listeners[type] = this._listeners[type].filter(l => l !== listener);
1794
+ this._listeners[type] = this._listeners[type].filter((l) => l !== listener);
1683
1795
  }
1684
1796
  super.removeEventListener(type, listener, options);
1685
1797
  if (type.startsWith('pointer')) {
1686
1798
  this.dispatchEvent(new CustomEvent(`${type}:disconnect`, { bubbles: true }));
1687
1799
  }
1688
1800
  }
1689
- 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) {
1690
1811
  return Boolean(this._listeners[type]?.length) || this._inlineHandlerTypes.has(type);
1691
1812
  }
1692
1813
  }
@@ -1977,7 +2098,7 @@ class AssetElement extends AsyncElement {
1977
2098
  const app = appElement.app;
1978
2099
  if (!app)
1979
2100
  return; // pc-app is re-connecting; its own boot will create this asset
1980
- this.createAsset();
2101
+ this._createAsset();
1981
2102
  if (this.asset) {
1982
2103
  app.assets.add(this.asset); // add() auto-loads when preload is true
1983
2104
  if (!this.lazy) {
@@ -1985,13 +2106,15 @@ class AssetElement extends AsyncElement {
1985
2106
  }
1986
2107
  }
1987
2108
  }
1988
- // Never ready if createAsset failed (unsupported asset type)
2109
+ // Never ready if _createAsset failed (unsupported asset type)
1989
2110
  if (this.asset) {
1990
2111
  this._onReady();
1991
2112
  }
1992
2113
  }
1993
2114
  disconnectedCallback() {
1994
- this.destroyAsset();
2115
+ this._destroyAsset();
2116
+ // Re-arm readiness so a re-inserted element announces the asset it creates then
2117
+ this._resetReady();
1995
2118
  }
1996
2119
  _onAssetLoad() {
1997
2120
  this.dispatchEvent(new Event('load'));
@@ -2001,7 +2124,14 @@ class AssetElement extends AsyncElement {
2001
2124
  message: err instanceof Error ? err.message : String(err)
2002
2125
  }));
2003
2126
  }
2004
- 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() {
2005
2135
  const id = this.getAttribute('id') || '';
2006
2136
  const src = this.getAttribute('src') || '';
2007
2137
  let type = this.getAttribute('type');
@@ -2091,7 +2221,7 @@ class AssetElement extends AsyncElement {
2091
2221
  }
2092
2222
  return data;
2093
2223
  }
2094
- destroyAsset() {
2224
+ _destroyAsset() {
2095
2225
  if (this.asset) {
2096
2226
  // A caller that keeps the Asset alive must not dispatch on a removed element
2097
2227
  this.asset.off('load', this._onAssetLoad, this);
@@ -2119,6 +2249,13 @@ class AssetElement extends AsyncElement {
2119
2249
  get lazy() {
2120
2250
  return this._lazy;
2121
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
+ */
2122
2259
  static get(id) {
2123
2260
  const assetElement = document.querySelector(`pc-asset[id="${id}"]`);
2124
2261
  return assetElement?.asset;
@@ -2144,6 +2281,14 @@ class ComponentElement extends AsyncElement {
2144
2281
  _enabled = true;
2145
2282
  _component = null;
2146
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;
2147
2292
  /**
2148
2293
  * Creates a new ComponentElement instance.
2149
2294
  *
@@ -2154,11 +2299,17 @@ class ComponentElement extends AsyncElement {
2154
2299
  super();
2155
2300
  this._componentName = componentName;
2156
2301
  }
2157
- // 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
+ */
2158
2308
  getInitialComponentData() {
2159
2309
  return {};
2160
2310
  }
2161
- async addComponent() {
2311
+ async _addComponent() {
2312
+ const generation = this._connectionGeneration;
2162
2313
  const entityElement = this.closestEntity;
2163
2314
  if (!entityElement) {
2164
2315
  // A component can only exist on an entity, so an element placed outside one is inert.
@@ -2168,19 +2319,42 @@ class ComponentElement extends AsyncElement {
2168
2319
  return;
2169
2320
  }
2170
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
+ }
2171
2327
  // Add the component to the entity
2172
2328
  const data = this.getInitialComponentData();
2173
2329
  this._component = entityElement.entity.addComponent(this._componentName, data);
2174
2330
  }
2175
- 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
+ }
2176
2338
  async connectedCallback() {
2339
+ const generation = ++this._connectionGeneration;
2177
2340
  this._appElement = this.closestApp ?? null;
2178
2341
  await this._appElement?.ready();
2179
- 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
+ }
2180
2352
  this.initComponent();
2181
2353
  this._onReady();
2182
2354
  }
2183
2355
  disconnectedCallback() {
2356
+ // Invalidate any connectedCallback still suspended on an await
2357
+ this._connectionGeneration++;
2184
2358
  // Remove the component when the element is disconnected. Skip this when the owning
2185
2359
  // application has already been destroyed — removing a <pc-app> disconnects it before
2186
2360
  // its children, taking the component systems with it.
@@ -2189,6 +2363,7 @@ class ComponentElement extends AsyncElement {
2189
2363
  }
2190
2364
  this._component = null;
2191
2365
  this._appElement = null;
2366
+ this._resetReady();
2192
2367
  }
2193
2368
  /**
2194
2369
  * The PlayCanvas component instance. `null` until the element is ready, and also for an
@@ -3246,7 +3421,17 @@ class CollisionComponentElement extends ComponentElement {
3246
3421
  return this._type;
3247
3422
  }
3248
3423
  static get observedAttributes() {
3249
- 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
+ ];
3250
3435
  }
3251
3436
  attributeChangedCallback(name, _oldValue, newValue) {
3252
3437
  super.attributeChangedCallback(name, _oldValue, newValue);
@@ -4951,7 +5136,7 @@ class ParticleSystemComponentElement extends ComponentElement {
4951
5136
  }
4952
5137
  // Set all the config properties on the component
4953
5138
  for (const key in resource) {
4954
- if (resource.hasOwnProperty(key)) {
5139
+ if (Object.hasOwn(resource, key)) {
4955
5140
  this.component[key] = resource[key];
4956
5141
  }
4957
5142
  }
@@ -5024,10 +5209,7 @@ class ParticleSystemComponentElement extends ComponentElement {
5024
5209
  }
5025
5210
  }
5026
5211
  static get observedAttributes() {
5027
- return [
5028
- ...super.observedAttributes,
5029
- 'asset'
5030
- ];
5212
+ return [...super.observedAttributes, 'asset'];
5031
5213
  }
5032
5214
  attributeChangedCallback(name, _oldValue, newValue) {
5033
5215
  super.attributeChangedCallback(name, _oldValue, newValue);
@@ -5187,7 +5369,7 @@ class MaterialElement extends HTMLElement {
5187
5369
  _twoSidedLighting = false;
5188
5370
  _useFog = true;
5189
5371
  _useLighting = true;
5190
- // 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()
5191
5373
  _useMetalness = true;
5192
5374
  _useMetalnessSpecularColor = false;
5193
5375
  _useSkybox = true;
@@ -5200,6 +5382,10 @@ class MaterialElement extends HTMLElement {
5200
5382
  _mapHandles = new Map();
5201
5383
  _updateScheduled = false;
5202
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
+ */
5203
5389
  material = null;
5204
5390
  async connectedCallback() {
5205
5391
  const appElement = this.parentElement?.closest('pc-app') ?? null;
@@ -5217,10 +5403,17 @@ class MaterialElement extends HTMLElement {
5217
5403
  if (!this.material) {
5218
5404
  if (!appElement.app)
5219
5405
  return; // pc-app is re-connecting; its own boot will create this
5220
- this.createMaterial();
5406
+ this._createMaterial();
5221
5407
  }
5222
5408
  }
5223
- 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() {
5224
5417
  const material = new playcanvas.StandardMaterial();
5225
5418
  this.material = material;
5226
5419
  material.alphaTest = this._alphaTest;
@@ -5347,9 +5540,9 @@ class MaterialElement extends HTMLElement {
5347
5540
  * warning latches and reports once per episode, clearing when the clash is resolved.
5348
5541
  */
5349
5542
  _warnGlossConflict() {
5350
- const quote = (names) => `'${names.join('\', \'')}'`;
5351
- const roughness = roughnessAliases.filter(name => this.hasAttribute(name));
5352
- 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));
5353
5546
  if (roughness.length === 0 || gloss.length === 0) {
5354
5547
  this._glossConflictWarned = false;
5355
5548
  return;
@@ -5367,7 +5560,7 @@ class MaterialElement extends HTMLElement {
5367
5560
  * @param id - The id of the `pc-asset`, or an empty string to clear the slot.
5368
5561
  * @param slot - The material property to write.
5369
5562
  */
5370
- setMap(id, slot) {
5563
+ _setMap(id, slot) {
5371
5564
  // Drop any load still pending for this slot - its texture is no longer the one we want
5372
5565
  this._mapHandles.get(slot)?.off();
5373
5566
  this._mapHandles.delete(slot);
@@ -5461,7 +5654,7 @@ class MaterialElement extends HTMLElement {
5461
5654
  */
5462
5655
  set aoMap(value) {
5463
5656
  this._aoMap = value;
5464
- this.setMap(value, 'aoMap');
5657
+ this._setMap(value, 'aoMap');
5465
5658
  }
5466
5659
  /**
5467
5660
  * Gets the id of the `pc-asset` used as the ambient occlusion map.
@@ -5693,7 +5886,7 @@ class MaterialElement extends HTMLElement {
5693
5886
  */
5694
5887
  set diffuseMap(value) {
5695
5888
  this._diffuseMap = value;
5696
- this.setMap(value, 'diffuseMap');
5889
+ this._setMap(value, 'diffuseMap');
5697
5890
  }
5698
5891
  /**
5699
5892
  * Gets the id of the `pc-asset` used as the diffuse map.
@@ -5834,7 +6027,7 @@ class MaterialElement extends HTMLElement {
5834
6027
  */
5835
6028
  set emissiveMap(value) {
5836
6029
  this._emissiveMap = value;
5837
- this.setMap(value, 'emissiveMap');
6030
+ this._setMap(value, 'emissiveMap');
5838
6031
  }
5839
6032
  /**
5840
6033
  * Gets the id of the `pc-asset` used as the emissive map.
@@ -6012,7 +6205,7 @@ class MaterialElement extends HTMLElement {
6012
6205
  */
6013
6206
  set glossMap(value) {
6014
6207
  this._glossMap = value;
6015
- this.setMap(value, 'glossMap');
6208
+ this._setMap(value, 'glossMap');
6016
6209
  }
6017
6210
  /**
6018
6211
  * Gets the id of the `pc-asset` used as the gloss map.
@@ -6117,7 +6310,7 @@ class MaterialElement extends HTMLElement {
6117
6310
  */
6118
6311
  set heightMap(value) {
6119
6312
  this._heightMap = value;
6120
- this.setMap(value, 'heightMap');
6313
+ this._setMap(value, 'heightMap');
6121
6314
  }
6122
6315
  /**
6123
6316
  * Gets the id of the `pc-asset` used as the height map.
@@ -6258,7 +6451,7 @@ class MaterialElement extends HTMLElement {
6258
6451
  */
6259
6452
  set metalnessMap(value) {
6260
6453
  this._metalnessMap = value;
6261
- this.setMap(value, 'metalnessMap');
6454
+ this._setMap(value, 'metalnessMap');
6262
6455
  }
6263
6456
  /**
6264
6457
  * Gets the id of the `pc-asset` used as the metalness map.
@@ -6363,7 +6556,7 @@ class MaterialElement extends HTMLElement {
6363
6556
  */
6364
6557
  set normalMap(value) {
6365
6558
  this._normalMap = value;
6366
- this.setMap(value, 'normalMap');
6559
+ this._setMap(value, 'normalMap');
6367
6560
  }
6368
6561
  /**
6369
6562
  * Gets the id of the `pc-asset` used as the normal map.
@@ -6451,7 +6644,7 @@ class MaterialElement extends HTMLElement {
6451
6644
  set occludeDirect(value) {
6452
6645
  this._occludeDirect = value;
6453
6646
  if (this.material) {
6454
- // @ts-ignore see createMaterial() - the engine mistypes occludeDirect as a number
6647
+ // @ts-ignore see _createMaterial() - the engine mistypes occludeDirect as a number
6455
6648
  this.material.occludeDirect = value;
6456
6649
  this._scheduleUpdate();
6457
6650
  }
@@ -6543,7 +6736,7 @@ class MaterialElement extends HTMLElement {
6543
6736
  */
6544
6737
  set opacityMap(value) {
6545
6738
  this._opacityMap = value;
6546
- this.setMap(value, 'opacityMap');
6739
+ this._setMap(value, 'opacityMap');
6547
6740
  }
6548
6741
  /**
6549
6742
  * Gets the id of the `pc-asset` used as the opacity map.
@@ -6861,6 +7054,13 @@ class MaterialElement extends HTMLElement {
6861
7054
  get useTonemap() {
6862
7055
  return this._useTonemap;
6863
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
+ */
6864
7064
  static get(id) {
6865
7065
  const materialElement = document.querySelector(`pc-material[id="${id}"]`);
6866
7066
  return materialElement?.material;
@@ -7499,7 +7699,18 @@ class RigidBodyComponentElement extends ComponentElement {
7499
7699
  return this._type;
7500
7700
  }
7501
7701
  static get observedAttributes() {
7502
- 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
+ ];
7503
7714
  }
7504
7715
  attributeChangedCallback(name, _oldValue, newValue) {
7505
7716
  super.attributeChangedCallback(name, _oldValue, newValue);
@@ -7804,13 +8015,7 @@ class ScrollbarComponentElement extends ComponentElement {
7804
8015
  return this._handle;
7805
8016
  }
7806
8017
  static get observedAttributes() {
7807
- return [
7808
- ...super.observedAttributes,
7809
- 'orientation',
7810
- 'value',
7811
- 'handle-size',
7812
- 'handle'
7813
- ];
8018
+ return [...super.observedAttributes, 'orientation', 'value', 'handle-size', 'handle'];
7814
8019
  }
7815
8020
  attributeChangedCallback(name, _oldValue, newValue) {
7816
8021
  super.attributeChangedCallback(name, _oldValue, newValue);
@@ -8036,7 +8241,8 @@ class ScrollViewComponentElement extends ComponentElement {
8036
8241
  set horizontalScrollbarVisibility(value) {
8037
8242
  this._horizontalScrollbarVisibility = value;
8038
8243
  if (this.component) {
8039
- 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;
8040
8246
  }
8041
8247
  }
8042
8248
  /**
@@ -8054,7 +8260,8 @@ class ScrollViewComponentElement extends ComponentElement {
8054
8260
  set verticalScrollbarVisibility(value) {
8055
8261
  this._verticalScrollbarVisibility = value;
8056
8262
  if (this.component) {
8057
- 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;
8058
8265
  }
8059
8266
  }
8060
8267
  /**
@@ -8241,15 +8448,9 @@ customElements.define('pc-scrollview', ScrollViewComponentElement);
8241
8448
  class ScriptElement extends AsyncElement {
8242
8449
  _attributes = {};
8243
8450
  _enabled = true;
8244
- /**
8245
- * Whether readiness has been signalled. Creation can happen more than once over an
8246
- * element's life (a runtime `name` change recreates the instance), but `ready` is a
8247
- * one-shot signal, so only the first successful creation fires it.
8248
- */
8249
- _readySignalled = false;
8250
8451
  /**
8251
8452
  * The Script instance created for this element by its parent `<pc-scripts>` element.
8252
- * @ignore
8453
+ * @internal
8253
8454
  */
8254
8455
  _script = null;
8255
8456
  /**
@@ -8331,14 +8532,20 @@ class ScriptElement extends AsyncElement {
8331
8532
  console.warn(`pc-script '${this.getAttribute('name')}' must be a direct child of pc-scripts - script not created`);
8332
8533
  }
8333
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
+ }
8334
8542
  /**
8335
8543
  * Called by the parent `<pc-scripts>` element when the script instance has been created.
8336
- * @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
8337
8547
  */
8338
8548
  _onScriptCreated() {
8339
- if (this._readySignalled)
8340
- return;
8341
- this._readySignalled = true;
8342
8549
  this._onReady();
8343
8550
  }
8344
8551
  static get observedAttributes() {
@@ -8383,10 +8590,34 @@ customElements.define('pc-script', ScriptElement);
8383
8590
  */
8384
8591
  const RESERVED_ATTRIBUTES = new Set([
8385
8592
  ...ScriptElement.observedAttributes,
8386
- 'accesskey', 'autocapitalize', 'autofocus', 'class', 'contenteditable', 'dir', 'draggable',
8387
- 'exportparts', 'hidden', 'id', 'inert', 'is', 'itemid', 'itemprop', 'itemref', 'itemscope',
8388
- 'itemtype', 'lang', 'nonce', 'part', 'popover', 'role', 'slot', 'spellcheck', 'style',
8389
- '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'
8390
8621
  ]);
8391
8622
  /**
8392
8623
  * Checks whether a `pc-script` attribute name is reserved (and so never maps to a script
@@ -8398,18 +8629,25 @@ const RESERVED_ATTRIBUTES = new Set([
8398
8629
  * @returns Whether the attribute name is reserved.
8399
8630
  */
8400
8631
  const isReservedAttribute = (name) => {
8401
- return RESERVED_ATTRIBUTES.has(name) ||
8632
+ return (RESERVED_ATTRIBUTES.has(name) ||
8402
8633
  name.startsWith('data-') ||
8403
8634
  name.startsWith('aria-') ||
8404
8635
  name.startsWith('_') ||
8405
- (name.startsWith('on') && name in HTMLElement.prototype);
8636
+ (name.startsWith('on') && name in HTMLElement.prototype));
8406
8637
  };
8407
8638
  /**
8408
8639
  * Script API members that per-property attributes must never overwrite: the engine bindings and
8409
8640
  * the (optional, so possibly undefined) lifecycle methods.
8410
8641
  */
8411
8642
  const SCRIPT_API_MEMBERS = new Set([
8412
- 'app', 'entity', 'destroy', 'initialize', 'postInitialize', 'postUpdate', 'swap', 'update'
8643
+ 'app',
8644
+ 'entity',
8645
+ 'destroy',
8646
+ 'initialize',
8647
+ 'postInitialize',
8648
+ 'postUpdate',
8649
+ 'swap',
8650
+ 'update'
8413
8651
  ]);
8414
8652
  /**
8415
8653
  * Converts a kebab-case attribute name to the camelCase script attribute name.
@@ -8425,7 +8663,7 @@ const kebabToCamel = (name) => {
8425
8663
  * @returns The kebab-case name.
8426
8664
  */
8427
8665
  const camelToKebab = (name) => {
8428
- return name.replace(/[A-Z]/g, char => `-${char.toLowerCase()}`);
8666
+ return name.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`);
8429
8667
  };
8430
8668
  /**
8431
8669
  * Resolves an `asset:` prefix to the Asset created by the `pc-asset` element with that id.
@@ -8624,7 +8862,10 @@ class ScriptComponentElement extends ComponentElement {
8624
8862
  // Only recurse into plain objects. Class instances (Vec3, Color, Asset, Entity...)
8625
8863
  // are leaf values assigned whole, so accessor-typed script attributes receive them
8626
8864
  // through their setters instead of having a getter's returned copy mutated.
8627
- 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) {
8628
8869
  if (!current || typeof current !== 'object') {
8629
8870
  target[key] = {};
8630
8871
  }
@@ -8642,7 +8883,11 @@ class ScriptComponentElement extends ComponentElement {
8642
8883
  * @returns Whether the value is a math type.
8643
8884
  */
8644
8885
  isMathType(value) {
8645
- 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);
8646
8891
  }
8647
8892
  /**
8648
8893
  * Converts a plain numeric array to the math type of `current`. A 3-element array targeting
@@ -8655,7 +8900,7 @@ class ScriptComponentElement extends ComponentElement {
8655
8900
  * @returns The converted value, or `null`.
8656
8901
  */
8657
8902
  arrayToMathType(current, value, key) {
8658
- if (value.every(component => typeof component === 'number' && Number.isFinite(component))) {
8903
+ if (value.every((component) => typeof component === 'number' && Number.isFinite(component))) {
8659
8904
  if (current instanceof playcanvas.Vec2 && value.length === 2)
8660
8905
  return new playcanvas.Vec2(value);
8661
8906
  if (current instanceof playcanvas.Vec3 && value.length === 3)
@@ -8927,7 +9172,10 @@ class ScriptComponentElement extends ComponentElement {
8927
9172
  mutation.removedNodes.forEach((node) => {
8928
9173
  if (node instanceof ScriptElement) {
8929
9174
  const scriptName = node.getAttribute('name');
8930
- 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) {
8931
9179
  this.destroyScript(scriptName);
8932
9180
  }
8933
9181
  node._script = null;
@@ -9176,18 +9424,27 @@ class SoundSlotElement extends AsyncElement {
9176
9424
  * emit a misleading "must be a direct child" warning for what is an ordinary removal.
9177
9425
  */
9178
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;
9179
9434
  /**
9180
9435
  * The sound slot.
9181
9436
  */
9182
9437
  soundSlot = null;
9183
9438
  async connectedCallback() {
9439
+ const generation = ++this._connectionGeneration;
9184
9440
  const soundElement = this.soundElement;
9185
9441
  await soundElement?.ready();
9186
- // The element may have been removed, or its parent torn down, while we were waiting. A
9187
- // <pc-app> disconnects before its children, so by the time we resume the component can
9188
- // 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.
9189
9446
  const component = soundElement?.component;
9190
- if (!this.isConnected || !component) {
9447
+ if (generation !== this._connectionGeneration || !component) {
9191
9448
  return;
9192
9449
  }
9193
9450
  const options = {
@@ -9210,12 +9467,15 @@ class SoundSlotElement extends AsyncElement {
9210
9467
  this._onReady();
9211
9468
  }
9212
9469
  disconnectedCallback() {
9470
+ // Invalidate any connectedCallback still suspended on an await
9471
+ this._connectionGeneration++;
9213
9472
  // Uses the cached parent rather than a fresh lookup, since parentElement is already null
9214
9473
  // by now. The component itself is null if the parent <pc-sound> (or the whole <pc-app>) is
9215
9474
  // being torn down — parents disconnect first and have already removed the component.
9216
9475
  this._soundElement?.component?.removeSlot(this._name);
9217
9476
  this._soundElement = null;
9218
9477
  this.soundSlot = null;
9478
+ this._resetReady();
9219
9479
  }
9220
9480
  get soundElement() {
9221
9481
  const soundElement = this.parentElement;
@@ -9615,39 +9875,85 @@ customElements.define('pc-gsplat', GSplatComponentElement);
9615
9875
  class ModelElement extends AsyncElement {
9616
9876
  _asset = '';
9617
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
+ }
9618
9899
  connectedCallback() {
9619
9900
  this._loadModel();
9620
9901
  this._onReady();
9621
9902
  }
9622
9903
  disconnectedCallback() {
9904
+ this._loadGeneration++;
9905
+ this._detachLoadHandler();
9623
9906
  this._unloadModel();
9907
+ this._resetReady();
9908
+ }
9909
+ _detachLoadHandler() {
9910
+ this._loadHandle?.off();
9911
+ this._loadHandle = null;
9624
9912
  }
9625
9913
  _instantiate(container) {
9626
- this._entity = container.instantiateRenderEntity();
9914
+ const generation = this._loadGeneration;
9915
+ const entity = container.instantiateRenderEntity();
9916
+ this._entity = entity;
9627
9917
  // @ts-ignore
9628
9918
  if (container.animations.length > 0) {
9629
- this._entity.addComponent('anim');
9919
+ entity.addComponent('anim');
9630
9920
  // @ts-ignore
9631
- this._entity.anim.assignAnimation('animation', container.animations[0].resource);
9921
+ entity.anim.assignAnimation('animation', container.animations[0].resource);
9632
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.
9633
9926
  const parentEntityElement = this.closestEntity;
9634
9927
  if (parentEntityElement) {
9635
9928
  parentEntityElement.ready().then(() => {
9636
- parentEntityElement.entity.addChild(this._entity);
9929
+ if (generation !== this._loadGeneration) {
9930
+ return;
9931
+ }
9932
+ parentEntityElement.entity.addChild(entity);
9637
9933
  });
9638
9934
  }
9639
9935
  else {
9640
9936
  const appElement = this.closestApp;
9641
9937
  if (appElement) {
9642
9938
  appElement.ready().then(() => {
9643
- appElement.app.root.addChild(this._entity);
9939
+ if (generation !== this._loadGeneration) {
9940
+ return;
9941
+ }
9942
+ appElement.app.root.addChild(entity);
9644
9943
  });
9645
9944
  }
9646
9945
  }
9647
9946
  }
9648
9947
  async _loadModel() {
9649
9948
  this._unloadModel();
9949
+ // Supersede any load already in flight - only the newest load may instantiate
9950
+ const generation = ++this._loadGeneration;
9951
+ this._detachLoadHandler();
9650
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
+ }
9651
9957
  const app = appElement?.app;
9652
9958
  const asset = AssetElement.get(this._asset);
9653
9959
  if (!asset) {
@@ -9657,7 +9963,14 @@ class ModelElement extends AsyncElement {
9657
9963
  this._instantiate(asset.resource);
9658
9964
  }
9659
9965
  else {
9660
- 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
+ }
9661
9974
  this._instantiate(asset.resource);
9662
9975
  });
9663
9976
  app.assets.load(asset);
@@ -9759,10 +10072,17 @@ class SceneElement extends AsyncElement {
9759
10072
  return;
9760
10073
  }
9761
10074
  this._scene = app.scene;
9762
- this.updateSceneSettings();
10075
+ this._updateSceneSettings();
9763
10076
  this._onReady();
9764
10077
  }
9765
- 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() {
9766
10086
  if (this._scene) {
9767
10087
  this._scene.fog.type = this._fog;
9768
10088
  this._scene.fog.color = this._fogColor;
@@ -9930,13 +10250,32 @@ class SkyElement extends AsyncElement {
9930
10250
  _type = 'infinite';
9931
10251
  _scene = null;
9932
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;
9933
10265
  connectedCallback() {
9934
10266
  this._loadSkybox();
9935
10267
  this._onReady();
9936
10268
  }
9937
10269
  disconnectedCallback() {
10270
+ this._loadGeneration++;
10271
+ this._detachLoadHandler();
9938
10272
  this._unloadSkybox();
9939
10273
  this._appElement = null;
10274
+ this._resetReady();
10275
+ }
10276
+ _detachLoadHandler() {
10277
+ this._loadHandle?.off();
10278
+ this._loadHandle = null;
9940
10279
  }
9941
10280
  _generateSkybox(asset) {
9942
10281
  if (!this._scene)
@@ -9944,10 +10283,17 @@ class SkyElement extends AsyncElement {
9944
10283
  const source = asset.resource;
9945
10284
  const skybox = playcanvas.EnvLighting.generateSkyboxCubemap(source);
9946
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();
9947
10289
  this._scene.skybox = skybox;
9948
10290
  if (this._lighting) {
9949
10291
  const lighting = playcanvas.EnvLighting.generateLightingSource(source);
9950
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();
9951
10297
  this._scene.envAtlas = envAtlas;
9952
10298
  }
9953
10299
  const layer = this._scene.layers.getLayerById(playcanvas.LAYERID_SKYBOX);
@@ -9961,7 +10307,14 @@ class SkyElement extends AsyncElement {
9961
10307
  this._scene.skyboxMip = this._mipLevel;
9962
10308
  }
9963
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();
9964
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
+ }
9965
10318
  const app = appElement?.app;
9966
10319
  if (!appElement || !app) {
9967
10320
  return;
@@ -9976,7 +10329,14 @@ class SkyElement extends AsyncElement {
9976
10329
  this._generateSkybox(asset);
9977
10330
  }
9978
10331
  else {
9979
- 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
+ }
9980
10340
  this._generateSkybox(asset);
9981
10341
  });
9982
10342
  app.assets.load(asset);