@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.js CHANGED
@@ -1252,21 +1252,63 @@
1252
1252
  }
1253
1253
  return null;
1254
1254
  }
1255
- // New helper to convert CSS coordinates to canvas (picker) coordinates
1256
- _getPickerCoordinates(event) {
1257
- // Get the canvas' bounding rectangle in CSS pixels.
1258
- const canvasRect = this._canvas.getBoundingClientRect();
1259
- // Compute scale factors based on canvas actual resolution vs. its CSS display size.
1260
- const scaleX = this._canvas.width / canvasRect.width;
1261
- const scaleY = this._canvas.height / canvasRect.height;
1262
- // Convert the client coordinates accordingly.
1263
- const x = (event.clientX - canvasRect.left) * scaleX;
1264
- const y = (event.clientY - canvasRect.top) * scaleY;
1265
- return { x, y };
1255
+ /**
1256
+ * Converts a pointer event's client coordinates into drawing-buffer coordinates - the space
1257
+ * the pick buffer and the camera viewports are laid out in. When the canvas has no CSS box
1258
+ * to map through (jsdom; a hidden canvas receives no pointer events in a browser), the
1259
+ * client coordinates are passed through unmapped and `mapped` is false, so callers know the
1260
+ * coordinates correspond to no real geometry.
1261
+ *
1262
+ * @param event - The pointer event to convert.
1263
+ * @param canvas - The canvas the event was dispatched on.
1264
+ * @returns The buffer-space coordinates, and whether they were actually mapped.
1265
+ */
1266
+ _getPickerCoordinates(event, canvas) {
1267
+ const canvasRect = canvas.getBoundingClientRect();
1268
+ if (canvasRect.width === 0 || canvasRect.height === 0) {
1269
+ return { x: event.clientX, y: event.clientY, mapped: false };
1270
+ }
1271
+ const scaleX = canvas.width / canvasRect.width;
1272
+ const scaleY = canvas.height / canvasRect.height;
1273
+ return {
1274
+ x: (event.clientX - canvasRect.left) * scaleX,
1275
+ y: (event.clientY - canvasRect.top) * scaleY,
1276
+ mapped: true
1277
+ };
1278
+ }
1279
+ /**
1280
+ * Whether a camera's viewport contains the point. A camera renders into its normalized
1281
+ * `rect`, whose origin is the bottom-left of the canvas while buffer coordinates run from
1282
+ * the top-left - so the vertical test flips, as the engine's ElementInput flips it for UI
1283
+ * input. The right and bottom edges are exclusive: a viewport rasterizes the half-open
1284
+ * pixel range [left, right) x [top, bottom), so a coordinate on a shared edge belongs to
1285
+ * the viewport whose first pixel it is - never to the one it just left, whose pick buffer
1286
+ * holds nothing there.
1287
+ *
1288
+ * @param camera - The camera to test.
1289
+ * @param x - The x coordinate, in buffer space.
1290
+ * @param y - The y coordinate, in buffer space.
1291
+ * @param canvas - The canvas the coordinates are relative to.
1292
+ * @returns Whether the camera's viewport contains the point.
1293
+ */
1294
+ _cameraContains(camera, x, y, canvas) {
1295
+ const rect = camera.rect;
1296
+ const left = rect.x * canvas.width;
1297
+ const bottom = (1 - rect.y) * canvas.height;
1298
+ const top = bottom - rect.w * canvas.height;
1299
+ return x >= left && x < left + rect.z * canvas.width && y >= top && y < bottom;
1266
1300
  }
1267
1301
  /**
1268
1302
  * Picks the scene under the pointer and returns the graph node that was hit, or `null`.
1269
1303
  *
1304
+ * The camera is resolved the way the engine's ElementInput resolves it for UI input:
1305
+ * enabled cameras are tried topmost-first (they render in ascending `priority` order),
1306
+ * skipping cameras that render to a texture and cameras whose viewport `rect` does not
1307
+ * contain the pointer. A camera that picks nothing ends the search if it clears the color
1308
+ * buffer - its background visually owns the pixel - and otherwise cedes to the cameras
1309
+ * beneath it, so an overlay camera only intercepts picks where it actually drew something.
1310
+ * The pick buffer is prepared per camera, so each camera picks from its own layers.
1311
+ *
1270
1312
  * The read back is asynchronous because the synchronous {@link Picker.getSelection} is not
1271
1313
  * supported on WebGPU, where it returns an empty selection rather than failing - which
1272
1314
  * silently disabled every `onpointer*` handler once WebGPU became the resolved backend. The
@@ -1276,16 +1318,40 @@
1276
1318
  * @returns The graph node under the pointer, or `null` if nothing was hit.
1277
1319
  */
1278
1320
  async _pickNode(event) {
1279
- const camera = this.app.root.findComponent('camera');
1280
- if (!camera)
1281
- return null;
1282
- const { x, y } = this._getPickerCoordinates(event);
1283
- this._picker.prepare(camera, this.app.scene);
1284
- const selection = await this._picker.getSelectionAsync(x, y);
1285
- if (selection.length === 0)
1321
+ const app = this.app;
1322
+ const picker = this._picker;
1323
+ const canvas = this._canvas;
1324
+ if (!app || !picker || !canvas)
1286
1325
  return null;
1287
- const item = selection[0];
1288
- return item instanceof playcanvas.MeshInstance ? item.node : item.entity;
1326
+ const { x, y, mapped } = this._getPickerCoordinates(event, canvas);
1327
+ // Walked from the end: the array is sorted by ascending priority, so the last camera
1328
+ // renders last and sits on top. Read through .at() because a pick handler may remove
1329
+ // cameras while an earlier iteration's read back is in flight.
1330
+ const cameras = app.systems.camera?.cameras ?? [];
1331
+ for (let i = cameras.length - 1; i >= 0; i--) {
1332
+ const camera = cameras.at(i);
1333
+ // A camera rendering to a texture is not on the canvas.
1334
+ if (!camera || camera.renderTarget)
1335
+ continue;
1336
+ // Coordinates that could not be mapped cannot be tested for containment.
1337
+ if (mapped && !this._cameraContains(camera, x, y, canvas))
1338
+ continue;
1339
+ picker.prepare(camera, app.scene);
1340
+ const selection = await picker.getSelectionAsync(x, y);
1341
+ // The element may have disconnected while the read back was in flight.
1342
+ if (!this._picker || !this.app)
1343
+ return null;
1344
+ if (selection.length > 0) {
1345
+ const item = selection[0];
1346
+ return item instanceof playcanvas.MeshInstance ? item.node : item.entity;
1347
+ }
1348
+ // Nothing hit. A camera that clears the color buffer paints its background over
1349
+ // everything beneath it, so the miss is final; one that does not is an overlay
1350
+ // that the cameras beneath show through, so they get their turn.
1351
+ if (camera.clearColorBuffer)
1352
+ return null;
1353
+ }
1354
+ return null;
1289
1355
  }
1290
1356
  async _onPointerMove(event) {
1291
1357
  if (!this._picker || !this.app)
@@ -2229,6 +2295,10 @@
2229
2295
  * immediately unless `lazy`. A `pc-asset` must be a direct child of `pc-app` — elements placed
2230
2296
  * elsewhere, or with an unsupported asset type, never become ready.
2231
2297
  *
2298
+ * A `lazy` asset loads on first use: the first time any element resolves it by `id` — a model,
2299
+ * a material map, a sky, a script `asset:` reference — or when the `lazy` attribute is removed,
2300
+ * whichever comes first. Until then it stays registered and unloaded.
2301
+ *
2232
2302
  * For `texture` and `textureatlas` assets, the texture options (`address-u`, `address-v`,
2233
2303
  * `min-filter`, `mag-filter`, `anisotropy`, `mipmaps`, `srgb`, `flip-y`) apply when the texture is
2234
2304
  * created and — like `lazy` — are observed: changing one updates a texture that has already
@@ -2428,7 +2498,9 @@
2428
2498
  if (type === 'sprite') {
2429
2499
  data = data ?? {};
2430
2500
  // Resolve the referenced texture atlas to its (numeric) asset id. The atlas must be
2431
- // declared before the sprite so its asset already exists in the registry.
2501
+ // declared before the sprite so its asset already exists in the registry. Resolved
2502
+ // with get, not useAsset: creation-time wiring is not a use, and the engine's
2503
+ // sprite handler loads the atlas when the sprite itself loads.
2432
2504
  const atlas = this.getAttribute('atlas') ?? data.textureAtlasAsset;
2433
2505
  if (typeof atlas === 'string') {
2434
2506
  const atlasAsset = AssetElement.get(atlas);
@@ -2589,13 +2661,18 @@
2589
2661
  return this._flipY;
2590
2662
  }
2591
2663
  /**
2592
- * Sets whether the asset should be loaded lazily.
2664
+ * Sets whether the asset should be loaded lazily. A lazy asset is registered without being
2665
+ * loaded; it loads on first use - the first time any element resolves it by `id` - or when
2666
+ * this flag is cleared on a registered asset, whichever comes first.
2593
2667
  * @param value - The lazy loading flag.
2594
2668
  */
2595
2669
  set lazy(value) {
2596
2670
  this._lazy = value;
2597
2671
  if (this.asset) {
2598
2672
  this.asset.preload = !value;
2673
+ if (!value) {
2674
+ this.asset.registry?.load(this.asset);
2675
+ }
2599
2676
  }
2600
2677
  }
2601
2678
  /**
@@ -2779,6 +2856,26 @@
2779
2856
  }
2780
2857
  }
2781
2858
  customElements.define('pc-asset', AssetElement);
2859
+ /**
2860
+ * Resolves an asset reference for use: {@link AssetElement.get}, plus starting the load of a
2861
+ * registered asset that has not begun one - a `lazy` asset. Every element that consumes assets
2862
+ * resolves its references here, which is what makes `lazy` mean load on first use without any
2863
+ * consumer having to remember the load. The load is asynchronous - callers observe the asset's
2864
+ * `load` event for the resource.
2865
+ *
2866
+ * @param id - The `id` of the `<pc-asset>` element.
2867
+ * @returns The asset, or `undefined`.
2868
+ * @internal
2869
+ */
2870
+ const useAsset = (id) => {
2871
+ const asset = AssetElement.get(id);
2872
+ // load() ignores an asset that is already loaded or loading, so repeated resolution
2873
+ // costs nothing.
2874
+ if (asset) {
2875
+ asset.registry?.load(asset);
2876
+ }
2877
+ return asset;
2878
+ };
2782
2879
 
2783
2880
  /**
2784
2881
  * Represents a component in the PlayCanvas engine.
@@ -3062,15 +3159,15 @@
3062
3159
  if (imageEntity) {
3063
3160
  data.imageEntity = imageEntity;
3064
3161
  }
3065
- const hoverSpriteAsset = AssetElement.get(this._hoverSpriteAsset);
3162
+ const hoverSpriteAsset = useAsset(this._hoverSpriteAsset);
3066
3163
  if (hoverSpriteAsset) {
3067
3164
  data.hoverSpriteAsset = hoverSpriteAsset.id;
3068
3165
  }
3069
- const pressedSpriteAsset = AssetElement.get(this._pressedSpriteAsset);
3166
+ const pressedSpriteAsset = useAsset(this._pressedSpriteAsset);
3070
3167
  if (pressedSpriteAsset) {
3071
3168
  data.pressedSpriteAsset = pressedSpriteAsset.id;
3072
3169
  }
3073
- const inactiveSpriteAsset = AssetElement.get(this._inactiveSpriteAsset);
3170
+ const inactiveSpriteAsset = useAsset(this._inactiveSpriteAsset);
3074
3171
  if (inactiveSpriteAsset) {
3075
3172
  data.inactiveSpriteAsset = inactiveSpriteAsset.id;
3076
3173
  }
@@ -3232,7 +3329,7 @@
3232
3329
  */
3233
3330
  set hoverSpriteAsset(value) {
3234
3331
  this._hoverSpriteAsset = value;
3235
- const asset = AssetElement.get(value);
3332
+ const asset = useAsset(value);
3236
3333
  if (this.component && asset) {
3237
3334
  this.component.hoverSpriteAsset = asset.id;
3238
3335
  }
@@ -3268,7 +3365,7 @@
3268
3365
  */
3269
3366
  set pressedSpriteAsset(value) {
3270
3367
  this._pressedSpriteAsset = value;
3271
- const asset = AssetElement.get(value);
3368
+ const asset = useAsset(value);
3272
3369
  if (this.component && asset) {
3273
3370
  this.component.pressedSpriteAsset = asset.id;
3274
3371
  }
@@ -3304,7 +3401,7 @@
3304
3401
  */
3305
3402
  set inactiveSpriteAsset(value) {
3306
3403
  this._inactiveSpriteAsset = value;
3307
- const asset = AssetElement.get(value);
3404
+ const asset = useAsset(value);
3308
3405
  if (this.component && asset) {
3309
3406
  this.component.inactiveSpriteAsset = asset.id;
3310
3407
  }
@@ -4162,15 +4259,15 @@
4162
4259
  };
4163
4260
  // Asset references are resolved from `<pc-asset>` element ids to engine asset ids. They are
4164
4261
  // only included when they resolve, so image/group elements (with no font) don't error.
4165
- const fontAsset = AssetElement.get(this._fontAsset);
4262
+ const fontAsset = useAsset(this._fontAsset);
4166
4263
  if (fontAsset) {
4167
4264
  data.fontAsset = fontAsset.id;
4168
4265
  }
4169
- const spriteAsset = AssetElement.get(this._spriteAsset);
4266
+ const spriteAsset = useAsset(this._spriteAsset);
4170
4267
  if (spriteAsset) {
4171
4268
  data.spriteAsset = spriteAsset.id;
4172
4269
  }
4173
- const textureAsset = AssetElement.get(this._textureAsset);
4270
+ const textureAsset = useAsset(this._textureAsset);
4174
4271
  if (textureAsset) {
4175
4272
  data.textureAsset = textureAsset.id;
4176
4273
  }
@@ -4284,7 +4381,7 @@
4284
4381
  */
4285
4382
  set fontAsset(value) {
4286
4383
  this._fontAsset = value;
4287
- const asset = AssetElement.get(value);
4384
+ const asset = useAsset(value);
4288
4385
  if (this.component && asset) {
4289
4386
  this.component.fontAsset = asset.id;
4290
4387
  }
@@ -4439,7 +4536,7 @@
4439
4536
  */
4440
4537
  set spriteAsset(value) {
4441
4538
  this._spriteAsset = value;
4442
- const asset = AssetElement.get(value);
4539
+ const asset = useAsset(value);
4443
4540
  if (this.component && asset) {
4444
4541
  this.component.spriteAsset = asset.id;
4445
4542
  }
@@ -4491,7 +4588,7 @@
4491
4588
  */
4492
4589
  set textureAsset(value) {
4493
4590
  this._textureAsset = value;
4494
- const asset = AssetElement.get(value);
4591
+ const asset = useAsset(value);
4495
4592
  if (this.component && asset) {
4496
4593
  this.component.textureAsset = asset.id;
4497
4594
  }
@@ -5726,13 +5823,14 @@
5726
5823
  super('particlesystem');
5727
5824
  }
5728
5825
  getInitialComponentData() {
5729
- const asset = AssetElement.get(this._asset);
5730
- if (!asset) {
5826
+ const asset = useAsset(this._asset);
5827
+ // A lazy config has no resource yet - _loadAsset applies it once the load completes
5828
+ if (!asset || !asset.resource) {
5731
5829
  return {};
5732
5830
  }
5733
5831
  if (asset.resource.colorMapAsset) {
5734
5832
  const id = asset.resource.colorMapAsset;
5735
- const colorMapAsset = AssetElement.get(id)?.id;
5833
+ const colorMapAsset = useAsset(id)?.id;
5736
5834
  if (colorMapAsset) {
5737
5835
  asset.resource.colorMapAsset = colorMapAsset;
5738
5836
  }
@@ -5758,9 +5856,8 @@
5758
5856
  }
5759
5857
  }
5760
5858
  async _loadAsset() {
5761
- const appElement = await this.closestApp?.ready();
5762
- const app = appElement?.app;
5763
- const asset = AssetElement.get(this._asset);
5859
+ await this.closestApp?.ready();
5860
+ const asset = useAsset(this._asset);
5764
5861
  if (!asset) {
5765
5862
  return;
5766
5863
  }
@@ -5771,7 +5868,6 @@
5771
5868
  asset.once('load', () => {
5772
5869
  this.applyConfig(asset.resource);
5773
5870
  });
5774
- app.assets.load(asset);
5775
5871
  }
5776
5872
  }
5777
5873
  /**
@@ -6187,7 +6283,7 @@
6187
6283
  this._scheduleUpdate();
6188
6284
  return;
6189
6285
  }
6190
- const asset = AssetElement.get(id);
6286
+ const asset = useAsset(id);
6191
6287
  if (!asset)
6192
6288
  return;
6193
6289
  if (asset.loaded) {
@@ -9288,7 +9384,7 @@
9288
9384
  * @returns The asset, or `raw`.
9289
9385
  */
9290
9386
  const assetConversion = (rest, raw) => {
9291
- const asset = AssetElement.get(rest);
9387
+ const asset = useAsset(rest);
9292
9388
  if (asset) {
9293
9389
  return asset;
9294
9390
  }
@@ -10108,7 +10204,7 @@
10108
10204
  set asset(value) {
10109
10205
  this._asset = value;
10110
10206
  if (this.soundSlot) {
10111
- const id = AssetElement.get(value)?.id;
10207
+ const id = useAsset(value)?.id;
10112
10208
  if (id) {
10113
10209
  this.soundSlot.asset = id;
10114
10210
  }
@@ -10315,7 +10411,7 @@
10315
10411
  }
10316
10412
  getInitialComponentData() {
10317
10413
  return {
10318
- asset: AssetElement.get(this._asset),
10414
+ asset: useAsset(this._asset),
10319
10415
  castShadows: this._castShadows,
10320
10416
  lodBaseDistance: this._lodBaseDistance,
10321
10417
  lodMultiplier: this._lodMultiplier,
@@ -10336,7 +10432,7 @@
10336
10432
  */
10337
10433
  set asset(value) {
10338
10434
  this._asset = value;
10339
- const asset = AssetElement.get(value);
10435
+ const asset = useAsset(value);
10340
10436
  if (this.component && asset) {
10341
10437
  this.component.asset = asset;
10342
10438
  }
@@ -10614,8 +10710,7 @@
10614
10710
  if (generation !== this._loadGeneration) {
10615
10711
  return;
10616
10712
  }
10617
- const app = appElement.app;
10618
- const asset = AssetElement.get(this._asset);
10713
+ const asset = useAsset(this._asset);
10619
10714
  if (!asset) {
10620
10715
  // An empty id is a legitimate transient (the asset may be assigned later); a
10621
10716
  // non-empty one that resolves to nothing is a dead end - say so rather than staying
@@ -10651,7 +10746,6 @@
10651
10746
  }));
10652
10747
  this._onReady();
10653
10748
  });
10654
- app.assets.load(asset);
10655
10749
  }
10656
10750
  }
10657
10751
  _unloadModel() {
@@ -11616,7 +11710,7 @@
11616
11710
  return;
11617
11711
  }
11618
11712
  this._appElement = appElement;
11619
- const asset = AssetElement.get(this._asset);
11713
+ const asset = useAsset(this._asset);
11620
11714
  if (!asset) {
11621
11715
  return;
11622
11716
  }
@@ -11635,7 +11729,6 @@
11635
11729
  }
11636
11730
  this._generateSkybox(asset);
11637
11731
  });
11638
- app.assets.load(asset);
11639
11732
  }
11640
11733
  }
11641
11734
  _unloadSkybox() {