@playcanvas/web-components 0.12.0 → 0.13.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 (95) hide show
  1. package/README.md +1 -1
  2. package/dist/app.d.cts +281 -0
  3. package/dist/app.d.ts +61 -3
  4. package/dist/asset.d.cts +224 -0
  5. package/dist/asset.d.ts +8 -2
  6. package/dist/async-element.d.cts +103 -0
  7. package/dist/async-element.d.ts +6 -4
  8. package/dist/colors.d.cts +1 -0
  9. package/dist/components/button-component.d.cts +186 -0
  10. package/dist/components/button-component.d.ts +1 -1
  11. package/dist/components/camera-component.d.cts +253 -0
  12. package/dist/components/camera-component.d.ts +1 -1
  13. package/dist/components/collision-component.d.cts +73 -0
  14. package/dist/components/collision-component.d.ts +1 -1
  15. package/dist/components/component.d.cts +82 -0
  16. package/dist/components/component.d.ts +1 -1
  17. package/dist/components/element-component.d.cts +316 -0
  18. package/dist/components/element-component.d.ts +1 -1
  19. package/dist/components/gsplat-component.d.cts +108 -0
  20. package/dist/components/gsplat-component.d.ts +1 -1
  21. package/dist/components/layoutchild-component.d.cts +110 -0
  22. package/dist/components/layoutchild-component.d.ts +1 -1
  23. package/dist/components/layoutgroup-component.d.cts +136 -0
  24. package/dist/components/layoutgroup-component.d.ts +1 -1
  25. package/dist/components/light-component.d.cts +264 -0
  26. package/dist/components/light-component.d.ts +1 -1
  27. package/dist/components/listener-component.d.cts +20 -0
  28. package/dist/components/listener-component.d.ts +1 -1
  29. package/dist/components/particlesystem-component.d.cts +52 -0
  30. package/dist/components/particlesystem-component.d.ts +1 -1
  31. package/dist/components/render-component.d.cts +76 -0
  32. package/dist/components/render-component.d.ts +1 -1
  33. package/dist/components/rigidbody-component.d.cts +88 -0
  34. package/dist/components/rigidbody-component.d.ts +1 -1
  35. package/dist/components/screen-component.d.cts +70 -0
  36. package/dist/components/screen-component.d.ts +1 -1
  37. package/dist/components/script-component.d.cts +163 -0
  38. package/dist/components/script-component.d.ts +1 -1
  39. package/dist/components/script.d.cts +94 -0
  40. package/dist/components/script.d.ts +1 -1
  41. package/dist/components/scrollbar-component.d.cts +69 -0
  42. package/dist/components/scrollbar-component.d.ts +1 -1
  43. package/dist/components/scrollview-component.d.cts +178 -0
  44. package/dist/components/scrollview-component.d.ts +1 -1
  45. package/dist/components/sound-component.d.cts +108 -0
  46. package/dist/components/sound-component.d.ts +1 -1
  47. package/dist/components/sound-slot.d.cts +134 -0
  48. package/dist/components/sound-slot.d.ts +2 -2
  49. package/dist/custom-elements.json +80 -22
  50. package/dist/entity-base.d.cts +67 -0
  51. package/dist/entity-base.d.ts +2 -2
  52. package/dist/entity.d.cts +131 -0
  53. package/dist/entity.d.ts +1 -1
  54. package/dist/index.d.cts +79 -0
  55. package/dist/index.d.ts +33 -33
  56. package/dist/loading-bar.d.cts +35 -0
  57. package/dist/material.d.cts +1011 -0
  58. package/dist/model.d.cts +72 -0
  59. package/dist/model.d.ts +1 -1
  60. package/dist/module.d.cts +29 -0
  61. package/dist/module.d.ts +16 -10
  62. package/dist/node.d.cts +253 -0
  63. package/dist/node.d.ts +1 -1
  64. package/dist/parse.d.cts +147 -0
  65. package/dist/pwc.cjs +285 -92
  66. package/dist/pwc.cjs.map +1 -1
  67. package/dist/pwc.js +286 -93
  68. package/dist/pwc.js.map +1 -1
  69. package/dist/pwc.min.js +1 -1
  70. package/dist/pwc.min.js.map +1 -1
  71. package/dist/pwc.min.mjs +1 -1
  72. package/dist/pwc.min.mjs.map +1 -1
  73. package/dist/pwc.mjs +286 -93
  74. package/dist/pwc.mjs.map +1 -1
  75. package/dist/scene.d.cts +117 -0
  76. package/dist/scene.d.ts +1 -1
  77. package/dist/sky.d.cts +121 -0
  78. package/dist/sky.d.ts +1 -1
  79. package/dist/vscode.html-custom-data.json +3 -3
  80. package/dist/web-types.json +42 -6
  81. package/package.json +16 -7
  82. package/src/app.ts +211 -49
  83. package/src/asset.ts +34 -2
  84. package/src/async-element.ts +4 -2
  85. package/src/components/button-component.ts +7 -7
  86. package/src/components/element-component.ts +7 -7
  87. package/src/components/gsplat-component.ts +3 -3
  88. package/src/components/particlesystem-component.ts +7 -8
  89. package/src/components/script-component.ts +2 -2
  90. package/src/components/sound-slot.ts +2 -2
  91. package/src/loading-bar.ts +2 -2
  92. package/src/material.ts +2 -2
  93. package/src/model.ts +2 -5
  94. package/src/module.ts +39 -20
  95. package/src/sky.ts +2 -3
