@playcanvas/web-components 0.13.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.
package/dist/pwc.mjs CHANGED
@@ -1248,21 +1248,63 @@ class AppElement extends AsyncElement {
1248
1248
  }
1249
1249
  return null;
1250
1250
  }
1251
- // New helper to convert CSS coordinates to canvas (picker) coordinates
1252
- _getPickerCoordinates(event) {
1253
- // Get the canvas' bounding rectangle in CSS pixels.
1254
- const canvasRect = this._canvas.getBoundingClientRect();
1255
- // Compute scale factors based on canvas actual resolution vs. its CSS display size.
1256
- const scaleX = this._canvas.width / canvasRect.width;
1257
- const scaleY = this._canvas.height / canvasRect.height;
1258
- // Convert the client coordinates accordingly.
1259
- const x = (event.clientX - canvasRect.left) * scaleX;
1260
- const y = (event.clientY - canvasRect.top) * scaleY;
1261
- 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;
1262
1296
  }
1263
1297
  /**
1264
1298
  * Picks the scene under the pointer and returns the graph node that was hit, or `null`.
1265
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
+ *
1266
1308
  * The read back is asynchronous because the synchronous {@link Picker.getSelection} is not
1267
1309
  * supported on WebGPU, where it returns an empty selection rather than failing - which
1268
1310
  * silently disabled every `onpointer*` handler once WebGPU became the resolved backend. The
@@ -1272,16 +1314,40 @@ class AppElement extends AsyncElement {
1272
1314
  * @returns The graph node under the pointer, or `null` if nothing was hit.
1273
1315
  */
1274
1316
  async _pickNode(event) {
1275
- const camera = this.app.root.findComponent('camera');
1276
- if (!camera)
1277
- return null;
1278
- const { x, y } = this._getPickerCoordinates(event);
1279
- this._picker.prepare(camera, this.app.scene);
1280
- const selection = await this._picker.getSelectionAsync(x, y);
1281
- if (selection.length === 0)
1317
+ const app = this.app;
1318
+ const picker = this._picker;
1319
+ const canvas = this._canvas;
1320
+ if (!app || !picker || !canvas)
1282
1321
  return null;
1283
- const item = selection[0];
1284
- 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;
1285
1351
  }
