@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.mjs CHANGED
@@ -3,12 +3,14 @@ import { basisInitialize, WasmModule, Vec3, Color, Vec4, Quat, Vec2, createGraph
3
3
  /**
4
4
  * Base class for all PlayCanvas Web Components that initialize asynchronously.
5
5
  *
6
- * @fires {CustomEvent} ready - Fired once the element is fully initialized. Bubbles and is
7
- * composed.
6
+ * @fires {CustomEvent} ready - Fired when the element is fully initialized once per readiness
7
+ * cycle, so an element that is torn down and re-initialized (for example by removing and
8
+ * re-inserting it) fires it again. Bubbles and is composed.
8
9
  */
9
10
  class AsyncElement extends HTMLElement {
10
11
  _readyPromise;
11
12
  _readyResolve;
13
+ _readyResolved = false;
12
14
  /** @ignore */
13
15
  constructor() {
14
16
  super();
@@ -35,15 +37,39 @@ class AsyncElement extends HTMLElement {
35
37
  /**
36
38
  * Called when the element is fully initialized and ready. Subclasses should call this when
37
39
  * they're ready. Resolves the ready promise and dispatches a bubbling, composed `ready`
38
- * event.
40
+ * event. Signals at most once per readiness cycle: a repeat call before {@link _resetReady}
41
+ * has re-armed the promise does nothing.
39
42
  */
40
43
  _onReady() {
44
+ if (this._readyResolved)
45
+ return;
46
+ this._readyResolved = true;
41
47
  this._readyResolve();
42
48
  this.dispatchEvent(new CustomEvent('ready', { bubbles: true, composed: true }));
43
49
  }
50
+ /**
51
+ * Returns the ready promise to its pending state. Subclasses should call this when the
52
+ * resource their readiness announced is torn down (typically from `disconnectedCallback`),
53
+ * so that a later re-initialization can signal readiness again. Does nothing while the
54
+ * promise is still pending — an in-flight waiter carries over to the next readiness cycle
55
+ * rather than being stranded on a promise nothing will ever resolve.
56
+ */
57
+ _resetReady() {
58
+ if (!this._readyResolved)
59
+ return;
60
+ this._readyResolved = false;
61
+ this._readyPromise = new Promise((resolve) => {
62
+ this._readyResolve = resolve;
63
+ });
64
+ }
44
65
  /**
45
66
  * Returns a promise that resolves with this element when it's ready. This is the low-level
46
67
  * primitive underlying {@link whenReady}, which is the recommended way to wait for elements.
68
+ *
69
+ * Readiness tracks the element's current lifecycle: once a ready element is torn down (for
70
+ * example by removing it from the document), this returns a fresh promise that resolves when
71
+ * the element is next ready. A promise obtained earlier stays resolved — call this again
72
+ * after re-inserting an element rather than reusing a promise from before its removal.
47
73
  * @returns A promise that resolves with this element when it's ready.
48
74
  */
49
75
  ready() {
@@ -118,7 +144,14 @@ class ModuleElement extends HTMLElement {
118
144
  });
119
145
  }
120
146
  }
121
- getLoadPromise() {
147
+ /**
148
+ * Returns the promise that settles when the module has loaded. Awaited by the containing
149
+ * `<pc-app>` element before it creates its graphics device.
150
+ *
151
+ * @returns The load promise.
152
+ * @internal
153
+ */
154
+ _getLoadPromise() {
122
155
  return this.loadPromise;
123
156
  }
124
157
  }
@@ -177,10 +210,7 @@ class LoadingBar {
177
210
  // aria-valuenow is set, which is what marks a progressbar indeterminate. jsdom has no Web
178
211
  // Animations API, so the guard degrades to a static bar there rather than crashing boot.
179
212
  if (typeof this._fill.animate === 'function') {
180
- this._sweep = this._fill.animate([
181
- { transform: 'scaleX(0.25) translateX(-100%)' },
182
- { transform: 'scaleX(0.25) translateX(500%)' }
183
- ], {
213
+ this._sweep = this._fill.animate([{ transform: 'scaleX(0.25) translateX(-100%)' }, { transform: 'scaleX(0.25) translateX(500%)' }], {
184
214
  duration: 1000,
185
215
  iterations: Infinity,
186
216
  easing: 'ease-in-out'
@@ -413,7 +443,7 @@ const CSS_COLORS = {
413
443
  */
414
444
  const parseComponents = (value, count) => {
415
445
  const components = value.trim().split(/\s+/).map(Number);
416
- if (components.length !== count || components.some(component => !Number.isFinite(component))) {
446
+ if (components.length !== count || components.some((component) => !Number.isFinite(component))) {
417
447
  return null;
418
448
  }
419
449
  return components;
@@ -469,7 +499,10 @@ const parseColor = (value, defaultValue, attribute) => {
469
499
  if (/^#(?:[0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(value)) {
470
500
  let hex = value.slice(1);
471
501
  if (hex.length === 3 || hex.length === 4) {
472
- hex = hex.split('').map(char => char + char).join('');
502
+ hex = hex
503
+ .split('')
504
+ .map((char) => char + char)
505
+ .join('');
473
506
  }
474
507
  return new Color().fromString(`#${hex}`);
475
508
  }
@@ -566,7 +599,10 @@ const parseTags = (value, defaultValue = []) => {
566
599
  // caller's default, or a later mutation would write back through it.
567
600
  return [...defaultValue];
568
601
  }
569
- return value.split(',').map(tag => tag.trim()).filter(tag => tag !== '');
602
+ return value
603
+ .split(',')
604
+ .map((tag) => tag.trim())
605
+ .filter((tag) => tag !== '');
570
606
  };
571
607
  /**
572
608
  * Parse a Vec2 attribute value. The expected format is 2 space-separated numbers (e.g. '1 2').
@@ -658,6 +694,8 @@ const getEntity = (ref) => {
658
694
  return element?.entity ?? null;
659
695
  };
660
696
 
697
+ /** The pointer event types the application synthesizes on `<pc-entity>` elements via picking. */
698
+ const pointerEventTypes = ['pointermove', 'pointerdown', 'pointerup', 'pointerenter', 'pointerleave'];
661
699
  /**
662
700
  * The AppElement interface provides properties and methods for manipulating
663
701
  * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-app/ | `<pc-app>`} elements.
@@ -688,7 +726,20 @@ class AppElement extends AsyncElement {
688
726
  */
689
727
  _optionsLocked = false;
690
728
  _bar = null;
729
+ /**
730
+ * Whether the application has created its initial entity hierarchy. Read by EntityElement to
731
+ * decide whether a newly connected element must create its entity itself or leave it to the
732
+ * boot sweep.
733
+ * @internal
734
+ */
691
735
  _hierarchyReady = false;
736
+ /**
737
+ * Incremented on every connect and disconnect. Boot captures the value on entry and abandons
738
+ * itself wherever it resumes from an await if the value has moved on — so a boot whose
739
+ * element was removed cannot complete against a torn-down element, and a boot whose element
740
+ * was removed and re-inserted (which starts a boot of its own) cannot race the newer one.
741
+ */
742
+ _bootGeneration = 0;
692
743
  /**
693
744
  * The elements backing this application's entities, keyed by the entity itself. Registered
694
745
  * by EntityElement at creation and removed when an entity is destroyed, this joins engine
@@ -741,8 +792,16 @@ class AppElement extends AsyncElement {
741
792
  super();
742
793
  // Bind methods to maintain 'this' context
743
794
  this._onWindowResize = this._onWindowResize.bind(this);
795
+ // Track pointer listeners being added to and removed from descendant entities.
796
+ // Registered once here rather than on every boot - the handlers no-op while there is no
797
+ // canvas, and a re-booted element must not stack a second set.
798
+ pointerEventTypes.forEach((type) => {
799
+ this.addEventListener(`${type}:connect`, () => this._onPointerListenerAdded(type));
800
+ this.addEventListener(`${type}:disconnect`, () => this._onPointerListenerRemoved(type));
801
+ });
744
802
  }
745
803
  async connectedCallback() {
804
+ const generation = ++this._bootGeneration;
746
805
  // Created before the first await, so the bar is visible while modules and the graphics
747
806
  // device are created, and exists before any disconnect could need to clean it up
748
807
  if (this._loadingBar && !this._bar) {
@@ -751,7 +810,12 @@ class AppElement extends AsyncElement {
751
810
  // Get all pc-module elements that are direct children of the pc-app element
752
811
  const moduleElements = this.querySelectorAll(':scope > pc-module');
753
812
  // Wait for all modules to load
754
- await Promise.all(Array.from(moduleElements).map(module => module.getLoadPromise()));
813
+ await Promise.all(Array.from(moduleElements).map((module) => module._getLoadPromise()));
814
+ // The element may have been removed while the modules loaded. Nothing beyond the loading
815
+ // bar exists yet, and disconnectedCallback has already destroyed that.
816
+ if (generation !== this._bootGeneration) {
817
+ return;
818
+ }
755
819
  // Create and append the canvas to the element
756
820
  this._canvas = document.createElement('canvas');
757
821
  this.appendChild(this._canvas);
@@ -771,6 +835,13 @@ class AppElement extends AsyncElement {
771
835
  deviceTypes: deviceTypes,
772
836
  stencil: this._stencilBuffer
773
837
  });
838
+ // The element may have been removed while the device was created. disconnectedCallback
839
+ // has already cleaned up the canvas; the device was created inside the await, so it is
840
+ // this boot's to release.
841
+ if (generation !== this._bootGeneration) {
842
+ device.destroy();
843
+ return;
844
+ }
774
845
  // Assigned rather than resolved to a number here: the engine caps against the live
775
846
  // window.devicePixelRatio on every resize, so an uncapped Infinity keeps following the
776
847
  // display when a window moves between monitors of differing density.
@@ -847,27 +918,41 @@ class AppElement extends AsyncElement {
847
918
  this._pickerCreate();
848
919
  // Get all pc-asset elements that are direct children of the pc-app element
849
920
  const assetElements = this.querySelectorAll(':scope > pc-asset');
850
- Array.from(assetElements).forEach((assetElement) => {
851
- assetElement.createAsset();
921
+ for (const assetElement of Array.from(assetElements)) {
922
+ assetElement._createAsset();
852
923
  const asset = assetElement.asset;
853
924
  if (asset) {
854
925
  app.assets.add(asset);
926
+ // Adding a fileless asset (one built purely from data, such as a sprite)
927
+ // completes it synchronously, dispatching the element's load event - whose
928
+ // listeners may have removed this element. Stop before the next addition
929
+ // reaches the destroyed registry, and before orphan entities are created.
930
+ if (generation !== this._bootGeneration) {
931
+ return;
932
+ }
855
933
  }
856
- });
934
+ }
857
935
  // Get all pc-material elements that are direct children of the pc-app element
858
936
  const materialElements = this.querySelectorAll(':scope > pc-material');
859
937
  Array.from(materialElements).forEach((materialElement) => {
860
- materialElement.createMaterial();
938
+ materialElement._createMaterial();
861
939
  });
862
940
  // Create all entities
863
941
  const entityElements = this.querySelectorAll('pc-entity');
864
942
  Array.from(entityElements).forEach((entityElement) => {
865
- entityElement.createEntity(app);
943
+ entityElement._createEntity(app);
866
944
  });
867
945
  // Build hierarchy
868
946
  entityElements.forEach((entityElement) => {
869
- entityElement.buildHierarchy(app);
947
+ entityElement._buildHierarchy(app);
870
948
  });
949
+ // Building the hierarchy dispatched each entity's ready event synchronously, and a
950
+ // listener may have removed the element. The sweep itself degrades safely - destroying
951
+ // the application nulls every element's entity, so the remaining builds no-op - but the
952
+ // teardown's reset must not be overwritten here.
953
+ if (generation !== this._bootGeneration) {
954
+ return;
955
+ }
871
956
  this._hierarchyReady = true;
872
957
  // Forward the engine's preload lifecycle as DOM ProgressEvents on this element. The
873
958
  // listener must be attached before preload() is called: an asset that is already loaded
@@ -884,8 +969,19 @@ class AppElement extends AsyncElement {
884
969
  this._loadProgress = total === 0 ? 1 : 0;
885
970
  this._bar?.progress(0, total);
886
971
  this.dispatchEvent(new ProgressEvent('progress', { lengthComputable: true, loaded: 0, total }));
972
+ // The progress dispatch above ran listeners synchronously, and one may have removed the
973
+ // element. The application is already destroyed - it must not be asked to preload.
974
+ if (generation !== this._bootGeneration) {
975
+ return;
976
+ }
887
977
  // Load assets before starting the application
888
978
  app.preload(() => {
979
+ // The element may have been removed while assets loaded. The application is already
980
+ // destroyed, so it must not be started — and readiness must not be signaled for a
981
+ // boot that no longer owns the element.
982
+ if (generation !== this._bootGeneration) {
983
+ return;
984
+ }
889
985
  // Scope the counter to this preload pass, so a later app.preload() call by user code
890
986
  // cannot push `loaded` past `total`
891
987
  app.off('preload:progress', onPreloadProgress);
@@ -901,6 +997,9 @@ class AppElement extends AsyncElement {
901
997
  });
902
998
  }
903
999
  disconnectedCallback() {
1000
+ // Invalidate any boot still in flight, so it abandons itself when it next resumes
1001
+ // instead of completing against a torn-down element.
1002
+ this._bootGeneration++;
904
1003
  this._optionsLocked = false;
905
1004
  this._pickerDestroy();
906
1005
  // Clean up the application. Destroying it destroys every entity, whose destroy hooks
@@ -913,6 +1012,11 @@ class AppElement extends AsyncElement {
913
1012
  this._loadProgress = 0;
914
1013
  this._bar?.destroy();
915
1014
  this._bar = null;
1015
+ // Return the element to its pre-boot state, so re-inserting it boots afresh: descendants
1016
+ // must neither see a hierarchy that no longer exists nor resume against a readiness that
1017
+ // no longer holds.
1018
+ this._hierarchyReady = false;
1019
+ this._resetReady();
916
1020
  // Remove event listeners
917
1021
  window.removeEventListener('resize', this._onWindowResize);
918
1022
  // Remove the canvas
@@ -940,14 +1044,11 @@ class AppElement extends AsyncElement {
940
1044
  this._pointerHandlers.pointermove = listener(this._onPointerMove);
941
1045
  this._pointerHandlers.pointerdown = listener(this._onPointerDown);
942
1046
  this._pointerHandlers.pointerup = listener(this._onPointerUp);
943
- // Listen for pointer listeners being added/removed
944
- ['pointermove', 'pointerdown', 'pointerup', 'pointerenter', 'pointerleave'].forEach((type) => {
945
- this.addEventListener(`${type}:connect`, () => this._onPointerListenerAdded(type));
946
- this.addEventListener(`${type}:disconnect`, () => this._onPointerListenerRemoved(type));
947
- // Attach canvas handlers for listeners registered before this point (e.g. handlers
948
- // created from onpointer* attributes when their elements were first upgraded)
949
- const anyListeners = Array.from(this.querySelectorAll('pc-entity'))
950
- .some(entity => entity.hasListeners(type));
1047
+ // Attach canvas handlers for listeners registered before this boot (e.g. handlers
1048
+ // created from onpointer* attributes when their elements were first upgraded, or
1049
+ // listeners carried over from before a re-boot)
1050
+ pointerEventTypes.forEach((type) => {
1051
+ const anyListeners = Array.from(this.querySelectorAll('pc-entity')).some((entity) => entity._hasListeners(type));
951
1052
  if (anyListeners) {
952
1053
  this._onPointerListenerAdded(type);
953
1054
  }
@@ -962,6 +1063,7 @@ class AppElement extends AsyncElement {
962
1063
  });
963
1064
  }
964
1065
  this._picker = null;
1066
+ this._hoveredEntity = null;
965
1067
  this._pointerHandlers = {
966
1068
  pointermove: null,
967
1069
  pointerdown: null,
@@ -981,7 +1083,7 @@ class AppElement extends AsyncElement {
981
1083
  *
982
1084
  * @param entity - The entity.
983
1085
  * @param element - The element that created it.
984
- * @ignore
1086
+ * @internal
985
1087
  */
986
1088
  _registerEntityElement(entity, element) {
987
1089
  this._entityElements.set(entity, element);
@@ -990,7 +1092,7 @@ class AppElement extends AsyncElement {
990
1092
  * Removes the registration for a destroyed entity. Called by EntityElement.
991
1093
  *
992
1094
  * @param entity - The entity.
993
- * @ignore
1095
+ * @internal
994
1096
  */
995
1097
  _unregisterEntityElement(entity) {
996
1098
  this._entityElements.delete(entity);
@@ -1036,7 +1138,7 @@ class AppElement extends AsyncElement {
1036
1138
  _elementWithListener(node, type) {
1037
1139
  while (node !== null) {
1038
1140
  const element = this._entityElements.get(node);
1039
- if (element?.hasListeners(type)) {
1141
+ if (element?._hasListeners(type)) {
1040
1142
  return element;
1041
1143
  }
1042
1144
  node = node.parent;
@@ -1093,17 +1195,17 @@ class AppElement extends AsyncElement {
1093
1195
  const newHoverEntity = this._elementFromNode(node);
1094
1196
  // Handle enter/leave events
1095
1197
  if (this._hoveredEntity !== newHoverEntity) {
1096
- if (this._hoveredEntity && this._hoveredEntity.hasListeners('pointerleave')) {
1198
+ if (this._hoveredEntity && this._hoveredEntity._hasListeners('pointerleave')) {
1097
1199
  this._hoveredEntity.dispatchEvent(new PointerEvent('pointerleave', event));
1098
1200
  }
1099
- if (newHoverEntity && newHoverEntity.hasListeners('pointerenter')) {
1201
+ if (newHoverEntity && newHoverEntity._hasListeners('pointerenter')) {
1100
1202
  newHoverEntity.dispatchEvent(new PointerEvent('pointerenter', event));
1101
1203
  }
1102
1204
  }
1103
1205
  // Update hover state
1104
1206
  this._hoveredEntity = newHoverEntity;
1105
1207
  // Handle pointermove event
1106
- if (newHoverEntity && newHoverEntity.hasListeners('pointermove')) {
1208
+ if (newHoverEntity && newHoverEntity._hasListeners('pointermove')) {
1107
1209
  newHoverEntity.dispatchEvent(new PointerEvent('pointermove', event));
1108
1210
  }
1109
1211
  }
@@ -1133,22 +1235,21 @@ class AppElement extends AsyncElement {
1133
1235
  if (!this._hasPointerListeners[type] && this._canvas) {
1134
1236
  this._hasPointerListeners[type] = true;
1135
1237
  // For enter/leave events, we need the move handler
1136
- const handler = (type === 'pointerenter' || type === 'pointerleave') ?
1137
- this._pointerHandlers.pointermove :
1138
- this._pointerHandlers[type];
1238
+ const handler = type === 'pointerenter' || type === 'pointerleave'
1239
+ ? this._pointerHandlers.pointermove
1240
+ : this._pointerHandlers[type];
1139
1241
  if (handler) {
1140
1242
  this._canvas.addEventListener(type === 'pointerenter' || type === 'pointerleave' ? 'pointermove' : type, handler);
1141
1243
  }
1142
1244
  }
1143
1245
  }
1144
1246
  _onPointerListenerRemoved(type) {
1145
- const hasListeners = Array.from(this.querySelectorAll('pc-entity'))
1146
- .some(entity => entity.hasListeners(type));
1247
+ const hasListeners = Array.from(this.querySelectorAll('pc-entity')).some((entity) => entity._hasListeners(type));
1147
1248
  if (!hasListeners && this._canvas) {
1148
1249
  this._hasPointerListeners[type] = false;
1149
- const handler = (type === 'pointerenter' || type === 'pointerleave') ?
1150
- this._pointerHandlers.pointermove :
1151
- this._pointerHandlers[type];
1250
+ const handler = type === 'pointerenter' || type === 'pointerleave'
1251
+ ? this._pointerHandlers.pointermove
1252
+ : this._pointerHandlers[type];
1152
1253
  if (handler) {
1153
1254
  this._canvas.removeEventListener(type === 'pointerenter' || type === 'pointerleave' ? 'pointermove' : type, handler);
1154
1255
  }
@@ -1229,14 +1330,6 @@ class AppElement extends AsyncElement {
1229
1330
  get depthBuffer() {
1230
1331
  return this._depthBuffer;
1231
1332
  }
1232
- /**
1233
- * Gets the hierarchy ready flag.
1234
- * @returns The hierarchy ready flag.
1235
- * @ignore
1236
- */
1237
- get hierarchyReady() {
1238
- return this._hierarchyReady;
1239
- }
1240
1333
  /**
1241
1334
  * Sets whether the application shows its built-in loading bar while it boots and preloads its
1242
1335
  * assets. Enabled by default; setting `false` removes the bar immediately, while setting
@@ -1405,7 +1498,14 @@ class EntityElement extends AsyncElement {
1405
1498
  get entity() {
1406
1499
  return this._entity;
1407
1500
  }
1408
- createEntity(app) {
1501
+ /**
1502
+ * Creates the backing entity. Called by the containing `<pc-app>` element during its boot
1503
+ * sweep, and on connection for elements inserted while the application is already running.
1504
+ *
1505
+ * @param app - The application to create the entity in.
1506
+ * @internal
1507
+ */
1508
+ _createEntity(app) {
1409
1509
  // Guard against double creation. When a subtree is inserted at runtime (e.g. cloning a
1410
1510
  // `<template>`), an ancestor's connectedCallback eagerly creates descendant entities; the
1411
1511
  // descendants' own connectedCallbacks would otherwise create them a second time.
@@ -1436,8 +1536,10 @@ class EntityElement extends AsyncElement {
1436
1536
  }
1437
1537
  /**
1438
1538
  * Handles the destruction of the backing entity. Resets the element so a later re-insertion
1439
- * starts clean: `_built` must be cleared alongside `_entity`, or buildHierarchy would bail
1440
- * and a re-created entity would never be parented.
1539
+ * starts clean: `_built` must be cleared alongside `_entity`, or _buildHierarchy would bail
1540
+ * and a re-created entity would never be parented. Readiness is re-armed for the same
1541
+ * reason — with the entity gone, a resolved ready promise would resume its awaiters against
1542
+ * a null `entity`.
1441
1543
  *
1442
1544
  * @param entity - The entity that was destroyed.
1443
1545
  */
@@ -1446,8 +1548,18 @@ class EntityElement extends AsyncElement {
1446
1548
  this._appElement = null;
1447
1549
  this._entity = null;
1448
1550
  this._built = false;
1551
+ this._resetReady();
1449
1552
  }
1450
- buildHierarchy(app) {
1553
+ /**
1554
+ * Parents the backing entity: under the entity of the nearest ancestor `<pc-entity>` when
1555
+ * there is one, and under the application root otherwise. Called by the containing `<pc-app>`
1556
+ * element once a sweep has created every entity, so a parent's existence never depends on
1557
+ * document order.
1558
+ *
1559
+ * @param app - The application whose root adopts parentless entities.
1560
+ * @internal
1561
+ */
1562
+ _buildHierarchy(app) {
1451
1563
  if (!this.entity || this._built)
1452
1564
  return;
1453
1565
  this._built = true;
@@ -1473,17 +1585,17 @@ class EntityElement extends AsyncElement {
1473
1585
  return;
1474
1586
  }
1475
1587
  // If app is already running, create entity immediately
1476
- if (closestApp.hierarchyReady) {
1588
+ if (closestApp._hierarchyReady) {
1477
1589
  const app = closestApp.app;
1478
- this.createEntity(app);
1479
- this.buildHierarchy(app);
1590
+ this._createEntity(app);
1591
+ this._buildHierarchy(app);
1480
1592
  // Handle any child entities that might exist
1481
1593
  const childEntities = this.querySelectorAll('pc-entity');
1482
1594
  childEntities.forEach((child) => {
1483
- child.createEntity(app);
1595
+ child._createEntity(app);
1484
1596
  });
1485
1597
  childEntities.forEach((child) => {
1486
- child.buildHierarchy(app);
1598
+ child._buildHierarchy(app);
1487
1599
  });
1488
1600
  }
1489
1601
  }
@@ -1677,14 +1789,23 @@ class EntityElement extends AsyncElement {
1677
1789
  }
1678
1790
  removeEventListener(type, listener, options) {
1679
1791
  if (this._listeners[type]) {
1680
- this._listeners[type] = this._listeners[type].filter(l => l !== listener);
1792
+ this._listeners[type] = this._listeners[type].filter((l) => l !== listener);
1681
1793
  }
1682
1794
  super.removeEventListener(type, listener, options);
1683
1795
  if (type.startsWith('pointer')) {
1684
1796
  this.dispatchEvent(new CustomEvent(`${type}:disconnect`, { bubbles: true }));
1685
1797
  }
1686
1798
  }
1687
- hasListeners(type) {
1799
+ /**
1800
+ * Whether the element has a listener for an event type, registered either with
1801
+ * {@link addEventListener} or with the matching inline `onpointer*` attribute. Read by the
1802
+ * containing `<pc-app>` element to gate pointer event synthesis.
1803
+ *
1804
+ * @param type - The event type.
1805
+ * @returns Whether a listener is registered.
1806
+ * @internal
1807
+ */
1808
+ _hasListeners(type) {
1688
1809
  return Boolean(this._listeners[type]?.length) || this._inlineHandlerTypes.has(type);
1689
1810
  }
1690
1811
  }
@@ -1975,7 +2096,7 @@ class AssetElement extends AsyncElement {
1975
2096
  const app = appElement.app;
1976
2097
  if (!app)
1977
2098
  return; // pc-app is re-connecting; its own boot will create this asset
1978
- this.createAsset();
2099
+ this._createAsset();
1979
2100
  if (this.asset) {
1980
2101
  app.assets.add(this.asset); // add() auto-loads when preload is true
1981
2102
  if (!this.lazy) {
@@ -1983,13 +2104,15 @@ class AssetElement extends AsyncElement {
1983
2104
  }
1984
2105
  }
1985
2106
  }
1986
- // Never ready if createAsset failed (unsupported asset type)
2107
+ // Never ready if _createAsset failed (unsupported asset type)
1987
2108
  if (this.asset) {
1988
2109
  this._onReady();
1989
2110
  }
1990
2111
  }
1991
2112
  disconnectedCallback() {
1992
- this.destroyAsset();
2113
+ this._destroyAsset();
2114
+ // Re-arm readiness so a re-inserted element announces the asset it creates then
2115
+ this._resetReady();
1993
2116
  }
1994
2117
  _onAssetLoad() {
1995
2118
  this.dispatchEvent(new Event('load'));
@@ -1999,7 +2122,14 @@ class AssetElement extends AsyncElement {
1999
2122
  message: err instanceof Error ? err.message : String(err)
2000
2123
  }));
2001
2124
  }
2002
- createAsset() {
2125
+ /**
2126
+ * Creates the asset from the element's attributes. Called by the containing `<pc-app>`
2127
+ * element during its boot sweep, and on connection for elements inserted while the
2128
+ * application is already running.
2129
+ *
2130
+ * @internal
2131
+ */
2132
+ _createAsset() {
2003
2133
  const id = this.getAttribute('id') || '';
2004
2134
  const src = this.getAttribute('src') || '';
2005
2135
  let type = this.getAttribute('type');
@@ -2089,7 +2219,7 @@ class AssetElement extends AsyncElement {
2089
2219
  }
2090
2220
  return data;
2091
2221
  }
2092
- destroyAsset() {
2222
+ _destroyAsset() {
2093
2223
  if (this.asset) {
2094
2224
  // A caller that keeps the Asset alive must not dispatch on a removed element
2095
2225
  this.asset.off('load', this._onAssetLoad, this);
@@ -2117,6 +2247,13 @@ class AssetElement extends AsyncElement {
2117
2247
  get lazy() {
2118
2248
  return this._lazy;
2119
2249
  }
2250
+ /**
2251
+ * Returns the {@link Asset} created by the `<pc-asset>` element with the given `id`, or
2252
+ * `undefined` if there is no such element or its asset has not been created yet.
2253
+ *
2254
+ * @param id - The `id` of the `<pc-asset>` element.
2255
+ * @returns The asset, or `undefined`.
2256
+ */
2120
2257
  static get(id) {
2121
2258
  const assetElement = document.querySelector(`pc-asset[id="${id}"]`);
2122
2259
  return assetElement?.asset;
@@ -2142,6 +2279,14 @@ class ComponentElement extends AsyncElement {
2142
2279
  _enabled = true;
2143
2280
  _component = null;
2144
2281
  _appElement = null;
2282
+ /**
2283
+ * Incremented on every connect and disconnect. connectedCallback captures the value on entry
2284
+ * and abandons itself wherever it resumes from an await if the value has moved on — so a
2285
+ * callback whose element was removed cannot act on a torn-down tree, and one whose element
2286
+ * was removed and re-inserted (which runs a callback of its own) cannot add the component a
2287
+ * second time.
2288
+ */
2289
+ _connectionGeneration = 0;
2145
2290
  /**
2146
2291
  * Creates a new ComponentElement instance.
2147
2292
  *
@@ -2152,11 +2297,17 @@ class ComponentElement extends AsyncElement {
2152
2297
  super();
2153
2298
  this._componentName = componentName;
2154
2299
  }
2155
- // Method to be overridden by subclasses to provide initial component data
2300
+ /**
2301
+ * Returns the data the component is created with. Overridden by subclasses to supply the
2302
+ * initial values of their cached properties.
2303
+ *
2304
+ * @returns The initial component data.
2305
+ */
2156
2306
  getInitialComponentData() {
2157
2307
  return {};
2158
2308
  }
2159
- async addComponent() {
2309
+ async _addComponent() {
2310
+ const generation = this._connectionGeneration;
2160
2311
  const entityElement = this.closestEntity;
2161
2312
  if (!entityElement) {
2162
2313
  // A component can only exist on an entity, so an element placed outside one is inert.
@@ -2166,19 +2317,42 @@ class ComponentElement extends AsyncElement {
2166
2317
  return;
2167
2318
  }
2168
2319
  await entityElement.ready();
2320
+ // The element may have been removed, or removed and re-inserted, while the entity became
2321
+ // ready — the component belongs to the connection that owns the current generation.
2322
+ if (generation !== this._connectionGeneration) {
2323
+ return;
2324
+ }
2169
2325
  // Add the component to the entity
2170
2326
  const data = this.getInitialComponentData();
2171
2327
  this._component = entityElement.entity.addComponent(this._componentName, data);
2172
2328
  }
2173
- initComponent() { }
2329
+ /**
2330
+ * Configures the newly added component. Overridden by subclasses whose setup goes beyond
2331
+ * the initial data — child-element handling, asset resolution and the like.
2332
+ */
2333
+ initComponent() {
2334
+ // optional hook
2335
+ }
2174
2336
  async connectedCallback() {
2337
+ const generation = ++this._connectionGeneration;
2175
2338
  this._appElement = this.closestApp ?? null;
2176
2339
  await this._appElement?.ready();
2177
- await this.addComponent();
2340
+ // The element may have been removed, or removed and re-inserted, while the application
2341
+ // became ready. A re-insertion runs a connectedCallback of its own, so a stale resume
2342
+ // must not add the component alongside it.
2343
+ if (generation !== this._connectionGeneration) {
2344
+ return;
2345
+ }
2346
+ await this._addComponent();
2347
+ if (generation !== this._connectionGeneration) {
2348
+ return;
2349
+ }
2178
2350
  this.initComponent();
2179
2351
  this._onReady();
2180
2352
  }
2181
2353
  disconnectedCallback() {
2354
+ // Invalidate any connectedCallback still suspended on an await
2355
+ this._connectionGeneration++;
2182
2356
  // Remove the component when the element is disconnected. Skip this when the owning
2183
2357
  // application has already been destroyed — removing a <pc-app> disconnects it before
2184
2358
  // its children, taking the component systems with it.
@@ -2187,6 +2361,7 @@ class ComponentElement extends AsyncElement {
2187
2361
  }
2188
2362
  this._component = null;
2189
2363
  this._appElement = null;
2364
+ this._resetReady();
2190
2365
  }
2191
2366
  /**
2192
2367
  * The PlayCanvas component instance. `null` until the element is ready, and also for an
@@ -3244,7 +3419,17 @@ class CollisionComponentElement extends ComponentElement {
3244
3419
  return this._type;
3245
3420
  }
3246
3421
  static get observedAttributes() {
3247
- return [...super.observedAttributes, 'angular-offset', 'axis', 'convex-hull', 'half-extents', 'height', 'linear-offset', 'radius', 'type'];
3422
+ return [
3423
+ ...super.observedAttributes,
3424
+ 'angular-offset',
3425
+ 'axis',
3426
+ 'convex-hull',
3427
+ 'half-extents',
3428
+ 'height',
3429
+ 'linear-offset',
3430
+ 'radius',
3431
+ 'type'
3432
+ ];
3248
3433
  }
3249
3434
  attributeChangedCallback(name, _oldValue, newValue) {
3250
3435
  super.attributeChangedCallback(name, _oldValue, newValue);
@@ -4949,7 +5134,7 @@ class ParticleSystemComponentElement extends ComponentElement {
4949
5134
  }
4950
5135
  // Set all the config properties on the component
4951
5136
  for (const key in resource) {
4952
- if (resource.hasOwnProperty(key)) {
5137
+ if (Object.hasOwn(resource, key)) {
4953
5138
  this.component[key] = resource[key];
4954
5139
  }
4955
5140
  }
@@ -5022,10 +5207,7 @@ class ParticleSystemComponentElement extends ComponentElement {
5022
5207
  }
5023
5208
  }
5024
5209
  static get observedAttributes() {
5025
- return [
5026
- ...super.observedAttributes,
5027
- 'asset'
5028
- ];
5210
+ return [...super.observedAttributes, 'asset'];
5029
5211
  }
5030
5212
  attributeChangedCallback(name, _oldValue, newValue) {
5031
5213
  super.attributeChangedCallback(name, _oldValue, newValue);
@@ -5185,7 +5367,7 @@ class MaterialElement extends HTMLElement {
5185
5367
  _twoSidedLighting = false;
5186
5368
  _useFog = true;
5187
5369
  _useLighting = true;
5188
- // Diverges from the engine default of false - see the class docblock and createMaterial()
5370
+ // Diverges from the engine default of false - see the class docblock and _createMaterial()
5189
5371
  _useMetalness = true;
5190
5372
  _useMetalnessSpecularColor = false;
5191
5373
  _useSkybox = true;
@@ -5198,6 +5380,10 @@ class MaterialElement extends HTMLElement {
5198
5380
  _mapHandles = new Map();
5199
5381
  _updateScheduled = false;
5200
5382
  _glossConflictWarned = false;
5383
+ /**
5384
+ * The material. `null` until the containing application has created it — an element present
5385
+ * at startup has its material once the application is ready.
5386
+ */
5201
5387
  material = null;
5202
5388
  async connectedCallback() {
5203
5389
  const appElement = this.parentElement?.closest('pc-app') ?? null;
@@ -5215,10 +5401,17 @@ class MaterialElement extends HTMLElement {
5215
5401
  if (!this.material) {
5216
5402
  if (!appElement.app)
5217
5403
  return; // pc-app is re-connecting; its own boot will create this
5218
- this.createMaterial();
5404
+ this._createMaterial();
5219
5405
  }
5220
5406
  }
5221
- createMaterial() {
5407
+ /**
5408
+ * Creates the material from the element's cached properties. Called by the containing
5409
+ * `<pc-app>` element during its boot sweep, and on connection for elements inserted while
5410
+ * the application is already running.
5411
+ *
5412
+ * @internal
5413
+ */
5414
+ _createMaterial() {
5222
5415
  const material = new StandardMaterial();
5223
5416
  this.material = material;
5224
5417
  material.alphaTest = this._alphaTest;
@@ -5345,9 +5538,9 @@ class MaterialElement extends HTMLElement {
5345
5538
  * warning latches and reports once per episode, clearing when the clash is resolved.
5346
5539
  */
5347
5540
  _warnGlossConflict() {
5348
- const quote = (names) => `'${names.join('\', \'')}'`;
5349
- const roughness = roughnessAliases.filter(name => this.hasAttribute(name));
5350
- const gloss = glossConflicts.filter(name => this.hasAttribute(name));
5541
+ const quote = (names) => `'${names.join("', '")}'`;
5542
+ const roughness = roughnessAliases.filter((name) => this.hasAttribute(name));
5543
+ const gloss = glossConflicts.filter((name) => this.hasAttribute(name));
5351
5544
  if (roughness.length === 0 || gloss.length === 0) {
5352
5545
  this._glossConflictWarned = false;
5353
5546
  return;
@@ -5365,7 +5558,7 @@ class MaterialElement extends HTMLElement {
5365
5558
  * @param id - The id of the `pc-asset`, or an empty string to clear the slot.
5366
5559
  * @param slot - The material property to write.
5367
5560
  */
5368
- setMap(id, slot) {
5561
+ _setMap(id, slot) {
5369
5562
  // Drop any load still pending for this slot - its texture is no longer the one we want
5370
5563
  this._mapHandles.get(slot)?.off();
5371
5564
  this._mapHandles.delete(slot);
@@ -5459,7 +5652,7 @@ class MaterialElement extends HTMLElement {
5459
5652
  */
5460
5653
  set aoMap(value) {
5461
5654
  this._aoMap = value;
5462
- this.setMap(value, 'aoMap');
5655
+ this._setMap(value, 'aoMap');
5463
5656
  }
5464
5657
  /**
5465
5658
  * Gets the id of the `pc-asset` used as the ambient occlusion map.
@@ -5691,7 +5884,7 @@ class MaterialElement extends HTMLElement {
5691
5884
  */
5692
5885
  set diffuseMap(value) {
5693
5886
  this._diffuseMap = value;
5694
- this.setMap(value, 'diffuseMap');
5887
+ this._setMap(value, 'diffuseMap');
5695
5888
  }
5696
5889
  /**
5697
5890
  * Gets the id of the `pc-asset` used as the diffuse map.
@@ -5832,7 +6025,7 @@ class MaterialElement extends HTMLElement {
5832
6025
  */
5833
6026
  set emissiveMap(value) {
5834
6027
  this._emissiveMap = value;
5835
- this.setMap(value, 'emissiveMap');
6028
+ this._setMap(value, 'emissiveMap');
5836
6029
  }
5837
6030
  /**
5838
6031
  * Gets the id of the `pc-asset` used as the emissive map.
@@ -6010,7 +6203,7 @@ class MaterialElement extends HTMLElement {
6010
6203
  */
6011
6204
  set glossMap(value) {
6012
6205
  this._glossMap = value;
6013
- this.setMap(value, 'glossMap');
6206
+ this._setMap(value, 'glossMap');
6014
6207
  }
6015
6208
  /**
6016
6209
  * Gets the id of the `pc-asset` used as the gloss map.
@@ -6115,7 +6308,7 @@ class MaterialElement extends HTMLElement {
6115
6308
  */
6116
6309
  set heightMap(value) {
6117
6310
  this._heightMap = value;
6118
- this.setMap(value, 'heightMap');
6311
+ this._setMap(value, 'heightMap');
6119
6312
  }
6120
6313
  /**
6121
6314
  * Gets the id of the `pc-asset` used as the height map.
@@ -6256,7 +6449,7 @@ class MaterialElement extends HTMLElement {
6256
6449
  */
6257
6450
  set metalnessMap(value) {
6258
6451
  this._metalnessMap = value;
6259
- this.setMap(value, 'metalnessMap');
6452
+ this._setMap(value, 'metalnessMap');
6260
6453
  }
6261
6454
  /**
6262
6455
  * Gets the id of the `pc-asset` used as the metalness map.
@@ -6361,7 +6554,7 @@ class MaterialElement extends HTMLElement {
6361
6554
  */
6362
6555
  set normalMap(value) {
6363
6556
  this._normalMap = value;
6364
- this.setMap(value, 'normalMap');
6557
+ this._setMap(value, 'normalMap');
6365
6558
  }
6366
6559
  /**
6367
6560
  * Gets the id of the `pc-asset` used as the normal map.
@@ -6449,7 +6642,7 @@ class MaterialElement extends HTMLElement {
6449
6642
  set occludeDirect(value) {
6450
6643
  this._occludeDirect = value;
6451
6644
  if (this.material) {
6452
- // @ts-ignore see createMaterial() - the engine mistypes occludeDirect as a number
6645
+ // @ts-ignore see _createMaterial() - the engine mistypes occludeDirect as a number
6453
6646
  this.material.occludeDirect = value;
6454
6647
  this._scheduleUpdate();
6455
6648
  }
@@ -6541,7 +6734,7 @@ class MaterialElement extends HTMLElement {
6541
6734
  */
6542
6735
  set opacityMap(value) {
6543
6736
  this._opacityMap = value;
6544
- this.setMap(value, 'opacityMap');
6737
+ this._setMap(value, 'opacityMap');
6545
6738
  }
6546
6739
  /**
6547
6740
  * Gets the id of the `pc-asset` used as the opacity map.
@@ -6859,6 +7052,13 @@ class MaterialElement extends HTMLElement {
6859
7052
  get useTonemap() {
6860
7053
  return this._useTonemap;
6861
7054
  }
7055
+ /**
7056
+ * Returns the {@link StandardMaterial} created by the `<pc-material>` element with the given
7057
+ * `id`, or `undefined` if there is no such element or its material has not been created yet.
7058
+ *
7059
+ * @param id - The `id` of the `<pc-material>` element.
7060
+ * @returns The material, or `undefined`.
7061
+ */
6862
7062
  static get(id) {
6863
7063
  const materialElement = document.querySelector(`pc-material[id="${id}"]`);
6864
7064
  return materialElement?.material;
@@ -7497,7 +7697,18 @@ class RigidBodyComponentElement extends ComponentElement {
7497
7697
  return this._type;
7498
7698
  }
7499
7699
  static get observedAttributes() {
7500
- return [...super.observedAttributes, 'angular-damping', 'angular-factor', 'friction', 'linear-damping', 'linear-factor', 'mass', 'restitution', 'rolling-friction', 'type'];
7700
+ return [
7701
+ ...super.observedAttributes,
7702
+ 'angular-damping',
7703
+ 'angular-factor',
7704
+ 'friction',
7705
+ 'linear-damping',
7706
+ 'linear-factor',
7707
+ 'mass',
7708
+ 'restitution',
7709
+ 'rolling-friction',
7710
+ 'type'
7711
+ ];
7501
7712
  }
7502
7713
  attributeChangedCallback(name, _oldValue, newValue) {
7503
7714
  super.attributeChangedCallback(name, _oldValue, newValue);
@@ -7802,13 +8013,7 @@ class ScrollbarComponentElement extends ComponentElement {
7802
8013
  return this._handle;
7803
8014
  }
7804
8015
  static get observedAttributes() {
7805
- return [
7806
- ...super.observedAttributes,
7807
- 'orientation',
7808
- 'value',
7809
- 'handle-size',
7810
- 'handle'
7811
- ];
8016
+ return [...super.observedAttributes, 'orientation', 'value', 'handle-size', 'handle'];
7812
8017
  }
7813
8018
  attributeChangedCallback(name, _oldValue, newValue) {
7814
8019
  super.attributeChangedCallback(name, _oldValue, newValue);
@@ -8034,7 +8239,8 @@ class ScrollViewComponentElement extends ComponentElement {
8034
8239
  set horizontalScrollbarVisibility(value) {
8035
8240
  this._horizontalScrollbarVisibility = value;
8036
8241
  if (this.component) {
8037
- this.component.horizontalScrollbarVisibility = visibilities.get(value) ?? SCROLLBAR_VISIBILITY_SHOW_WHEN_REQUIRED;
8242
+ this.component.horizontalScrollbarVisibility =
8243
+ visibilities.get(value) ?? SCROLLBAR_VISIBILITY_SHOW_WHEN_REQUIRED;
8038
8244
  }
8039
8245
  }
8040
8246
  /**
@@ -8052,7 +8258,8 @@ class ScrollViewComponentElement extends ComponentElement {
8052
8258
  set verticalScrollbarVisibility(value) {
8053
8259
  this._verticalScrollbarVisibility = value;
8054
8260
  if (this.component) {
8055
- this.component.verticalScrollbarVisibility = visibilities.get(value) ?? SCROLLBAR_VISIBILITY_SHOW_WHEN_REQUIRED;
8261
+ this.component.verticalScrollbarVisibility =
8262
+ visibilities.get(value) ?? SCROLLBAR_VISIBILITY_SHOW_WHEN_REQUIRED;
8056
8263
  }
8057
8264
  }
8058
8265
  /**
@@ -8239,15 +8446,9 @@ customElements.define('pc-scrollview', ScrollViewComponentElement);
8239
8446
  class ScriptElement extends AsyncElement {
8240
8447
  _attributes = {};
8241
8448
  _enabled = true;
8242
- /**
8243
- * Whether readiness has been signalled. Creation can happen more than once over an
8244
- * element's life (a runtime `name` change recreates the instance), but `ready` is a
8245
- * one-shot signal, so only the first successful creation fires it.
8246
- */
8247
- _readySignalled = false;
8248
8449
  /**
8249
8450
  * The Script instance created for this element by its parent `<pc-scripts>` element.
8250
- * @ignore
8451
+ * @internal
8251
8452
  */
8252
8453
  _script = null;
8253
8454
  /**
@@ -8329,14 +8530,20 @@ class ScriptElement extends AsyncElement {
8329
8530
  console.warn(`pc-script '${this.getAttribute('name')}' must be a direct child of pc-scripts - script not created`);
8330
8531
  }
8331
8532
  }
8533
+ disconnectedCallback() {
8534
+ // Re-arm readiness so a re-inserted element announces the instance created for it then.
8535
+ // `_script` is deliberately NOT cleared here: the parent's mutation observer processes
8536
+ // this removal afterwards and reads it to establish which engine script this element
8537
+ // owned - the parent is what clears it.
8538
+ this._resetReady();
8539
+ }
8332
8540
  /**
8333
8541
  * Called by the parent `<pc-scripts>` element when the script instance has been created.
8334
- * @ignore
8542
+ * Creation can happen more than once per connection (a runtime `name` change recreates the
8543
+ * instance), but `_onReady` signals readiness at most once per cycle.
8544
+ * @internal
8335
8545
  */
8336
8546
  _onScriptCreated() {
8337
- if (this._readySignalled)
8338
- return;
8339
- this._readySignalled = true;
8340
8547
  this._onReady();
8341
8548
  }
8342
8549
  static get observedAttributes() {
@@ -8381,10 +8588,34 @@ customElements.define('pc-script', ScriptElement);
8381
8588
  */
8382
8589
  const RESERVED_ATTRIBUTES = new Set([
8383
8590
  ...ScriptElement.observedAttributes,
8384
- 'accesskey', 'autocapitalize', 'autofocus', 'class', 'contenteditable', 'dir', 'draggable',
8385
- 'exportparts', 'hidden', 'id', 'inert', 'is', 'itemid', 'itemprop', 'itemref', 'itemscope',
8386
- 'itemtype', 'lang', 'nonce', 'part', 'popover', 'role', 'slot', 'spellcheck', 'style',
8387
- 'tabindex', 'title', 'translate'
8591
+ 'accesskey',
8592
+ 'autocapitalize',
8593
+ 'autofocus',
8594
+ 'class',
8595
+ 'contenteditable',
8596
+ 'dir',
8597
+ 'draggable',
8598
+ 'exportparts',
8599
+ 'hidden',
8600
+ 'id',
8601
+ 'inert',
8602
+ 'is',
8603
+ 'itemid',
8604
+ 'itemprop',
8605
+ 'itemref',
8606
+ 'itemscope',
8607
+ 'itemtype',
8608
+ 'lang',
8609
+ 'nonce',
8610
+ 'part',
8611
+ 'popover',
8612
+ 'role',
8613
+ 'slot',
8614
+ 'spellcheck',
8615
+ 'style',
8616
+ 'tabindex',
8617
+ 'title',
8618
+ 'translate'
8388
8619
  ]);
8389
8620
  /**
8390
8621
  * Checks whether a `pc-script` attribute name is reserved (and so never maps to a script
@@ -8396,18 +8627,25 @@ const RESERVED_ATTRIBUTES = new Set([
8396
8627
  * @returns Whether the attribute name is reserved.
8397
8628
  */
8398
8629
  const isReservedAttribute = (name) => {
8399
- return RESERVED_ATTRIBUTES.has(name) ||
8630
+ return (RESERVED_ATTRIBUTES.has(name) ||
8400
8631
  name.startsWith('data-') ||
8401
8632
  name.startsWith('aria-') ||
8402
8633
  name.startsWith('_') ||
8403
- (name.startsWith('on') && name in HTMLElement.prototype);
8634
+ (name.startsWith('on') && name in HTMLElement.prototype));
8404
8635
  };
8405
8636
  /**
8406
8637
  * Script API members that per-property attributes must never overwrite: the engine bindings and
8407
8638
  * the (optional, so possibly undefined) lifecycle methods.
8408
8639
  */
8409
8640
  const SCRIPT_API_MEMBERS = new Set([
8410
- 'app', 'entity', 'destroy', 'initialize', 'postInitialize', 'postUpdate', 'swap', 'update'
8641
+ 'app',
8642
+ 'entity',
8643
+ 'destroy',
8644
+ 'initialize',
8645
+ 'postInitialize',
8646
+ 'postUpdate',
8647
+ 'swap',
8648
+ 'update'
8411
8649
  ]);
8412
8650
  /**
8413
8651
  * Converts a kebab-case attribute name to the camelCase script attribute name.
@@ -8423,7 +8661,7 @@ const kebabToCamel = (name) => {
8423
8661
  * @returns The kebab-case name.
8424
8662
  */
8425
8663
  const camelToKebab = (name) => {
8426
- return name.replace(/[A-Z]/g, char => `-${char.toLowerCase()}`);
8664
+ return name.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`);
8427
8665
  };
8428
8666
  /**
8429
8667
  * Resolves an `asset:` prefix to the Asset created by the `pc-asset` element with that id.
@@ -8622,7 +8860,10 @@ class ScriptComponentElement extends ComponentElement {
8622
8860
  // Only recurse into plain objects. Class instances (Vec3, Color, Asset, Entity...)
8623
8861
  // are leaf values assigned whole, so accessor-typed script attributes receive them
8624
8862
  // through their setters instead of having a getter's returned copy mutated.
8625
- if (value && typeof value === 'object' && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype) {
8863
+ if (value &&
8864
+ typeof value === 'object' &&
8865
+ !Array.isArray(value) &&
8866
+ Object.getPrototypeOf(value) === Object.prototype) {
8626
8867
  if (!current || typeof current !== 'object') {
8627
8868
  target[key] = {};
8628
8869
  }
@@ -8640,7 +8881,11 @@ class ScriptComponentElement extends ComponentElement {
8640
8881
  * @returns Whether the value is a math type.
8641
8882
  */
8642
8883
  isMathType(value) {
8643
- return value instanceof Vec2 || value instanceof Vec3 || value instanceof Vec4 || value instanceof Color || value instanceof Quat;
8884
+ return (value instanceof Vec2 ||
8885
+ value instanceof Vec3 ||
8886
+ value instanceof Vec4 ||
8887
+ value instanceof Color ||
8888
+ value instanceof Quat);
8644
8889
  }
8645
8890
  /**
8646
8891
  * Converts a plain numeric array to the math type of `current`. A 3-element array targeting
@@ -8653,7 +8898,7 @@ class ScriptComponentElement extends ComponentElement {
8653
8898
  * @returns The converted value, or `null`.
8654
8899
  */
8655
8900
  arrayToMathType(current, value, key) {
8656
- if (value.every(component => typeof component === 'number' && Number.isFinite(component))) {
8901
+ if (value.every((component) => typeof component === 'number' && Number.isFinite(component))) {
8657
8902
  if (current instanceof Vec2 && value.length === 2)
8658
8903
  return new Vec2(value);
8659
8904
  if (current instanceof Vec3 && value.length === 3)
@@ -8925,7 +9170,10 @@ class ScriptComponentElement extends ComponentElement {
8925
9170
  mutation.removedNodes.forEach((node) => {
8926
9171
  if (node instanceof ScriptElement) {
8927
9172
  const scriptName = node.getAttribute('name');
8928
- if (scriptName && node._script && this.component && this.component.get(scriptName) === node._script) {
9173
+ if (scriptName &&
9174
+ node._script &&
9175
+ this.component &&
9176
+ this.component.get(scriptName) === node._script) {
8929
9177
  this.destroyScript(scriptName);
8930
9178
  }
8931
9179
  node._script = null;
@@ -9174,18 +9422,27 @@ class SoundSlotElement extends AsyncElement {
9174
9422
  * emit a misleading "must be a direct child" warning for what is an ordinary removal.
9175
9423
  */
9176
9424
  _soundElement = null;
9425
+ /**
9426
+ * Incremented on every connect and disconnect, and captured by connectedCallback on entry —
9427
+ * a resume from an await abandons itself if the value has moved on, so a stale callback can
9428
+ * neither act on a torn-down tree nor add its slot alongside a re-inserted element's own
9429
+ * callback.
9430
+ */
9431
+ _connectionGeneration = 0;
9177
9432
  /**
9178
9433
  * The sound slot.
9179
9434
  */
9180
9435
  soundSlot = null;
9181
9436
  async connectedCallback() {
9437
+ const generation = ++this._connectionGeneration;
9182
9438
  const soundElement = this.soundElement;
9183
9439
  await soundElement?.ready();
9184
- // The element may have been removed, or its parent torn down, while we were waiting. A
9185
- // <pc-app> disconnects before its children, so by the time we resume the component can
9186
- // already be gone - see the matching guard in disconnectedCallback below.
9440
+ // The element may have been removed (perhaps re-inserted, which runs a callback of its
9441
+ // own), or its parent torn down, while we were waiting. A <pc-app> disconnects before
9442
+ // its children, so by the time we resume the component can already be gone - see the
9443
+ // matching guard in disconnectedCallback below.
9187
9444
  const component = soundElement?.component;
9188
- if (!this.isConnected || !component) {
9445
+ if (generation !== this._connectionGeneration || !component) {
9189
9446
  return;
9190
9447
  }
9191
9448
  const options = {
@@ -9208,12 +9465,15 @@ class SoundSlotElement extends AsyncElement {
9208
9465
  this._onReady();
9209
9466
  }
9210
9467
  disconnectedCallback() {
9468
+ // Invalidate any connectedCallback still suspended on an await
9469
+ this._connectionGeneration++;
9211
9470
  // Uses the cached parent rather than a fresh lookup, since parentElement is already null
9212
9471
  // by now. The component itself is null if the parent <pc-sound> (or the whole <pc-app>) is
9213
9472
  // being torn down — parents disconnect first and have already removed the component.
9214
9473
  this._soundElement?.component?.removeSlot(this._name);
9215
9474
  this._soundElement = null;
9216
9475
  this.soundSlot = null;
9476
+ this._resetReady();
9217
9477
  }
9218
9478
  get soundElement() {
9219
9479
  const soundElement = this.parentElement;
@@ -9613,39 +9873,85 @@ customElements.define('pc-gsplat', GSplatComponentElement);
9613
9873
  class ModelElement extends AsyncElement {
9614
9874
  _asset = '';
9615
9875
  _entity = null;
9876
+ /**
9877
+ * Incremented on every new load and on disconnect, and captured by a load when it starts. A
9878
+ * load that resumes from an await or a load callback abandons itself if the value has moved
9879
+ * on, so a superseded load can neither instantiate a second entity nor parent one that has
9880
+ * since been destroyed.
9881
+ */
9882
+ _loadGeneration = 0;
9883
+ /**
9884
+ * The pending asset-load subscription of the current load, if it is waiting for its asset.
9885
+ * Held so that whatever supersedes the load can detach the handler from the asset, rather
9886
+ * than leave it registered until the asset loads (or forever, if it never does).
9887
+ */
9888
+ _loadHandle = null;
9889
+ /**
9890
+ * The root entity of the instantiated model. `null` until the container asset has loaded
9891
+ * and been instantiated, and again once the element has been removed from the document.
9892
+ * @returns The model's root entity, or `null`.
9893
+ */
9894
+ get entity() {
9895
+ return this._entity;
9896
+ }
9616
9897
  connectedCallback() {
9617
9898
  this._loadModel();
9618
9899
  this._onReady();
9619
9900
  }
9620
9901
  disconnectedCallback() {
9902
+ this._loadGeneration++;
9903
+ this._detachLoadHandler();
9621
9904
  this._unloadModel();
9905
+ this._resetReady();
9906
+ }
9907
+ _detachLoadHandler() {
9908
+ this._loadHandle?.off();
9909
+ this._loadHandle = null;
9622
9910
  }
9623
9911
  _instantiate(container) {
9624
- this._entity = container.instantiateRenderEntity();
9912
+ const generation = this._loadGeneration;
9913
+ const entity = container.instantiateRenderEntity();
9914
+ this._entity = entity;
9625
9915
  // @ts-ignore
9626
9916
  if (container.animations.length > 0) {
9627
- this._entity.addComponent('anim');
9917
+ entity.addComponent('anim');
9628
9918
  // @ts-ignore
9629
- this._entity.anim.assignAnimation('animation', container.animations[0].resource);
9919
+ entity.anim.assignAnimation('animation', container.animations[0].resource);
9630
9920
  }
9921
+ // The parent's readiness re-arms when it is torn down, so these can resume in a later
9922
+ // connection cycle. The entity is captured above and the generation re-checked, so a
9923
+ // stale resume cannot parent an entity a newer cycle has already destroyed.
9631
9924
  const parentEntityElement = this.closestEntity;
9632
9925
  if (parentEntityElement) {
9633
9926
  parentEntityElement.ready().then(() => {
9634
- parentEntityElement.entity.addChild(this._entity);
9927
+ if (generation !== this._loadGeneration) {
9928
+ return;
9929
+ }
9930
+ parentEntityElement.entity.addChild(entity);
9635
9931
  });
9636
9932
  }
9637
9933
  else {
9638
9934
  const appElement = this.closestApp;
9639
9935
  if (appElement) {
9640
9936
  appElement.ready().then(() => {
9641
- appElement.app.root.addChild(this._entity);
9937
+ if (generation !== this._loadGeneration) {
9938
+ return;
9939
+ }
9940
+ appElement.app.root.addChild(entity);
9642
9941
  });
9643
9942
  }
9644
9943
  }
9645
9944
  }
9646
9945
  async _loadModel() {
9647
9946
  this._unloadModel();
9947
+ // Supersede any load already in flight - only the newest load may instantiate
9948
+ const generation = ++this._loadGeneration;
9949
+ this._detachLoadHandler();
9648
9950
  const appElement = await this.closestApp?.ready();
9951
+ // The element may have been removed, or another load started, while we waited
9952
+ if (generation !== this._loadGeneration) {
9953
+ return;
9954
+ }
9649
9955
  const app = appElement?.app;
9650
9956
  const asset = AssetElement.get(this._asset);
9651
9957
  if (!asset) {
@@ -9655,7 +9961,14 @@ class ModelElement extends AsyncElement {
9655
9961
  this._instantiate(asset.resource);
9656
9962
  }
9657
9963
  else {
9658
- asset.once('load', () => {
9964
+ // The generation is re-checked even though a superseded handler is detached: the
9965
+ // detach relies on how the engine's event emitter treats removal, while the check
9966
+ // holds on its own.
9967
+ this._loadHandle = asset.once('load', () => {
9968
+ this._loadHandle = null;
9969
+ if (generation !== this._loadGeneration) {
9970
+ return;
9971
+ }
9659
9972
  this._instantiate(asset.resource);
9660
9973
  });
9661
9974
  app.assets.load(asset);
@@ -9757,10 +10070,17 @@ class SceneElement extends AsyncElement {
9757
10070
  return;
9758
10071
  }
9759
10072
  this._scene = app.scene;
9760
- this.updateSceneSettings();
10073
+ this._updateSceneSettings();
9761
10074
  this._onReady();
9762
10075
  }
9763
- updateSceneSettings() {
10076
+ disconnectedCallback() {
10077
+ // The scene belongs to the application, and removing this element - or the <pc-app>
10078
+ // above it, which disconnects first - parts the two. Re-arm readiness so a re-inserted
10079
+ // element announces the scene it acquires then, not the one it lost here.
10080
+ this._scene = null;
10081
+ this._resetReady();
10082
+ }
10083
+ _updateSceneSettings() {
9764
10084
  if (this._scene) {
9765
10085
  this._scene.fog.type = this._fog;
9766
10086
  this._scene.fog.color = this._fogColor;
@@ -9928,13 +10248,32 @@ class SkyElement extends AsyncElement {
9928
10248
  _type = 'infinite';
9929
10249
  _scene = null;
9930
10250
  _appElement = null;
10251
+ /**
10252
+ * Incremented on every new load and on disconnect, and captured by a load when it starts. A
10253
+ * load that resumes from an await or a load callback abandons itself if the value has moved
10254
+ * on, so a superseded load cannot generate a skybox for a scene it no longer configures.
10255
+ */
10256
+ _loadGeneration = 0;
10257
+ /**
10258
+ * The pending asset-load subscription of the current load, if it is waiting for its asset.
10259
+ * Held so that whatever supersedes the load can detach the handler from the asset, rather
10260
+ * than leave it registered until the asset loads (or forever, if it never does).
10261
+ */
10262
+ _loadHandle = null;
9931
10263
  connectedCallback() {
9932
10264
  this._loadSkybox();
9933
10265
  this._onReady();
9934
10266
  }
9935
10267
  disconnectedCallback() {
10268
+ this._loadGeneration++;
10269
+ this._detachLoadHandler();
9936
10270
  this._unloadSkybox();
9937
10271
  this._appElement = null;
10272
+ this._resetReady();
10273
+ }
10274
+ _detachLoadHandler() {
10275
+ this._loadHandle?.off();
10276
+ this._loadHandle = null;
9938
10277
  }
9939
10278
  _generateSkybox(asset) {
9940
10279
  if (!this._scene)
@@ -9942,10 +10281,17 @@ class SkyElement extends AsyncElement {
9942
10281
  const source = asset.resource;
9943
10282
  const skybox = EnvLighting.generateSkyboxCubemap(source);
9944
10283
  skybox.anisotropy = 4;
10284
+ // This element owns what it generated (see _unloadSkybox) - replacing a skybox from an
10285
+ // earlier load must release it, not orphan it on the GPU
10286
+ this._scene.skybox?.destroy();
9945
10287
  this._scene.skybox = skybox;
9946
10288
  if (this._lighting) {
9947
10289
  const lighting = EnvLighting.generateLightingSource(source);
9948
10290
  const envAtlas = EnvLighting.generateAtlas(lighting);
10291
+ // The lighting source is an intermediate: the atlas is rendered from it and it is
10292
+ // not needed afterwards
10293
+ lighting.destroy();
10294
+ this._scene.envAtlas?.destroy();
9949
10295
  this._scene.envAtlas = envAtlas;
9950
10296
  }
9951
10297
  const layer = this._scene.layers.getLayerById(LAYERID_SKYBOX);
@@ -9959,7 +10305,14 @@ class SkyElement extends AsyncElement {
9959
10305
  this._scene.skyboxMip = this._mipLevel;
9960
10306
  }
9961
10307
  async _loadSkybox() {
10308
+ // Supersede any load already in flight - only the newest load may generate the skybox
10309
+ const generation = ++this._loadGeneration;
10310
+ this._detachLoadHandler();
9962
10311
  const appElement = await this.closestApp?.ready();
10312
+ // The element may have been removed, or another load started, while we waited
10313
+ if (generation !== this._loadGeneration) {
10314
+ return;
10315
+ }
9963
10316
  const app = appElement?.app;
9964
10317
  if (!appElement || !app) {
9965
10318
  return;
@@ -9974,7 +10327,14 @@ class SkyElement extends AsyncElement {
9974
10327
  this._generateSkybox(asset);
9975
10328
  }
9976
10329
  else {
9977
- asset.once('load', () => {
10330
+ // The generation is re-checked even though a superseded handler is detached: the
10331
+ // detach relies on how the engine's event emitter treats removal, while the check
10332
+ // holds on its own.
10333
+ this._loadHandle = asset.once('load', () => {
10334
+ this._loadHandle = null;
10335
+ if (generation !== this._loadGeneration) {
10336
+ return;
10337
+ }
9978
10338
  this._generateSkybox(asset);
9979
10339
  });
9980
10340
  app.assets.load(asset);