package/dist/pwc.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { basisInitialize, WasmModule, Vec3, Color, Vec4, Quat, Vec2, createGraphicsDevice, AppOptions, Keyboard, Mouse, ElementInput, AnimComponentSystem, AnimationComponentSystem, AudioListenerComponentSystem, ButtonComponentSystem, CameraComponentSystem, CollisionComponentSystem, ElementComponentSystem, GSplatComponentSystem, JointComponentSystem, LayoutChildComponentSystem, LayoutGroupComponentSystem, LightComponentSystem, ModelComponentSystem, ParticleSystemComponentSystem, RenderComponentSystem, RigidBodyComponentSystem, ScreenComponentSystem, ScriptComponentSystem, ScrollbarComponentSystem, ScrollViewComponentSystem, SoundComponentSystem, SpriteComponentSystem, ZoneComponentSystem, AnimClipHandler, AnimationHandler, AnimStateGraphHandler, AudioHandler, BinaryHandler, CssHandler, ContainerHandler, CubemapHandler, FolderHandler, FontHandler, GSplatHandler, HierarchyHandler, HtmlHandler, JsonHandler, MaterialHandler, ModelHandler, RenderHandler, ScriptHandler, SceneHandler, ShaderHandler, SpriteHandler, TemplateHandler, TextHandler, TextureAtlasHandler, TextureHandler, SoundManager, Lightmapper, BatchManager, XrManager, AppBase, FILLMODE_FILL_WINDOW, RESOLUTION_AUTO, Picker, MeshInstance, Entity, Asset, SPRITE_RENDERMODE_SIMPLE, FILTER_LINEAR_MIPMAP_LINEAR, FILTER_LINEAR, ADDRESS_REPEAT, SPRITE_RENDERMODE_SLICED, SPRITE_RENDERMODE_TILED, ADDRESS_CLAMP_TO_EDGE, ADDRESS_MIRRORED_REPEAT, FILTER_NEAREST, FILTER_NEAREST_MIPMAP_NEAREST, FILTER_LINEAR_MIPMAP_NEAREST, FILTER_NEAREST_MIPMAP_LINEAR, BUTTON_TRANSITION_MODE_TINT, BUTTON_TRANSITION_MODE_SPRITE_CHANGE, TONEMAP_NONE, PROJECTION_PERSPECTIVE, GAMMA_SRGB, GAMMA_NONE, XRTYPE_VR, TONEMAP_LINEAR, TONEMAP_FILMIC, TONEMAP_HEJL, TONEMAP_ACES, TONEMAP_ACES2, TONEMAP_NEUTRAL, PROJECTION_ORTHOGRAPHIC, ORIENTATION_HORIZONTAL, FITTING_NONE, FITTING_STRETCH, FITTING_SHRINK, FITTING_BOTH, ORIENTATION_VERTICAL, SHADOW_PCF3_32F, SHADOW_PCF1_16F, SHADOW_PCF1_32F, SHADOW_PCF3_16F, SHADOW_PCF5_16F, SHADOW_PCF5_32F, SHADOW_VSM_16F, SHADOW_VSM_32F, SHADOW_PCSS_32F, StandardMaterial, BLEND_NONE, CULLFACE_BACK, FRESNEL_SCHLICK, SPECOCC_AO, BLEND_NORMAL, BLEND_ADDITIVE, BLEND_ADDITIVEALPHA, BLEND_PREMULTIPLIED, BLEND_MULTIPLICATIVE, BLEND_MULTIPLICATIVE2X, BLEND_SCREEN, BLEND_MIN, BLEND_MAX, BLEND_SUBTRACTIVE, CULLFACE_NONE, CULLFACE_FRONT, CULLFACE_FRONTANDBACK, FRESNEL_NONE, SPECOCC_NONE, SPECOCC_GLOSSDEPENDENT, SCALEMODE_NONE, SCALEMODE_BLEND, SCROLL_MODE_BOUNCE, SCROLLBAR_VISIBILITY_SHOW_WHEN_REQUIRED, SCROLLBAR_VISIBILITY_SHOW_ALWAYS, SCROLL_MODE_CLAMP, SCROLL_MODE_INFINITE, EnvLighting, LAYERID_SKYBOX } from 'playcanvas';
1
+ import { basisInitialize, WasmModule, Vec3, Color, Vec4, Quat, Vec2, createGraphicsDevice, AppOptions, Keyboard, Mouse, ElementInput, AnimComponentSystem, AnimationComponentSystem, AudioListenerComponentSystem, ButtonComponentSystem, CameraComponentSystem, CollisionComponentSystem, ElementComponentSystem, GSplatComponentSystem, JointComponentSystem, LayoutChildComponentSystem, LayoutGroupComponentSystem, LightComponentSystem, ModelComponentSystem, ParticleSystemComponentSystem, RenderComponentSystem, RigidBodyComponentSystem, ScreenComponentSystem, ScriptComponentSystem, ScrollbarComponentSystem, ScrollViewComponentSystem, SoundComponentSystem, SpriteComponentSystem, ZoneComponentSystem, AnimClipHandler, AnimationHandler, AnimStateGraphHandler, AudioHandler, BinaryHandler, CssHandler, ContainerHandler, CubemapHandler, FolderHandler, FontHandler, GSplatHandler, HierarchyHandler, HtmlHandler, JsonHandler, MaterialHandler, ModelHandler, RenderHandler, ScriptHandler, SceneHandler, ShaderHandler, SpriteHandler, TemplateHandler, TextHandler, TextureAtlasHandler, TextureHandler, SoundManager, Lightmapper, BatchManager, XrManager, AppBase, FILLMODE_NONE, RESOLUTION_AUTO, Picker, MeshInstance, Entity, Asset, SPRITE_RENDERMODE_SIMPLE, FILTER_LINEAR_MIPMAP_LINEAR, FILTER_LINEAR, ADDRESS_REPEAT, SPRITE_RENDERMODE_SLICED, SPRITE_RENDERMODE_TILED, ADDRESS_CLAMP_TO_EDGE, ADDRESS_MIRRORED_REPEAT, FILTER_NEAREST, FILTER_NEAREST_MIPMAP_NEAREST, FILTER_LINEAR_MIPMAP_NEAREST, FILTER_NEAREST_MIPMAP_LINEAR, BUTTON_TRANSITION_MODE_TINT, BUTTON_TRANSITION_MODE_SPRITE_CHANGE, TONEMAP_NONE, PROJECTION_PERSPECTIVE, GAMMA_SRGB, GAMMA_NONE, XRTYPE_VR, TONEMAP_LINEAR, TONEMAP_FILMIC, TONEMAP_HEJL, TONEMAP_ACES, TONEMAP_ACES2, TONEMAP_NEUTRAL, PROJECTION_ORTHOGRAPHIC, ORIENTATION_HORIZONTAL, FITTING_NONE, FITTING_STRETCH, FITTING_SHRINK, FITTING_BOTH, ORIENTATION_VERTICAL, SHADOW_PCF3_32F, SHADOW_PCF1_16F, SHADOW_PCF1_32F, SHADOW_PCF3_16F, SHADOW_PCF5_16F, SHADOW_PCF5_32F, SHADOW_VSM_16F, SHADOW_VSM_32F, SHADOW_PCSS_32F, StandardMaterial, BLEND_NONE, CULLFACE_BACK, FRESNEL_SCHLICK, SPECOCC_AO, BLEND_NORMAL, BLEND_ADDITIVE, BLEND_ADDITIVEALPHA, BLEND_PREMULTIPLIED, BLEND_MULTIPLICATIVE, BLEND_MULTIPLICATIVE2X, BLEND_SCREEN, BLEND_MIN, BLEND_MAX, BLEND_SUBTRACTIVE, CULLFACE_NONE, CULLFACE_FRONT, CULLFACE_FRONTANDBACK, FRESNEL_NONE, SPECOCC_NONE, SPECOCC_GLOSSDEPENDENT, SCALEMODE_NONE, SCALEMODE_BLEND, SCROLL_MODE_BOUNCE, SCROLLBAR_VISIBILITY_SHOW_WHEN_REQUIRED, SCROLLBAR_VISIBILITY_SHOW_ALWAYS, SCROLL_MODE_CLAMP, SCROLL_MODE_INFINITE, EnvLighting, LAYERID_SKYBOX } from 'playcanvas';
2
2
 
