@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcanvas/web-components",
3
- "version": "0.11.0",
3
+ "version": "0.11.1",
4
4
  "author": "PlayCanvas <support@playcanvas.com>",
5
5
  "homepage": "https://playcanvas.com",
6
6
  "description": "Web Components for the PlayCanvas Engine",
@@ -51,7 +51,10 @@
51
51
  "cem": "cem analyze && node utils/cem/validate.mjs",
52
52
  "dev": "concurrently \"npm run watch\" \"npm run serve\"",
53
53
  "docs": "typedoc",
54
+ "fmt": "prettier --check eslint.config.mjs prettier.config.mjs examples/js examples/assets/scripts src test vitest.config.ts",
55
+ "fmt:fix": "prettier --write eslint.config.mjs prettier.config.mjs examples/js examples/assets/scripts src test vitest.config.ts",
54
56
  "lint": "eslint examples/js examples/assets/scripts src test vitest.config.ts",
57
+ "lint:fix": "eslint examples/js examples/assets/scripts src test vitest.config.ts --fix",
55
58
  "serve": "serve",
56
59
  "test": "vitest run",
57
60
  "test:watch": "vitest",
@@ -72,27 +75,25 @@
72
75
  "devDependencies": {
73
76
  "@custom-elements-manifest/analyzer": "0.11.0",
74
77
  "@mediapipe/tasks-vision": "1.0.1",
75
- "@playcanvas/eslint-config": "2.1.0",
78
+ "@playcanvas/eslint-config": "3.0.0-beta.8",
76
79
  "@rollup/plugin-commonjs": "29.0.3",
77
80
  "@rollup/plugin-node-resolve": "16.0.3",
78
81
  "@rollup/plugin-terser": "1.0.0",
79
82
  "@rollup/plugin-typescript": "12.3.0",
80
83
  "@tweenjs/tween.js": "25.0.0",
81
- "@typescript-eslint/eslint-plugin": "8.65.0",
82
- "@typescript-eslint/parser": "8.65.0",
83
84
  "@vitest/coverage-v8": "4.1.10",
84
85
  "concurrently": "10.0.4",
85
86
  "custom-element-jet-brains-integration": "1.7.0",
86
87
  "custom-element-vs-code-integration": "1.5.0",
87
88
  "earcut": "3.2.3",
88
89
  "eslint": "9.39.5",
89
- "eslint-import-resolver-typescript": "4.4.5",
90
90
  "globals": "17.9.0",
91
91
  "jsdom": "30.0.1",
92
- "mediabunny": "1.52.2",
92
+ "mediabunny": "1.52.3",
93
93
  "opentype.js": "2.0.0",
94
94
  "playcanvas": "2.21.3",
95
- "publint": "0.3.22",
95
+ "prettier": "3.9.6",
96
+ "publint": "0.3.23",
96
97
  "rollup": "4.62.4",
97
98
  "serve": "14.2.6",
98
99
  "tslib": "2.8.1",
package/src/app.ts CHANGED
@@ -1,12 +1,10 @@
1
+ import type { CameraComponent, GraphNode, GSplatComponent, Entity } from 'playcanvas';
1
2
  import {
2
3
  AppBase,
3
4
  AppOptions,
4
- CameraComponent,
5
5
  createGraphicsDevice,
6
6
  ElementInput,
7
- Entity,
8
7
  FILLMODE_FILL_WINDOW,
9
- GraphNode,
10
8
  Keyboard,
11
9
  Mouse,
12
10
  Picker,
@@ -63,18 +61,20 @@ import {
63
61
  SoundManager,
64
62
  Lightmapper,
65
63
  XrManager,
66
- MeshInstance,
67
- GSplatComponent
64
+ MeshInstance
68
65
  } from 'playcanvas';
69
66
 
70
- import { AssetElement } from './asset';
67
+ import type { AssetElement } from './asset';
71
68
  import { AsyncElement } from './async-element';
72
- import { EntityElement } from './entity';
69
+ import type { EntityElement } from './entity';
73
70
  import { LoadingBar } from './loading-bar';
74
- import { MaterialElement } from './material';
75
- import { ModuleElement } from './module';
71
+ import type { MaterialElement } from './material';
72
+ import type { ModuleElement } from './module';
76
73
  import { parseBool, parseEnum, parseNumber } from './parse';
77
74
 
75
+ /** The pointer event types the application synthesizes on `<pc-entity>` elements via picking. */
76
+ const pointerEventTypes = ['pointermove', 'pointerdown', 'pointerup', 'pointerenter', 'pointerleave'] as const;
77
+
78
78
  /**
79
79
  * The AppElement interface provides properties and methods for manipulating
80
80
  * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-app/ | `<pc-app>`} elements.
@@ -115,7 +115,21 @@ class AppElement extends AsyncElement {
115
115
 
116
116
  private _bar: LoadingBar | null = null;
117
117
 
118
- private _hierarchyReady = false;
118
+ /**
119
+ * Whether the application has created its initial entity hierarchy. Read by EntityElement to
120
+ * decide whether a newly connected element must create its entity itself or leave it to the
121
+ * boot sweep.
122
+ * @internal
123
+ */
124
+ _hierarchyReady = false;
125
+
126
+ /**
127
+ * Incremented on every connect and disconnect. Boot captures the value on entry and abandons
128
+ * itself wherever it resumes from an await if the value has moved on — so a boot whose
129
+ * element was removed cannot complete against a torn-down element, and a boot whose element
130
+ * was removed and re-inserted (which starts a boot of its own) cannot race the newer one.
131
+ */
132
+ private _bootGeneration = 0;
119
133
 
120
134
  /**
121
135
  * The elements backing this application's entities, keyed by the entity itself. Registered
@@ -126,7 +140,7 @@ class AppElement extends AsyncElement {
126
140
 
127
141
  private _picker: Picker | null = null;
128
142
 
129
- private _hasPointerListeners: { [key: string]: boolean } = {
143
+ private _hasPointerListeners: Record<string, boolean> = {
130
144
  pointerenter: false,
131
145
  pointerleave: false,
132
146
  pointerdown: false,
@@ -139,7 +153,7 @@ class AppElement extends AsyncElement {
139
153
  // Identifies the newest in-flight hover pick, so out-of-order results can be discarded
140
154
  private _pickToken = 0;
141
155
 
142
- private _pointerHandlers: { [key: string]: EventListener | null } = {
156
+ private _pointerHandlers: Record<string, EventListener | null> = {
143
157
  pointermove: null,
144
158
  pointerdown: null,
145
159
  pointerup: null
@@ -180,9 +194,19 @@ class AppElement extends AsyncElement {
180
194
 
181
195
  // Bind methods to maintain 'this' context
182
196
  this._onWindowResize = this._onWindowResize.bind(this);
197
+
198
+ // Track pointer listeners being added to and removed from descendant entities.
199
+ // Registered once here rather than on every boot - the handlers no-op while there is no
200
+ // canvas, and a re-booted element must not stack a second set.
201
+ pointerEventTypes.forEach((type) => {
202
+ this.addEventListener(`${type}:connect`, () => this._onPointerListenerAdded(type));
203
+ this.addEventListener(`${type}:disconnect`, () => this._onPointerListenerRemoved(type));
204
+ });
183
205
  }
184
206
 
185
207
  async connectedCallback() {
208
+ const generation = ++this._bootGeneration;
209
+
186
210
  // Created before the first await, so the bar is visible while modules and the graphics
187
211
  // device are created, and exists before any disconnect could need to clean it up
188
212
  if (this._loadingBar && !this._bar) {
@@ -193,14 +217,20 @@ class AppElement extends AsyncElement {
193
217
  const moduleElements = this.querySelectorAll<ModuleElement>(':scope > pc-module');
194
218
 
195
219
  // Wait for all modules to load
196
- await Promise.all(Array.from(moduleElements).map(module => module.getLoadPromise()));
220
+ await Promise.all(Array.from(moduleElements).map((module) => module._getLoadPromise()));
221
+
222
+ // The element may have been removed while the modules loaded. Nothing beyond the loading
223
+ // bar exists yet, and disconnectedCallback has already destroyed that.
224
+ if (generation !== this._bootGeneration) {
225
+ return;
226
+ }
197
227
 
198
228
  // Create and append the canvas to the element
199
229
  this._canvas = document.createElement('canvas');
200
230
  this.appendChild(this._canvas);
201
231
 
202
232
  // Configure device types based on backend selection
203
- const backendToDeviceTypes: { [key: string]: string[] } = {
233
+ const backendToDeviceTypes: Record<string, string[]> = {
204
234
  webgpu: ['webgpu', 'webgl2'], // fallback to webgl2 if webgpu not available
205
235
  webgl2: ['webgl2'],
206
236
  null: ['null']
@@ -218,6 +248,14 @@ class AppElement extends AsyncElement {
218
248
  stencil: this._stencilBuffer
219
249
  });
220
250
 
251
+ // The element may have been removed while the device was created. disconnectedCallback
252
+ // has already cleaned up the canvas; the device was created inside the await, so it is
253
+ // this boot's to release.
254
+ if (generation !== this._bootGeneration) {
255
+ device.destroy();
256
+ return;
257
+ }
258
+
221
259
  // Assigned rather than resolved to a number here: the engine caps against the live
222
260
  // window.devicePixelRatio on every resize, so an uncapped Infinity keeps following the
223
261
  // display when a window moves between monitors of differing density.
@@ -299,31 +337,47 @@ class AppElement extends AsyncElement {
299
337
 
300
338
  // Get all pc-asset elements that are direct children of the pc-app element
301
339
  const assetElements = this.querySelectorAll<AssetElement>(':scope > pc-asset');
302
- Array.from(assetElements).forEach((assetElement) => {
303
- assetElement.createAsset();
340
+ for (const assetElement of Array.from(assetElements)) {
341
+ assetElement._createAsset();
304
342
  const asset = assetElement.asset;
305
343
  if (asset) {
306
344
  app.assets.add(asset);
345
+
346
+ // Adding a fileless asset (one built purely from data, such as a sprite)
347
+ // completes it synchronously, dispatching the element's load event - whose
348
+ // listeners may have removed this element. Stop before the next addition
349
+ // reaches the destroyed registry, and before orphan entities are created.
350
+ if (generation !== this._bootGeneration) {
351
+ return;
352
+ }
307
353
  }
308
- });
354
+ }
309
355
 
310
356
  // Get all pc-material elements that are direct children of the pc-app element
311
357
  const materialElements = this.querySelectorAll<MaterialElement>(':scope > pc-material');
312
358
  Array.from(materialElements).forEach((materialElement) => {
313
- materialElement.createMaterial();
359
+ materialElement._createMaterial();
314
360
  });
315
361
 
316
362
  // Create all entities
317
363
  const entityElements = this.querySelectorAll<EntityElement>('pc-entity');
318
364
  Array.from(entityElements).forEach((entityElement) => {
319
- entityElement.createEntity(app);
365
+ entityElement._createEntity(app);
320
366
  });
321
367
 
322
368
  // Build hierarchy
323
369
  entityElements.forEach((entityElement) => {
324
- entityElement.buildHierarchy(app);
370
+ entityElement._buildHierarchy(app);
325
371
  });
326
372
 
373
+ // Building the hierarchy dispatched each entity's ready event synchronously, and a
374
+ // listener may have removed the element. The sweep itself degrades safely - destroying
375
+ // the application nulls every element's entity, so the remaining builds no-op - but the
376
+ // teardown's reset must not be overwritten here.
377
+ if (generation !== this._bootGeneration) {
378
+ return;
379
+ }
380
+
327
381
  this._hierarchyReady = true;
328
382
 
329
383
  // Forward the engine's preload lifecycle as DOM ProgressEvents on this element. The
@@ -343,8 +397,21 @@ class AppElement extends AsyncElement {
343
397
  this._bar?.progress(0, total);
344
398
  this.dispatchEvent(new ProgressEvent('progress', { lengthComputable: true, loaded: 0, total }));
345
399
 
400
+ // The progress dispatch above ran listeners synchronously, and one may have removed the
401
+ // element. The application is already destroyed - it must not be asked to preload.
402
+ if (generation !== this._bootGeneration) {
403
+ return;
404
+ }
405
+
346
406
  // Load assets before starting the application
347
407
  app.preload(() => {
408
+ // The element may have been removed while assets loaded. The application is already
409
+ // destroyed, so it must not be started — and readiness must not be signaled for a
410
+ // boot that no longer owns the element.
411
+ if (generation !== this._bootGeneration) {
412
+ return;
413
+ }
414
+
348
415
  // Scope the counter to this preload pass, so a later app.preload() call by user code
349
416
  // cannot push `loaded` past `total`
350
417
  app.off('preload:progress', onPreloadProgress);
@@ -365,6 +432,10 @@ class AppElement extends AsyncElement {
365
432
  }
366
433
 
367
434
  disconnectedCallback() {
435
+ // Invalidate any boot still in flight, so it abandons itself when it next resumes
436
+ // instead of completing against a torn-down element.
437
+ this._bootGeneration++;
438
+
368
439
  this._optionsLocked = false;
369
440
  this._pickerDestroy();
370
441
 
@@ -379,6 +450,12 @@ class AppElement extends AsyncElement {
379
450
  this._bar?.destroy();
380
451
  this._bar = null;
381
452
 
453
+ // Return the element to its pre-boot state, so re-inserting it boots afresh: descendants
454
+ // must neither see a hierarchy that no longer exists nor resume against a readiness that
455
+ // no longer holds.
456
+ this._hierarchyReady = false;
457
+ this._resetReady();
458
+
382
459
  // Remove event listeners
383
460
  window.removeEventListener('resize', this._onWindowResize);
384
461
 
@@ -389,13 +466,13 @@ class AppElement extends AsyncElement {
389
466
  }
390
467
  }
391
468
 
392
- _onWindowResize() {
469
+ private _onWindowResize() {
393
470
  if (this.app) {
394
471
  this.app.resizeCanvas();
395
472
  }
396
473
  }
397
474
 
398
- _pickerCreate() {
475
+ private _pickerCreate() {
399
476
  const { width, height } = this.app!.graphicsDevice;
400
477
  this._picker = new Picker(this.app!, width, height);
401
478
 
@@ -412,22 +489,20 @@ class AppElement extends AsyncElement {
412
489
  this._pointerHandlers.pointerdown = listener(this._onPointerDown);
413
490
  this._pointerHandlers.pointerup = listener(this._onPointerUp);
414
491
 
415
- // Listen for pointer listeners being added/removed
416
- ['pointermove', 'pointerdown', 'pointerup', 'pointerenter', 'pointerleave'].forEach((type) => {
417
- this.addEventListener(`${type}:connect`, () => this._onPointerListenerAdded(type));
418
- this.addEventListener(`${type}:disconnect`, () => this._onPointerListenerRemoved(type));
419
-
420
- // Attach canvas handlers for listeners registered before this point (e.g. handlers
421
- // created from onpointer* attributes when their elements were first upgraded)
422
- const anyListeners = Array.from(this.querySelectorAll<EntityElement>('pc-entity'))
423
- .some(entity => entity.hasListeners(type));
492
+ // Attach canvas handlers for listeners registered before this boot (e.g. handlers
493
+ // created from onpointer* attributes when their elements were first upgraded, or
494
+ // listeners carried over from before a re-boot)
495
+ pointerEventTypes.forEach((type) => {
496
+ const anyListeners = Array.from(this.querySelectorAll<EntityElement>('pc-entity')).some((entity) =>
497
+ entity._hasListeners(type)
498
+ );
424
499
  if (anyListeners) {
425
500
  this._onPointerListenerAdded(type);
426
501
  }
427
502
  });
428
503
  }
429
504
 
430
- _pickerDestroy() {
505
+ private _pickerDestroy() {
431
506
  if (this._canvas) {
432
507
  Object.entries(this._pointerHandlers).forEach(([type, handler]) => {
433
508
  if (handler) {
@@ -437,6 +512,7 @@ class AppElement extends AsyncElement {
437
512
  }
438
513
 
439
514
  this._picker = null;
515
+ this._hoveredEntity = null;
440
516
  this._pointerHandlers = {
441
517
  pointermove: null,
442
518
  pointerdown: null,
@@ -457,7 +533,7 @@ class AppElement extends AsyncElement {
457
533
  *
458
534
  * @param entity - The entity.
459
535
  * @param element - The element that created it.
460
- * @ignore
536
+ * @internal
461
537
  */
462
538
  _registerEntityElement(entity: Entity, element: EntityElement) {
463
539
  this._entityElements.set(entity, element);
@@ -467,7 +543,7 @@ class AppElement extends AsyncElement {
467
543
  * Removes the registration for a destroyed entity. Called by EntityElement.
468
544
  *
469
545
  * @param entity - The entity.
470
- * @ignore
546
+ * @internal
471
547
  */
472
548
  _unregisterEntityElement(entity: Entity) {
473
549
  this._entityElements.delete(entity);
@@ -516,7 +592,7 @@ class AppElement extends AsyncElement {
516
592
  private _elementWithListener(node: GraphNode | null, type: string): EntityElement | null {
517
593
  while (node !== null) {
518
594
  const element = this._entityElements.get(node);
519
- if (element?.hasListeners(type)) {
595
+ if (element?._hasListeners(type)) {
520
596
  return element;
521
597
  }
522
598
  node = node.parent;
@@ -525,7 +601,7 @@ class AppElement extends AsyncElement {
525
601
  }
526
602
 
527
603
  // New helper to convert CSS coordinates to canvas (picker) coordinates
528
- private _getPickerCoordinates(event: PointerEvent): { x: number, y: number } {
604
+ private _getPickerCoordinates(event: PointerEvent): { x: number; y: number } {
529
605
  // Get the canvas' bounding rectangle in CSS pixels.
530
606
  const canvasRect = this._canvas!.getBoundingClientRect();
531
607
  // Compute scale factors based on canvas actual resolution vs. its CSS display size.
@@ -562,7 +638,7 @@ class AppElement extends AsyncElement {
562
638
  return item instanceof MeshInstance ? item.node : (item as GSplatComponent).entity;
563
639
  }
564
640
 
565
- async _onPointerMove(event: PointerEvent) {
641
+ private async _onPointerMove(event: PointerEvent) {
566
642
  if (!this._picker || !this.app) return;
567
643
 
568
644
  // Moves arrive faster than a pick resolves, so results can land out of order. Only the
@@ -578,10 +654,10 @@ class AppElement extends AsyncElement {
578
654
 
579
655
  // Handle enter/leave events
580
656
  if (this._hoveredEntity !== newHoverEntity) {
581
- if (this._hoveredEntity && this._hoveredEntity.hasListeners('pointerleave')) {
657
+ if (this._hoveredEntity && this._hoveredEntity._hasListeners('pointerleave')) {
582
658
  this._hoveredEntity.dispatchEvent(new PointerEvent('pointerleave', event));
583
659
  }
584
- if (newHoverEntity && newHoverEntity.hasListeners('pointerenter')) {
660
+ if (newHoverEntity && newHoverEntity._hasListeners('pointerenter')) {
585
661
  newHoverEntity.dispatchEvent(new PointerEvent('pointerenter', event));
586
662
  }
587
663
  }
@@ -590,12 +666,12 @@ class AppElement extends AsyncElement {
590
666
  this._hoveredEntity = newHoverEntity;
591
667
 
592
668
  // Handle pointermove event
593
- if (newHoverEntity && newHoverEntity.hasListeners('pointermove')) {
669
+ if (newHoverEntity && newHoverEntity._hasListeners('pointermove')) {
594
670
  newHoverEntity.dispatchEvent(new PointerEvent('pointermove', event));
595
671
  }
596
672
  }
597
673
 
598
- async _onPointerDown(event: PointerEvent) {
674
+ private async _onPointerDown(event: PointerEvent) {
599
675
  if (!this._picker || !this.app) return;
600
676
 
601
677
  const node = await this._pickNode(event);
@@ -607,7 +683,7 @@ class AppElement extends AsyncElement {
607
683
  }
608
684
  }
609
685
 
610
- async _onPointerUp(event: PointerEvent) {
686
+ private async _onPointerUp(event: PointerEvent) {
611
687
  if (!this._picker || !this.app) return;
612
688
 
613
689
  const node = await this._pickNode(event);
@@ -619,34 +695,43 @@ class AppElement extends AsyncElement {
619
695
  }
620
696
  }
621
697
 
622
- _onPointerListenerAdded(type: string) {
698
+ private _onPointerListenerAdded(type: string) {
623
699
  if (!this._hasPointerListeners[type] && this._canvas) {
624
700
  this._hasPointerListeners[type] = true;
625
701
 
626
702
  // For enter/leave events, we need the move handler
627
- const handler = (type === 'pointerenter' || type === 'pointerleave') ?
628
- this._pointerHandlers.pointermove :
629
- this._pointerHandlers[type];
703
+ const handler =
704
+ type === 'pointerenter' || type === 'pointerleave'
705
+ ? this._pointerHandlers.pointermove
706
+ : this._pointerHandlers[type];
630
707
 
631
708
  if (handler) {
632
- this._canvas.addEventListener(type === 'pointerenter' || type === 'pointerleave' ? 'pointermove' : type, handler);
709
+ this._canvas.addEventListener(
710
+ type === 'pointerenter' || type === 'pointerleave' ? 'pointermove' : type,
711
+ handler
712
+ );
633
713
  }
634
714
  }
635
715
  }
636
716
 
637
- _onPointerListenerRemoved(type: string) {
638
- const hasListeners = Array.from(this.querySelectorAll<EntityElement>('pc-entity'))
639
- .some(entity => entity.hasListeners(type));
717
+ private _onPointerListenerRemoved(type: string) {
718
+ const hasListeners = Array.from(this.querySelectorAll<EntityElement>('pc-entity')).some((entity) =>
719
+ entity._hasListeners(type)
720
+ );
640
721
 
641
722
  if (!hasListeners && this._canvas) {
642
723
  this._hasPointerListeners[type] = false;
643
724
 
644
- const handler = (type === 'pointerenter' || type === 'pointerleave') ?
645
- this._pointerHandlers.pointermove :
646
- this._pointerHandlers[type];
725
+ const handler =
726
+ type === 'pointerenter' || type === 'pointerleave'
727
+ ? this._pointerHandlers.pointermove
728
+ : this._pointerHandlers[type];
647
729
 
648
730
  if (handler) {
649
- this._canvas.removeEventListener(type === 'pointerenter' || type === 'pointerleave' ? 'pointermove' : type, handler);
731
+ this._canvas.removeEventListener(
732
+ type === 'pointerenter' || type === 'pointerleave' ? 'pointermove' : type,
733
+ handler
734
+ );
650
735
  }
651
736
  }
652
737
  }
@@ -660,7 +745,9 @@ class AppElement extends AsyncElement {
660
745
  */
661
746
  private _warnIfBooted(name: string) {
662
747
  if (this._optionsLocked) {
663
- console.warn(`Attribute '${name}' on <pc-app> is only read when the application boots, so this change has no effect. Set it before the element is connected, or remove and re-insert the element to reboot with the new value.`);
748
+ console.warn(
749
+ `Attribute '${name}' on <pc-app> is only read when the application boots, so this change has no effect. Set it before the element is connected, or remove and re-insert the element to reboot with the new value.`
750
+ );
664
751
  }
665
752
  }
666
753
 
@@ -735,15 +822,6 @@ class AppElement extends AsyncElement {
735
822
  return this._depthBuffer;
736
823
  }
737
824
 
738
- /**
739
- * Gets the hierarchy ready flag.
740
- * @returns The hierarchy ready flag.
741
- * @ignore
742
- */
743
- get hierarchyReady() {
744
- return this._hierarchyReady;
745
- }
746
-
747
825
  /**
748
826
  * Sets whether the application shows its built-in loading bar while it boots and preloads its
749
827
  * assets. Enabled by default; setting `false` removes the bar immediately, while setting
@@ -844,10 +922,4 @@ class AppElement extends AsyncElement {
844
922
 
845
923
  customElements.define('pc-app', AppElement);
846
924
 
847
- declare global {
848
- interface HTMLElementTagNameMap {
849
- 'pc-app': AppElement;
850
- }
851
- }
852
-
853
925
  export { AppElement };
package/src/asset.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { Asset, SPRITE_RENDERMODE_SIMPLE, SPRITE_RENDERMODE_SLICED, SPRITE_RENDERMODE_TILED } from 'playcanvas';
2
2
 
3
+ import { MeshoptDecoder } from '../lib/meshopt_decoder.module.js';
4
+
3
5
  import { AsyncElement } from './async-element';
4
6
  import { parseBool, parseEnum, parseNumber } from './parse';
5
- import { MeshoptDecoder } from '../lib/meshopt_decoder.module.js';
6
7
 
7
8
  const renderModes = new Map<'simple' | 'sliced' | 'tiled', number>([
8
9
  ['simple', SPRITE_RENDERMODE_SIMPLE],
@@ -33,12 +34,11 @@ const extToType = new Map([
33
34
  ['webp', 'texture']
34
35
  ]);
35
36
 
36
-
37
37
  // provide buffer view callback so we can handle models compressed with MeshOptimizer
38
38
  // https://github.com/zeux/meshoptimizer
39
39
  const processBufferView = (
40
40
  gltfBuffer: any,
41
- buffers: Array<any>,
41
+ buffers: any[],
42
42
  continuation: (err: string | null, result: any) => void
43
43
  ) => {
44
44
  if (gltfBuffer.extensions && gltfBuffer.extensions.EXT_meshopt_compression) {
@@ -56,14 +56,7 @@ const processBufferView = (
56
56
  const result = new Uint8Array(count * stride);
57
57
  const source = new Uint8Array(buffer.buffer, buffer.byteOffset + byteOffset, byteLength);
58
58
 
59
- MeshoptDecoder.decodeGltfBuffer(
60
- result,
61
- count,
62
- stride,
63
- source,
64
- extensionDef.mode,
65
- extensionDef.filter
66
- );
59
+ MeshoptDecoder.decodeGltfBuffer(result, count, stride, source, extensionDef.mode, extensionDef.filter);
67
60
 
68
61
  continuation(null, result);
69
62
  });
@@ -108,7 +101,7 @@ const processBufferView = (
108
101
  * not that it succeeded.
109
102
  */
110
103
  class AssetElement extends AsyncElement {
111
- private _lazy: boolean = false;
104
+ private _lazy = false;
112
105
 
113
106
  /**
114
107
  * The asset that is loaded. Available once the element is ready — await
@@ -122,7 +115,9 @@ class AssetElement extends AsyncElement {
122
115
 
123
116
  // Assets must be direct children of pc-app (matches the boot query ':scope > pc-asset')
124
117
  if (this.parentElement !== appElement) {
125
- console.warn(`pc-asset '${this.getAttribute('id') ?? this.getAttribute('src')}' must be a direct child of pc-app - asset not created`);
118
+ console.warn(
119
+ `pc-asset '${this.getAttribute('id') ?? this.getAttribute('src')}' must be a direct child of pc-app - asset not created`
120
+ );
126
121
  return;
127
122
  }
128
123
 
@@ -137,7 +132,7 @@ class AssetElement extends AsyncElement {
137
132
  const app = appElement.app;
138
133
  if (!app) return; // pc-app is re-connecting; its own boot will create this asset
139
134
 
140
- this.createAsset();
135
+ this._createAsset();
141
136
  if (this.asset) {
142
137
  app.assets.add(this.asset); // add() auto-loads when preload is true
143
138
  if (!this.lazy) {
@@ -146,14 +141,16 @@ class AssetElement extends AsyncElement {
146
141
  }
147
142
  }
148
143
 
149
- // Never ready if createAsset failed (unsupported asset type)
144
+ // Never ready if _createAsset failed (unsupported asset type)
150
145
  if (this.asset) {
151
146
  this._onReady();
152
147
  }
153
148
  }
154
149
 
155
150
  disconnectedCallback() {
156
- this.destroyAsset();
151
+ this._destroyAsset();
152
+ // Re-arm readiness so a re-inserted element announces the asset it creates then
153
+ this._resetReady();
157
154
  }
158
155
 
159
156
  private _onAssetLoad() {
@@ -161,12 +158,21 @@ class AssetElement extends AsyncElement {
161
158
  }
162
159
 
163
160
  private _onAssetError(err: string | Error) {
164
- this.dispatchEvent(new ErrorEvent('error', {
165
- message: err instanceof Error ? err.message : String(err)
166
- }));
161
+ this.dispatchEvent(
162
+ new ErrorEvent('error', {
163
+ message: err instanceof Error ? err.message : String(err)
164
+ })
165
+ );
167
166
  }
168
167
 
169
- createAsset() {
168
+ /**
169
+ * Creates the asset from the element's attributes. Called by the containing `<pc-app>`
170
+ * element during its boot sweep, and on connection for elements inserted while the
171
+ * application is already running.
172
+ *
173
+ * @internal
174
+ */
175
+ _createAsset() {
170
176
  const id = this.getAttribute('id') || '';
171
177
  const src = this.getAttribute('src') || '';
172
178
  let type = this.getAttribute('type');
@@ -268,8 +274,7 @@ class AssetElement extends AsyncElement {
268
274
  return data;
269
275
  }
270
276
 
271
-
272
- destroyAsset() {
277
+ private _destroyAsset() {
273
278
  if (this.asset) {
274
279
  // A caller that keeps the Asset alive must not dispatch on a removed element
275
280
  this.asset.off('load', this._onAssetLoad, this);
@@ -300,6 +305,13 @@ class AssetElement extends AsyncElement {
300
305
  return this._lazy;
301
306
  }
302
307
 
308
+ /**
309
+ * Returns the {@link Asset} created by the `<pc-asset>` element with the given `id`, or
310
+ * `undefined` if there is no such element or its asset has not been created yet.
311
+ *
312
+ * @param id - The `id` of the `<pc-asset>` element.
313
+ * @returns The asset, or `undefined`.
314
+ */
303
315
  static get(id: string) {
304
316
  const assetElement = document.querySelector<AssetElement>(`pc-asset[id="${id}"]`);
305
317
  return assetElement?.asset;
@@ -318,10 +330,4 @@ class AssetElement extends AsyncElement {
318
330
 
319
331
  customElements.define('pc-asset', AssetElement);
320
332
 
321
- declare global {
322
- interface HTMLElementTagNameMap {
323
- 'pc-asset': AssetElement;
324
- }
325
- }
326
-
327
333
  export { AssetElement };