1286
1352
  async _onPointerMove(event) {
1287
1353
  if (!this._picker || !this.app)
@@ -2225,6 +2291,10 @@ const processBufferView = (gltfBuffer, buffers, continuation) => {
2225
2291
  * immediately unless `lazy`. A `pc-asset` must be a direct child of `pc-app` — elements placed
2226
2292
  * elsewhere, or with an unsupported asset type, never become ready.
2227
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
+ *
2228
2298
  * For `texture` and `textureatlas` assets, the texture options (`address-u`, `address-v`,
2229
2299
  * `min-filter`, `mag-filter`, `anisotropy`, `mipmaps`, `srgb`, `flip-y`) apply when the texture is
2230
2300
  * created and — like `lazy` — are observed: changing one updates a texture that has already
@@ -2424,7 +2494,9 @@ class AssetElement extends AsyncElement {
2424
2494
  if (type === 'sprite') {
2425
2495
  data = data ?? {};
2426
2496
  // Resolve the referenced texture atlas to its (numeric) asset id. The atlas must be
2427
- // 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.
2428
2500
  const atlas = this.getAttribute('atlas') ?? data.textureAtlasAsset;
2429
2501
  if (typeof atlas === 'string') {
2430
2502
  const atlasAsset = AssetElement.get(atlas);
@@ -2585,13 +2657,18 @@ class AssetElement extends AsyncElement {
2585
2657
  return this._flipY;
2586
2658
  }
2587
2659
  /**
2588
- * 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.
2589
2663
  * @param value - The lazy loading flag.
2590
2664
  */
2591
2665
  set lazy(value) {
2592
2666
  this._lazy = value;
2593
2667
  if (this.asset) {
2594
2668
  this.asset.preload = !value;
2669
+ if (!value) {
2670
+ this.asset.registry?.load(this.asset);
2671
+ }
2595
2672
  }
2596
2673
  }
2597
2674
  /**
@@ -2775,6 +2852,26 @@ class AssetElement extends AsyncElement {
2775
2852
  }
2776
2853
  }
2777
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
+ };
2778
2875
 
2779
2876
  /**
2780
2877
  * Represents a component in the PlayCanvas engine.
@@ -3058,15 +3155,15 @@ class ButtonComponentElement extends ComponentElement {
3058
3155
  if (imageEntity) {
3059
3156
  data.imageEntity = imageEntity;
3060
3157
  }
3061
- const hoverSpriteAsset = AssetElement.get(this._hoverSpriteAsset);
3158
+ const hoverSpriteAsset = useAsset(this._hoverSpriteAsset);
3062
3159
  if (hoverSpriteAsset) {
3063
3160
  data.hoverSpriteAsset = hoverSpriteAsset.id;
3064
3161
  }
3065
- const pressedSpriteAsset = AssetElement.get(this._pressedSpriteAsset);
3162
+ const pressedSpriteAsset = useAsset(this._pressedSpriteAsset);
3066
3163
  if (pressedSpriteAsset) {
3067
3164
  data.pressedSpriteAsset = pressedSpriteAsset.id;
3068
3165
  }
3069
- const inactiveSpriteAsset = AssetElement.get(this._inactiveSpriteAsset);
3166
+ const inactiveSpriteAsset = useAsset(this._inactiveSpriteAsset);
3070
3167
  if (inactiveSpriteAsset) {
3071
3168
  data.inactiveSpriteAsset = inactiveSpriteAsset.id;
3072
3169
  }
@@ -3228,7 +3325,7 @@ class ButtonComponentElement extends ComponentElement {
3228
3325
  */
3229
3326
  set hoverSpriteAsset(value) {
3230
3327
  this._hoverSpriteAsset = value;
3231
- const asset = AssetElement.get(value);
3328
+ const asset = useAsset(value);
3232
3329
  if (this.component && asset) {
3233
3330
  this.component.hoverSpriteAsset = asset.id;
3234
3331
  }
@@ -3264,7 +3361,7 @@ class ButtonComponentElement extends ComponentElement {
3264
3361
  */
3265
3362
  set pressedSpriteAsset(value) {
3266
3363
  this._pressedSpriteAsset = value;
3267
- const asset = AssetElement.get(value);
3364
+ const asset = useAsset(value);
3268
3365
  if (this.component && asset) {
3269
3366
  this.component.pressedSpriteAsset = asset.id;
3270
3367
  }
@@ -3300,7 +3397,7 @@ class ButtonComponentElement extends ComponentElement {
3300
3397
  */
3301
3398
  set inactiveSpriteAsset(value) {
3302
3399
  this._inactiveSpriteAsset = value;
3303
- const asset = AssetElement.get(value);
3400
+ const asset = useAsset(value);
3304
3401
  if (this.component && asset) {
3305
3402
  this.component.inactiveSpriteAsset = asset.id;
3306
3403
  }
@@ -4158,15 +4255,15 @@ class ElementComponentElement extends ComponentElement {
4158
4255
  };
4159
4256
  // Asset references are resolved from `<pc-asset>` element ids to engine asset ids. They are
4160
4257
  // only included when they resolve, so image/group elements (with no font) don't error.
4161
- const fontAsset = AssetElement.get(this._fontAsset);
4258
+ const fontAsset = useAsset(this._fontAsset);
4162
4259
  if (fontAsset) {
4163
4260
  data.fontAsset = fontAsset.id;
4164
4261
  }
4165
- const spriteAsset = AssetElement.get(this._spriteAsset);
4262
+ const spriteAsset = useAsset(this._spriteAsset);
4166
4263
  if (spriteAsset) {
4167
4264
  data.spriteAsset = spriteAsset.id;
4168
4265
  }
4169
- const textureAsset = AssetElement.get(this._textureAsset);
4266
+ const textureAsset = useAsset(this._textureAsset);
4170
4267
  if (textureAsset) {
4171
4268
  data.textureAsset = textureAsset.id;
4172
4269
  }
@@ -4280,7 +4377,7 @@ class ElementComponentElement extends ComponentElement {
4280
4377
  */
4281
4378
  set fontAsset(value) {
4282
4379
  this._fontAsset = value;
4283
- const asset = AssetElement.get(value);
4380
+ const asset = useAsset(value);
4284
4381
  if (this.component && asset) {
4285
4382
  this.component.fontAsset = asset.id;
4286
4383
  }
@@ -4435,7 +4532,7 @@ class ElementComponentElement extends ComponentElement {
4435
4532
  */
4436
4533
  set spriteAsset(value) {
4437
4534
  this._spriteAsset = value;
4438
- const asset = AssetElement.get(value);
4535
+ const asset = useAsset(value);
4439
4536
  if (this.component && asset) {
4440
4537
  this.component.spriteAsset = asset.id;
4441
4538
  }
@@ -4487,7 +4584,7 @@ class ElementComponentElement extends ComponentElement {
4487
4584
  */
4488
4585
  set textureAsset(value) {
4489
4586
  this._textureAsset = value;
4490
- const asset = AssetElement.get(value);
4587
+ const asset = useAsset(value);
4491
4588
  if (this.component && asset) {
4492
4589
  this.component.textureAsset = asset.id;
4493
4590
  }
@@ -5722,13 +5819,14 @@ class ParticleSystemComponentElement extends ComponentElement {
5722
5819
  super('particlesystem');
5723
5820
  }
5724
5821
  getInitialComponentData() {
5725
- const asset = AssetElement.get(this._asset);
5726
- 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) {
5727
5825
  return {};
5728
5826
  }
5729
5827
  if (asset.resource.colorMapAsset) {
5730
5828
  const id = asset.resource.colorMapAsset;
5731
- const colorMapAsset = AssetElement.get(id)?.id;
5829
+ const colorMapAsset = useAsset(id)?.id;
5732
5830
  if (colorMapAsset) {
5733
5831
  asset.resource.colorMapAsset = colorMapAsset;
5734
5832
  }
@@ -5754,9 +5852,8 @@ class ParticleSystemComponentElement extends ComponentElement {
5754
5852
  }
5755
5853
  }
5756
5854
  async _loadAsset() {
5757
- const appElement = await this.closestApp?.ready();
5758
- const app = appElement?.app;
5759
- const asset = AssetElement.get(this._asset);
5855
+ await this.closestApp?.ready();
5856
+ const asset = useAsset(this._asset);
5760
5857
  if (!asset) {
5761
5858
  return;
5762
5859
  }
@@ -5767,7 +5864,6 @@ class ParticleSystemComponentElement extends ComponentElement {
5767
5864
  asset.once('load', () => {
5768
5865
  this.applyConfig(asset.resource);
5769
5866
  });
5770
- app.assets.load(asset);
5771
5867
  }
5772
5868
  }
5773
5869
  /**
@@ -6183,7 +6279,7 @@ class MaterialElement extends HTMLElement {
6183
6279
  this._scheduleUpdate();
6184
6280
  return;
6185
6281
  }
6186
- const asset = AssetElement.get(id);
6282
+ const asset = useAsset(id);
6187
6283
  if (!asset)
6188
6284
  return;
6189
6285
  if (asset.loaded) {
@@ -9284,7 +9380,7 @@ const camelToKebab = (name) => {
9284
9380
  * @returns The asset, or `raw`.
9285
9381
  */
9286
9382
  const assetConversion = (rest, raw) => {
9287
- const asset = AssetElement.get(rest);
9383
+ const asset = useAsset(rest);
9288
9384
  if (asset) {
9289
9385
  return asset;
9290
9386
  }
@@ -10104,7 +10200,7 @@ class SoundSlotElement extends AsyncElement {
10104
10200
  set asset(value) {
10105
10201
  this._asset = value;
10106
10202
  if (this.soundSlot) {
10107
- const id = AssetElement.get(value)?.id;
10203
+ const id = useAsset(value)?.id;
10108
10204
  if (id) {
10109
10205
  this.soundSlot.asset = id;
10110
10206
  }
@@ -10311,7 +10407,7 @@ class GSplatComponentElement extends ComponentElement {
10311
10407
  }
10312
10408
  getInitialComponentData() {
10313
10409
  return {
10314
- asset: AssetElement.get(this._asset),
10410
+ asset: useAsset(this._asset),
10315
10411
  castShadows: this._castShadows,
10316
10412
  lodBaseDistance: this._lodBaseDistance,
10317
10413
  lodMultiplier: this._lodMultiplier,
@@ -10332,7 +10428,7 @@ class GSplatComponentElement extends ComponentElement {
10332
10428
  */
10333
10429
  set asset(value) {
10334
10430
  this._asset = value;
10335
- const asset = AssetElement.get(value);
10431
+ const asset = useAsset(value);
10336
10432
  if (this.component && asset) {
10337
10433
  this.component.asset = asset;
10338
10434
  }
@@ -10610,8 +10706,7 @@ class ModelElement extends AsyncElement {
10610
10706
  if (generation !== this._loadGeneration) {
10611
10707
  return;
10612
10708
  }
10613
- const app = appElement.app;
10614
- const asset = AssetElement.get(this._asset);
10709
+ const asset = useAsset(this._asset);
10615
10710
  if (!asset) {
10616
10711
  // An empty id is a legitimate transient (the asset may be assigned later); a
10617
10712
  // non-empty one that resolves to nothing is a dead end - say so rather than staying
@@ -10647,7 +10742,6 @@ class ModelElement extends AsyncElement {
10647
10742
  }));
10648
10743
  this._onReady();
10649
10744
  });
10650
- app.assets.load(asset);
10651
10745
  }
10652
10746
  }
10653
10747
  _unloadModel() {
@@ -11612,7 +11706,7 @@ class SkyElement extends AsyncElement {
11612
11706
  return;
11613
11707
  }
11614
11708
  this._appElement = appElement;
11615
- const asset = AssetElement.get(this._asset);
11709
+ const asset = useAsset(this._asset);
11616
11710
  if (!asset) {
11617
11711
  return;
11618
11712
  }
@@ -11631,7 +11725,6 @@ class SkyElement extends AsyncElement {
11631
11725
  }
11632
11726
  this._generateSkybox(asset);
11633
11727
  });
11634
- app.assets.load(asset);
11635
11728
  }
11636
11729
  }
11637
11730
  _unloadSkybox() {