3
3
  /**
4
4
  * Base class for all PlayCanvas Web Components that initialize asynchronously.
@@ -108,12 +108,18 @@ async function whenReady(target) {
108
108
 
109
109
  /**
110
110
  * The ModuleElement interface provides properties and methods for manipulating
111
- * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-module/ | `<pc-module>`} elements.
112
- * The ModuleElement interface also inherits the properties and methods of the
113
- * {@link HTMLElement} interface.
111
+ * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-module/ | `<pc-module>`}
112
+ * elements. The ModuleElement interface also inherits the properties and methods of the
113
+ * {@link AsyncElement} interface.
114
+ *
115
+ * The attributes are read once, when the module starts loading - on the element's first
116
+ * connection, or earlier if a containing `<pc-app>` boots first and collects it - so changing
117
+ * them later has no effect. The element becomes ready once the module has loaded. WebAssembly
118
+ * modules configure engine-global state that never unloads, so readiness is not re-armed by
119
+ * removing the element, and a re-inserted element does not load again.
114
120
  *
115
- * Note that these attributes are read once when the element is created, so changing them later
116
- * has no effect.
121
+ * A `<pc-module>` without a `name` warns and never becomes ready; a containing `<pc-app>` still
122
+ * boots.
117
123
  *
118
124
  * @attribute {string} name - The name of the WebAssembly module to configure, e.g. `Basis` or
119
125
  * `Ammo`.
@@ -122,19 +128,22 @@ async function whenReady(target) {
122
128
  * @attribute {string} fallback - The URL of the module's asm.js fallback script, used when
123
129
  * WebAssembly is unavailable.
124
130
  */
125
- class ModuleElement extends HTMLElement {
126
- loadPromise;
127
- /** @ignore */
128
- constructor() {
129
- super();
130
- this.loadPromise = this.loadModule();
131
+ class ModuleElement extends AsyncElement {
132
+ _loadPromise = null;
133
+ connectedCallback() {
134
+ this._getLoadPromise();
131
135
  }
132
- async loadModule() {
136
+ async _loadModule() {
133
137
  const name = this.getAttribute('name');
134
- const glueUrl = this.getAttribute('glue');
135
- const wasmUrl = this.getAttribute('wasm');
136
- const fallbackUrl = this.getAttribute('fallback');
137
- const config = { glueUrl, wasmUrl, fallbackUrl };
138
+ if (!name) {
139
+ console.warn("pc-module requires a 'name' attribute - no module was configured");
140
+ return;
141
+ }
142
+ const config = {
143
+ glueUrl: this.getAttribute('glue') ?? undefined,
144
+ wasmUrl: this.getAttribute('wasm') ?? undefined,
145
+ fallbackUrl: this.getAttribute('fallback') ?? undefined
146
+ };
138
147
  if (name === 'Basis') {
139
148
  basisInitialize(config);
140
149
  }
@@ -144,16 +153,22 @@ class ModuleElement extends HTMLElement {
144
153
  WasmModule.getInstance(name, () => resolve());
145
154
  });
146
155
  }
156
+ this._onReady();
147
157
  }
148
158
  /**
149
- * Returns the promise that settles when the module has loaded. Awaited by the containing
150
- * `<pc-app>` element before it creates its graphics device.
159
+ * Returns the promise that settles when the module has loaded, starting the load if it has
160
+ * not already started - a containing `<pc-app>` boots in document order, so it may collect
161
+ * this element before the element's own connectedCallback has run. A missing `name` resolves
162
+ * the promise without configuring anything, so a misconfigured module never blocks the app.
151
163
  *
152
164
  * @returns The load promise.
153
165
  * @internal
154
166
  */
155
167
  _getLoadPromise() {
156
- return this.loadPromise;
168
+ if (!this._loadPromise) {
169
+ this._loadPromise = this._loadModule();
170
+ }
171
+ return this._loadPromise;
157
172
  }
158
173
  }
159
174
  customElements.define('pc-module', ModuleElement);
@@ -183,9 +198,9 @@ class LoadingBar {
183
198
  this._track.setAttribute('aria-label', 'Loading');
184
199
  this._track.setAttribute('aria-valuemin', '0');
185
200
  this._track.setAttribute('aria-valuemax', '100');
186
- // Fixed positioning matches the canvas, which always fills the window (FILLMODE_FILL_WINDOW)
201
+ // Anchored to the pc-app element, which the library's base styles make a positioned box
187
202
  this._track.style.cssText = [
188
- 'position: fixed',
203
+ 'position: absolute',
189
204
  'top: 0',
190
205
  'left: 0',
191
206
  'width: 100%',
@@ -697,16 +712,44 @@ const getEntity = (ref) => {
697
712
 
698
713
  /** The pointer event types the application synthesizes on `<pc-entity>` elements via picking. */
699
714
  const pointerEventTypes = ['pointermove', 'pointerdown', 'pointerup', 'pointerenter', 'pointerleave'];
715
+ /**
716
+ * Gives `pc-app` the sizing contract of a replaced element (`<video>`, `<img>`): a block-level
717
+ * box that the page's CSS sizes, defaulting to the canvas's own 300x150 intrinsic size, with the
718
+ * canvas and loading bar anchored to it. `:where()` keeps every declaration at zero specificity,
719
+ * so any page rule - however plain - overrides these defaults.
720
+ */
721
+ const ensureBaseStyles = () => {
722
+ const id = 'pc-app-styles';
723
+ if (document.getElementById(id)) {
724
+ return;
725
+ }
726
+ const style = document.createElement('style');
727
+ style.id = id;
728
+ style.textContent = ':where(pc-app) { display: block; position: relative; width: 300px; height: 150px; }';
729
+ document.head.appendChild(style);
730
+ };
700
731
  /**
701
732
  * The AppElement interface provides properties and methods for manipulating
702
733
  * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-app/ | `<pc-app>`} elements.
703
734
  * The AppElement interface also inherits the properties and methods of the
704
735
  * {@link HTMLElement} interface.
705
736
  *
737
+ * The element is sized like a replaced element such as `<video>`: a block-level box that the
738
+ * page's CSS controls, 300x150 by default. The application's canvas always fills the element,
739
+ * and the drawing buffer resolution follows the element's size (capped by `max-pixel-ratio`),
740
+ * tracked live via a ResizeObserver — so the element can be embedded at any size, resized by
741
+ * its container, or made fullscreen with ordinary CSS such as `width: 100vw; height: 100dvh`.
742
+ *
706
743
  * @fires {ProgressEvent} progress - Fired while the application preloads its assets. `loaded` and
707
744
  * `total` are asset counts, not bytes, and an asset that fails to load still counts as loaded.
708
745
  * Fired at least once per boot, and the final event always has `loaded` equal to `total`. Does
709
746
  * not bubble.
747
+ *
748
+ * @fires {ErrorEvent} error - Fired when the application cannot boot because no graphics device
749
+ * could be created (for example, a browser with WebGL disabled). `message` names the requested
750
+ * backends and `error` holds the underlying failure. The element never becomes ready
751
+ * and `app` stays `null` — listen for this event to show a fallback UI. Removing the element and
752
+ * re-inserting it retries the boot with its current attributes. Does not bubble.
710
753
  */
711
754
  class AppElement extends AsyncElement {
712
755
  /**
@@ -766,6 +809,12 @@ class AppElement extends AsyncElement {
766
809
  };
767
810
  _app = null;
768
811
  _loadProgress = 0;
812
+ /**
813
+ * Tracks the element's box so the drawing buffer and picker follow it. Created per boot once
814
+ * the application exists, and disconnected on teardown. `null` where ResizeObserver is
815
+ * unavailable (jsdom), where the boot-time resolution set is the only sizing that happens.
816
+ */
817
+ _resizeObserver = null;
769
818
  /**
770
819
  * The PlayCanvas application instance. `null` until the element is ready, and again once it
771
820
  * has been removed from the document — await {@link whenReady} or the element's `ready()`
@@ -792,8 +841,6 @@ class AppElement extends AsyncElement {
792
841
  */
793
842
  constructor() {
794
843
  super();
795
- // Bind methods to maintain 'this' context
796
- this._onWindowResize = this._onWindowResize.bind(this);
797
844
  // Track pointer listeners being added to and removed from descendant entities.
798
845
  // Registered once here rather than on every boot - the handlers no-op while there is no
799
846
  // canvas, and a re-booted element must not stack a second set.
@@ -804,6 +851,9 @@ class AppElement extends AsyncElement {
804
851
  }
805
852
  async connectedCallback() {
806
853
  const generation = ++this._bootGeneration;
854
+ // Installed before the loading bar is created: the bar anchors to this element, which
855
+ // these styles make a positioned block box
856
+ ensureBaseStyles();
807
857
  // Created before the first await, so the bar is visible while modules and the graphics
808
858
  // device are created, and exists before any disconnect could need to clean it up
809
859
  if (this._loadingBar && !this._bar) {
@@ -818,8 +868,11 @@ class AppElement extends AsyncElement {
818
868
  if (generation !== this._bootGeneration) {
819
869
  return;
820
870
  }
821
- // Create and append the canvas to the element
871
+ // Create and append the canvas, filling the element's content box - the page sizes the
872
+ // element, and everything else follows. touch-action: none keeps touch drags driving the
873
+ // engine's input handlers instead of scrolling the page.
822
874
  this._canvas = document.createElement('canvas');
875
+ this._canvas.style.cssText = 'display: block; width: 100%; height: 100%; touch-action: none;';
823
876
  this.appendChild(this._canvas);
824
877
  // Configure device types based on backend selection
825
878
  const backendToDeviceTypes = {
@@ -829,14 +882,44 @@ class AppElement extends AsyncElement {
829
882
  };
830
883
  const deviceTypes = backendToDeviceTypes[this._backend] || [];
831
884
  this._optionsLocked = true;
832
- const device = await createGraphicsDevice(this._canvas, {
833
- // @ts-ignore - alpha needs to be documented
834
- alpha: this._alpha,
835
- antialias: this._antialias,
836
- depth: this._depthBuffer,
837
- deviceTypes: deviceTypes,
838
- stencil: this._stencilBuffer
839
- });
885
+ // createGraphicsDevice appends its final null-device fallback to the array in place, so
886
+ // the requested list is captured now for the failure message.
887
+ const requested = deviceTypes.join(', ');
888
+ let device;
889
+ try {
890
+ device = await createGraphicsDevice(this._canvas, {
891
+ // @ts-ignore - alpha needs to be documented
892
+ alpha: this._alpha,
893
+ antialias: this._antialias,
894
+ depth: this._depthBuffer,
895
+ deviceTypes: deviceTypes,
896
+ stencil: this._stencilBuffer
897
+ });
898
+ }
899
+ catch (error) {
900
+ // The element may have been removed while device creation was failing. The teardown
901
+ // has already cleaned up, and the failure belongs to a boot that no longer owns the
902
+ // element.
903
+ if (generation !== this._bootGeneration) {
904
+ return;
905
+ }
906
+ // Return the element to its pre-boot state - no dead canvas, no loading bar stuck at
907
+ // zero - before announcing the failure. Readiness deliberately stays pending: nothing
908
+ // it would announce (the app, the entity hierarchy) exists, so a device-less element
909
+ // joins the documented never-ready cases and the failure surfaces through the error
910
+ // event instead.
911
+ if (this._canvas && this.contains(this._canvas)) {
912
+ this.removeChild(this._canvas);
913
+ }
914
+ this._canvas = null;
915
+ this._bar?.destroy();
916
+ this._bar = null;
917
+ const reason = error instanceof Error ? error.message : String(error);
918
+ const message = `pc-app failed to create a graphics device (${requested}) - ${reason}`;
919
+ console.error(message, error);
920
+ this.dispatchEvent(new ErrorEvent('error', { message, error }));
921
+ return;
922
+ }
840
923
  // The element may have been removed while the device was created. disconnectedCallback
841
924
  // has already cleaned up the canvas; the device was created inside the await, so it is
842
925
  // this boot's to release.
@@ -915,9 +998,19 @@ class AppElement extends AsyncElement {
915
998
  const app = new AppBase(this._canvas);
916
999
  this._app = app;
917
1000
  app.init(createOptions);
918
- app.setCanvasFillMode(FILLMODE_FILL_WINDOW);
1001
+ // FILLMODE_NONE leaves the canvas's CSS sizing alone (the engine's other fill modes
1002
+ // stamp window-derived pixel sizes onto it); RESOLUTION_AUTO sizes the drawing buffer
1003
+ // from the canvas's client size
1004
+ app.setCanvasFillMode(FILLMODE_NONE);
919
1005
  app.setCanvasResolution(RESOLUTION_AUTO);
920
1006
  this._pickerCreate();
1007
+ // Track the element's box rather than the window: containers resize without any window
1008
+ // event (splitter drags, flex reflow, animations). Guarded because jsdom has no
1009
+ // ResizeObserver - there, the resolution set above is the only sizing that happens.
1010
+ if (typeof ResizeObserver !== 'undefined') {
1011
+ this._resizeObserver = new ResizeObserver(() => this._syncCanvasSize());
1012
+ this._resizeObserver.observe(this);
1013
+ }
921
1014
  // Get all pc-asset elements that are direct children of the pc-app element
922
1015
  const assetElements = this.querySelectorAll(':scope > pc-asset');
923
1016
  for (const assetElement of Array.from(assetElements)) {
@@ -993,8 +1086,6 @@ class AppElement extends AsyncElement {
993
1086
  // Dismiss the bar only once a frame has actually rendered; ready fires before the
994
1087
  // first rAF tick
995
1088
  app.once('frameend', () => this._bar?.complete());
996
- // Handle window resize to keep the canvas responsive
997
- window.addEventListener('resize', this._onWindowResize);
998
1089
  this._onReady();
999
1090
  });
1000
1091
  }
@@ -1019,18 +1110,27 @@ class AppElement extends AsyncElement {
1019
1110
  // no longer holds.
1020
1111
  this._hierarchyReady = false;
1021
1112
  this._resetReady();
1022
- // Remove event listeners
1023
- window.removeEventListener('resize', this._onWindowResize);
1113
+ // Stop tracking the element's size
1114
+ this._resizeObserver?.disconnect();
1115
+ this._resizeObserver = null;
1024
1116
  // Remove the canvas
1025
1117
  if (this._canvas && this.contains(this._canvas)) {
1026
1118
  this.removeChild(this._canvas);
1027
1119
  this._canvas = null;
1028
1120
  }
1029
1121
  }
1030
- _onWindowResize() {
1031
- if (this.app) {
1032
- this.app.resizeCanvas();
1122
+ /**
1123
+ * Syncs the drawing buffer and the picker to the canvas's current CSS size. The picker must
1124
+ * track the buffer, or picks would land at stale coordinates after a resize. Skipped while
1125
+ * an XR session presents - the session owns the buffer size.
1126
+ */
1127
+ _syncCanvasSize() {
1128
+ if (!this.app || this.app.xr?.active) {
1129
+ return;
1033
1130
  }
1131
+ this.app.updateCanvasSize();
1132
+ const { width, height } = this.app.graphicsDevice;
1133
+ this._picker?.resize(width, height);
1034
1134
  }
1035
1135
  _pickerCreate() {
1036
1136
  const { width, height } = this.app.graphicsDevice;
@@ -1148,21 +1248,63 @@ class AppElement extends AsyncElement {
1148
1248
  }
1149
1249
  return null;
1150
1250
  }
1151
- // New helper to convert CSS coordinates to canvas (picker) coordinates
1152
- _getPickerCoordinates(event) {
1153
- // Get the canvas' bounding rectangle in CSS pixels.
1154
- const canvasRect = this._canvas.getBoundingClientRect();
1155
- // Compute scale factors based on canvas actual resolution vs. its CSS display size.
1156
- const scaleX = this._canvas.width / canvasRect.width;
1157
- const scaleY = this._canvas.height / canvasRect.height;
1158
- // Convert the client coordinates accordingly.
1159
- const x = (event.clientX - canvasRect.left) * scaleX;
1160
- const y = (event.clientY - canvasRect.top) * scaleY;
1161
- return { x, y };
1251
+ /**
1252
+ * Converts a pointer event's client coordinates into drawing-buffer coordinates - the space
1253
+ * the pick buffer and the camera viewports are laid out in. When the canvas has no CSS box
1254
+ * to map through (jsdom; a hidden canvas receives no pointer events in a browser), the
1255
+ * client coordinates are passed through unmapped and `mapped` is false, so callers know the
1256
+ * coordinates correspond to no real geometry.
1257
+ *
1258
+ * @param event - The pointer event to convert.
1259
+ * @param canvas - The canvas the event was dispatched on.
1260
+ * @returns The buffer-space coordinates, and whether they were actually mapped.
1261
+ */
1262
+ _getPickerCoordinates(event, canvas) {
1263
+ const canvasRect = canvas.getBoundingClientRect();
1264
+ if (canvasRect.width === 0 || canvasRect.height === 0) {
1265
+ return { x: event.clientX, y: event.clientY, mapped: false };
1266
+ }
1267
+ const scaleX = canvas.width / canvasRect.width;
1268
+ const scaleY = canvas.height / canvasRect.height;
1269
+ return {
1270
+ x: (event.clientX - canvasRect.left) * scaleX,
1271
+ y: (event.clientY - canvasRect.top) * scaleY,
1272
+ mapped: true
1273
+ };
1274
+ }
1275
+ /**
1276
+ * Whether a camera's viewport contains the point. A camera renders into its normalized
1277
+ * `rect`, whose origin is the bottom-left of the canvas while buffer coordinates run from
1278
+ * the top-left - so the vertical test flips, as the engine's ElementInput flips it for UI
1279
+ * input. The right and bottom edges are exclusive: a viewport rasterizes the half-open
1280
+ * pixel range [left, right) x [top, bottom), so a coordinate on a shared edge belongs to
1281
+ * the viewport whose first pixel it is - never to the one it just left, whose pick buffer
1282
+ * holds nothing there.
1283
+ *
1284
+ * @param camera - The camera to test.
1285
+ * @param x - The x coordinate, in buffer space.
1286
+ * @param y - The y coordinate, in buffer space.
1287
+ * @param canvas - The canvas the coordinates are relative to.
1288
+ * @returns Whether the camera's viewport contains the point.
1289
+ */
1290
+ _cameraContains(camera, x, y, canvas) {
1291
+ const rect = camera.rect;
1292
+ const left = rect.x * canvas.width;
1293
+ const bottom = (1 - rect.y) * canvas.height;
1294
+ const top = bottom - rect.w * canvas.height;
1295
+ return x >= left && x < left + rect.z * canvas.width && y >= top && y < bottom;
1162
1296
  }
1163
1297
  /**
1164
1298
  * Picks the scene under the pointer and returns the graph node that was hit, or `null`.
1165
1299
  *
1300
+ * The camera is resolved the way the engine's ElementInput resolves it for UI input:
1301
+ * enabled cameras are tried topmost-first (they render in ascending `priority` order),
1302
+ * skipping cameras that render to a texture and cameras whose viewport `rect` does not
1303
+ * contain the pointer. A camera that picks nothing ends the search if it clears the color
1304
+ * buffer - its background visually owns the pixel - and otherwise cedes to the cameras
1305
+ * beneath it, so an overlay camera only intercepts picks where it actually drew something.
1306
+ * The pick buffer is prepared per camera, so each camera picks from its own layers.
1307
+ *
1166
1308
  * The read back is asynchronous because the synchronous {@link Picker.getSelection} is not
1167
1309
  * supported on WebGPU, where it returns an empty selection rather than failing - which
1168
1310
  * silently disabled every `onpointer*` handler once WebGPU became the resolved backend. The
@@ -1172,16 +1314,40 @@ class AppElement extends AsyncElement {
1172
1314
  * @returns The graph node under the pointer, or `null` if nothing was hit.
1173
1315
  */
1174
1316
  async _pickNode(event) {
1175
- const camera = this.app.root.findComponent('camera');
1176
- if (!camera)
1177
- return null;
1178
- const { x, y } = this._getPickerCoordinates(event);
1179
- this._picker.prepare(camera, this.app.scene);
1180
- const selection = await this._picker.getSelectionAsync(x, y);
1181
- if (selection.length === 0)
1317
+ const app = this.app;
1318
+ const picker = this._picker;
1319
+ const canvas = this._canvas;
1320
+ if (!app || !picker || !canvas)
1182
1321
  return null;
1183
- const item = selection[0];
1184
- return item instanceof MeshInstance ? item.node : item.entity;
1322
+ const { x, y, mapped } = this._getPickerCoordinates(event, canvas);
1323
+ // Walked from the end: the array is sorted by ascending priority, so the last camera
1324
+ // renders last and sits on top. Read through .at() because a pick handler may remove
1325
+ // cameras while an earlier iteration's read back is in flight.
1326
+ const cameras = app.systems.camera?.cameras ?? [];
1327
+ for (let i = cameras.length - 1; i >= 0; i--) {
1328
+ const camera = cameras.at(i);
1329
+ // A camera rendering to a texture is not on the canvas.
1330
+ if (!camera || camera.renderTarget)
1331
+ continue;
1332
+ // Coordinates that could not be mapped cannot be tested for containment.
1333
+ if (mapped && !this._cameraContains(camera, x, y, canvas))
1334
+ continue;
1335
+ picker.prepare(camera, app.scene);
1336
+ const selection = await picker.getSelectionAsync(x, y);
1337
+ // The element may have disconnected while the read back was in flight.
1338
+ if (!this._picker || !this.app)
1339
+ return null;
1340
+ if (selection.length > 0) {
1341
+ const item = selection[0];
1342
+ return item instanceof MeshInstance ? item.node : item.entity;
1343
+ }
1344
+ // Nothing hit. A camera that clears the color buffer paints its background over
1345
+ // everything beneath it, so the miss is final; one that does not is an overlay
1346
+ // that the cameras beneath show through, so they get their turn.
1347
+ if (camera.clearColorBuffer)
1348
+ return null;
1349
+ }
1350
+ return null;
1185
1351
  }
1186
1352
  async _onPointerMove(event) {
1187
1353
  if (!this._picker || !this.app)
@@ -1368,7 +1534,7 @@ class AppElement extends AsyncElement {
1368
1534
  this._maxPixelRatio = value;
1369
1535
  if (this.app) {
1370
1536
  this.app.graphicsDevice.maxPixelRatio = value;
1371
- this.app.resizeCanvas();
1537
+ this._syncCanvasSize();
1372
1538
  }
1373
1539
  }
1374
1540
  /**
@@ -2125,6 +2291,10 @@ const processBufferView = (gltfBuffer, buffers, continuation) => {
2125
2291
  * immediately unless `lazy`. A `pc-asset` must be a direct child of `pc-app` — elements placed
2126
2292
  * elsewhere, or with an unsupported asset type, never become ready.
2127
2293
  *
2294
+ * A `lazy` asset loads on first use: the first time any element resolves it by `id` — a model,
2295
+ * a material map, a sky, a script `asset:` reference — or when the `lazy` attribute is removed,
2296
+ * whichever comes first. Until then it stays registered and unloaded.
2297
+ *
2128
2298
  * For `texture` and `textureatlas` assets, the texture options (`address-u`, `address-v`,
2129
2299
  * `min-filter`, `mag-filter`, `anisotropy`, `mipmaps`, `srgb`, `flip-y`) apply when the texture is
2130
2300
  * created and — like `lazy` — are observed: changing one updates a texture that has already
@@ -2324,7 +2494,9 @@ class AssetElement extends AsyncElement {
2324
2494
  if (type === 'sprite') {
2325
2495
  data = data ?? {};
2326
2496
  // Resolve the referenced texture atlas to its (numeric) asset id. The atlas must be
2327
- // declared before the sprite so its asset already exists in the registry.
2497
+ // declared before the sprite so its asset already exists in the registry. Resolved
2498
+ // with get, not useAsset: creation-time wiring is not a use, and the engine's
2499
+ // sprite handler loads the atlas when the sprite itself loads.
2328
2500
  const atlas = this.getAttribute('atlas') ?? data.textureAtlasAsset;
2329
2501
  if (typeof atlas === 'string') {
2330
2502
  const atlasAsset = AssetElement.get(atlas);
@@ -2485,13 +2657,18 @@ class AssetElement extends AsyncElement {
2485
2657
  return this._flipY;
2486
2658
  }
2487
2659
  /**
2488
- * Sets whether the asset should be loaded lazily.
2660
+ * Sets whether the asset should be loaded lazily. A lazy asset is registered without being
2661
+ * loaded; it loads on first use - the first time any element resolves it by `id` - or when
2662
+ * this flag is cleared on a registered asset, whichever comes first.
2489
2663
  * @param value - The lazy loading flag.
2490
2664
  */
2491
2665
  set lazy(value) {
2492
2666
  this._lazy = value;
2493
2667
  if (this.asset) {
2494
2668
  this.asset.preload = !value;
2669
+ if (!value) {
2670
+ this.asset.registry?.load(this.asset);
2671
+ }
2495
2672
  }
2496
2673
  }
2497
2674
  /**
@@ -2675,6 +2852,26 @@ class AssetElement extends AsyncElement {
2675
2852
  }
2676
2853
  }
2677
2854
  customElements.define('pc-asset', AssetElement);
2855
+ /**
2856
+ * Resolves an asset reference for use: {@link AssetElement.get}, plus starting the load of a
2857
+ * registered asset that has not begun one - a `lazy` asset. Every element that consumes assets
2858
+ * resolves its references here, which is what makes `lazy` mean load on first use without any
2859
+ * consumer having to remember the load. The load is asynchronous - callers observe the asset's
2860
+ * `load` event for the resource.
2861
+ *
2862
+ * @param id - The `id` of the `<pc-asset>` element.
2863
+ * @returns The asset, or `undefined`.
2864
+ * @internal
2865
+ */
2866
+ const useAsset = (id) => {
2867
+ const asset = AssetElement.get(id);
2868
+ // load() ignores an asset that is already loaded or loading, so repeated resolution
2869
+ // costs nothing.
2870
+ if (asset) {
2871
+ asset.registry?.load(asset);
2872
+ }
2873
+ return asset;
2874
+ };
2678
2875
 
2679
2876
  /**
2680
2877
  * Represents a component in the PlayCanvas engine.
@@ -2958,15 +3155,15 @@ class ButtonComponentElement extends ComponentElement {
2958
3155
  if (imageEntity) {
2959
3156
  data.imageEntity = imageEntity;
2960
3157
  }
2961
- const hoverSpriteAsset = AssetElement.get(this._hoverSpriteAsset);
3158
+ const hoverSpriteAsset = useAsset(this._hoverSpriteAsset);
2962
3159
  if (hoverSpriteAsset) {
2963
3160
  data.hoverSpriteAsset = hoverSpriteAsset.id;
2964
3161
  }
2965
- const pressedSpriteAsset = AssetElement.get(this._pressedSpriteAsset);
3162
+ const pressedSpriteAsset = useAsset(this._pressedSpriteAsset);
2966
3163
  if (pressedSpriteAsset) {
2967
3164
  data.pressedSpriteAsset = pressedSpriteAsset.id;
2968
3165
  }
2969
- const inactiveSpriteAsset = AssetElement.get(this._inactiveSpriteAsset);
3166
+ const inactiveSpriteAsset = useAsset(this._inactiveSpriteAsset);
2970
3167
  if (inactiveSpriteAsset) {
2971
3168
  data.inactiveSpriteAsset = inactiveSpriteAsset.id;
2972
3169
  }
@@ -3128,7 +3325,7 @@ class ButtonComponentElement extends ComponentElement {
3128
3325
  */
3129
3326
  set hoverSpriteAsset(value) {
3130
3327
  this._hoverSpriteAsset = value;
3131
- const asset = AssetElement.get(value);
3328
+ const asset = useAsset(value);
3132
3329
  if (this.component && asset) {
3133
3330
  this.component.hoverSpriteAsset = asset.id;
3134
3331
  }
@@ -3164,7 +3361,7 @@ class ButtonComponentElement extends ComponentElement {
3164
3361
  */
3165
3362
  set pressedSpriteAsset(value) {
3166
3363
  this._pressedSpriteAsset = value;
3167
- const asset = AssetElement.get(value);
3364
+ const asset = useAsset(value);
3168
3365
  if (this.component && asset) {
3169
3366
  this.component.pressedSpriteAsset = asset.id;
3170
3367
  }
@@ -3200,7 +3397,7 @@ class ButtonComponentElement extends ComponentElement {
3200
3397
  */
3201
3398
  set inactiveSpriteAsset(value) {
3202
3399
  this._inactiveSpriteAsset = value;
3203
- const asset = AssetElement.get(value);
3400
+ const asset = useAsset(value);
3204
3401
  if (this.component && asset) {
3205
3402
  this.component.inactiveSpriteAsset = asset.id;
3206
3403
  }
@@ -4058,15 +4255,15 @@ class ElementComponentElement extends ComponentElement {
4058
4255
  };
4059
4256
  // Asset references are resolved from `<pc-asset>` element ids to engine asset ids. They are
4060
4257
  // only included when they resolve, so image/group elements (with no font) don't error.
4061
- const fontAsset = AssetElement.get(this._fontAsset);
4258
+ const fontAsset = useAsset(this._fontAsset);
4062
4259
  if (fontAsset) {
4063
4260
  data.fontAsset = fontAsset.id;
4064
4261
  }
4065
- const spriteAsset = AssetElement.get(this._spriteAsset);
4262
+ const spriteAsset = useAsset(this._spriteAsset);
4066
4263
  if (spriteAsset) {
4067
4264
  data.spriteAsset = spriteAsset.id;
4068
4265
  }
4069
- const textureAsset = AssetElement.get(this._textureAsset);
4266
+ const textureAsset = useAsset(this._textureAsset);
4070
4267
  if (textureAsset) {
4071
4268
  data.textureAsset = textureAsset.id;
4072
4269
  }
@@ -4180,7 +4377,7 @@ class ElementComponentElement extends ComponentElement {
4180
4377
  */
4181
4378
  set fontAsset(value) {
4182
4379
  this._fontAsset = value;
4183
- const asset = AssetElement.get(value);
4380
+ const asset = useAsset(value);
4184
4381
  if (this.component && asset) {
4185
4382
  this.component.fontAsset = asset.id;
4186
4383
  }
@@ -4335,7 +4532,7 @@ class ElementComponentElement extends ComponentElement {
4335
4532
  */
4336
4533
  set spriteAsset(value) {
4337
4534
  this._spriteAsset = value;
4338
- const asset = AssetElement.get(value);
4535
+ const asset = useAsset(value);
4339
4536
  if (this.component && asset) {
4340
4537
  this.component.spriteAsset = asset.id;
4341
4538
  }
@@ -4387,7 +4584,7 @@ class ElementComponentElement extends ComponentElement {
4387
4584
  */
4388
4585
  set textureAsset(value) {
4389
4586
  this._textureAsset = value;
4390
- const asset = AssetElement.get(value);
4587
+ const asset = useAsset(value);
4391
4588
  if (this.component && asset) {
4392
4589
  this.component.textureAsset = asset.id;
4393
4590
  }
@@ -5622,13 +5819,14 @@ class ParticleSystemComponentElement extends ComponentElement {
5622
5819
  super('particlesystem');
5623
5820
  }
5624
5821
  getInitialComponentData() {
5625
- const asset = AssetElement.get(this._asset);
5626
- if (!asset) {
5822
+ const asset = useAsset(this._asset);
5823
+ // A lazy config has no resource yet - _loadAsset applies it once the load completes
5824
+ if (!asset || !asset.resource) {
5627
5825
  return {};
5628
5826
  }
5629
5827
  if (asset.resource.colorMapAsset) {
5630
5828
  const id = asset.resource.colorMapAsset;
5631
- const colorMapAsset = AssetElement.get(id)?.id;
5829
+ const colorMapAsset = useAsset(id)?.id;
5632
5830
  if (colorMapAsset) {
5633
5831
  asset.resource.colorMapAsset = colorMapAsset;
5634
5832
  }
@@ -5654,9 +5852,8 @@ class ParticleSystemComponentElement extends ComponentElement {
5654
5852
  }
5655
5853
  }
5656
5854
  async _loadAsset() {
5657
- const appElement = await this.closestApp?.ready();
5658
- const app = appElement?.app;
5659
- const asset = AssetElement.get(this._asset);
5855
+ await this.closestApp?.ready();
5856
+ const asset = useAsset(this._asset);
5660
5857
  if (!asset) {
5661
5858
  return;
5662
5859
  }
@@ -5667,7 +5864,6 @@ class ParticleSystemComponentElement extends ComponentElement {
5667
5864
  asset.once('load', () => {
5668
5865
  this.applyConfig(asset.resource);
5669
5866
  });
5670
- app.assets.load(asset);
5671
5867
  }
5672
5868
  }
5673
5869
  /**
@@ -6083,7 +6279,7 @@ class MaterialElement extends HTMLElement {
6083
6279
  this._scheduleUpdate();
6084
6280
  return;
6085
6281
  }
6086
- const asset = AssetElement.get(id);
6282
+ const asset = useAsset(id);
6087
6283
  if (!asset)
6088
6284
  return;
6089
6285
  if (asset.loaded) {
@@ -9184,7 +9380,7 @@ const camelToKebab = (name) => {
9184
9380
  * @returns The asset, or `raw`.
9185
9381
  */
9186
9382
  const assetConversion = (rest, raw) => {
9187
- const asset = AssetElement.get(rest);
9383
+ const asset = useAsset(rest);
9188
9384
  if (asset) {
9189
9385
  return asset;
9190
9386
  }
@@ -10004,7 +10200,7 @@ class SoundSlotElement extends AsyncElement {
10004
10200
  set asset(value) {
10005
10201
  this._asset = value;
10006
10202
  if (this.soundSlot) {
10007
- const id = AssetElement.get(value)?.id;
10203
+ const id = useAsset(value)?.id;
10008
10204
  if (id) {
10009
10205
  this.soundSlot.asset = id;
10010
10206
  }
@@ -10211,7 +10407,7 @@ class GSplatComponentElement extends ComponentElement {
10211
10407
  }
10212
10408
  getInitialComponentData() {
10213
10409
  return {
10214
- asset: AssetElement.get(this._asset),
10410
+ asset: useAsset(this._asset),
10215
10411
  castShadows: this._castShadows,
10216
10412
  lodBaseDistance: this._lodBaseDistance,
10217
10413
  lodMultiplier: this._lodMultiplier,
@@ -10232,7 +10428,7 @@ class GSplatComponentElement extends ComponentElement {
10232
10428
  */
10233
10429
  set asset(value) {
10234
10430
  this._asset = value;
10235
- const asset = AssetElement.get(value);
10431
+ const asset = useAsset(value);
10236
10432
  if (this.component && asset) {
10237
10433
  this.component.asset = asset;
10238
10434
  }
@@ -10510,8 +10706,7 @@ class ModelElement extends AsyncElement {
10510
10706
  if (generation !== this._loadGeneration) {
10511
10707
  return;
10512
10708
  }
10513
- const app = appElement.app;
10514
- const asset = AssetElement.get(this._asset);
10709
+ const asset = useAsset(this._asset);
10515
10710
  if (!asset) {
10516
10711
  // An empty id is a legitimate transient (the asset may be assigned later); a
10517
10712
  // non-empty one that resolves to nothing is a dead end - say so rather than staying
@@ -10547,7 +10742,6 @@ class ModelElement extends AsyncElement {
10547
10742
  }));
10548
10743
  this._onReady();
10549
10744
  });
10550
- app.assets.load(asset);
10551
10745
  }
10552
10746
  }
10553
10747
  _unloadModel() {
@@ -11512,7 +11706,7 @@ class SkyElement extends AsyncElement {
11512
11706
  return;
11513
11707
  }
11514
11708
  this._appElement = appElement;
11515
- const asset = AssetElement.get(this._asset);
11709
+ const asset = useAsset(this._asset);
11516
11710
  if (!asset) {
11517
11711
  return;
11518
11712
  }
@@ -11531,7 +11725,6 @@ class SkyElement extends AsyncElement {
11531
11725
  }
11532
11726
  this._generateSkybox(asset);
11533
11727
  });
11534
- app.assets.load(asset);
11535
11728
  }
11536
11729
  }
11537
11730
  _unloadSkybox() {