@playcanvas/web-components 0.11.0 → 0.12.0

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 (82) hide show
  1. package/README.md +1 -1
  2. package/dist/app.d.ts +31 -49
  3. package/dist/asset.d.ts +152 -12
  4. package/dist/async-element.d.ts +26 -9
  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 +19 -7
  8. package/dist/components/component.d.ts +41 -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 +5824 -10581
  26. package/dist/entity-base.d.ts +67 -0
  27. package/dist/entity.d.ts +7 -48
  28. package/dist/index.d.ts +41 -1
  29. package/dist/material.d.ts +14 -13
  30. package/dist/model.d.ts +43 -5
  31. package/dist/module.d.ts +0 -6
  32. package/dist/node.d.ts +253 -0
  33. package/dist/parse.d.ts +2 -1
  34. package/dist/pwc.cjs +1830 -274
  35. package/dist/pwc.cjs.map +1 -1
  36. package/dist/pwc.js +1830 -274
  37. package/dist/pwc.js.map +1 -1
  38. package/dist/pwc.min.js +1 -1
  39. package/dist/pwc.min.js.map +1 -1
  40. package/dist/pwc.min.mjs +1 -1
  41. package/dist/pwc.min.mjs.map +1 -1
  42. package/dist/pwc.mjs +1830 -276
  43. package/dist/pwc.mjs.map +1 -1
  44. package/dist/scene.d.ts +4 -7
  45. package/dist/sky.d.ts +13 -5
  46. package/dist/vscode.html-custom-data.json +148 -26
  47. package/dist/web-types.json +894 -581
  48. package/package.json +9 -8
  49. package/src/app.ts +163 -88
  50. package/src/asset.ts +472 -36
  51. package/src/async-element.ts +39 -12
  52. package/src/components/button-component.ts +5 -9
  53. package/src/components/camera-component.ts +24 -10
  54. package/src/components/collision-component.ts +61 -15
  55. package/src/components/component.ts +151 -11
  56. package/src/components/element-component.ts +26 -30
  57. package/src/components/gsplat-component.ts +4 -9
  58. package/src/components/layoutchild-component.ts +4 -9
  59. package/src/components/layoutgroup-component.ts +14 -9
  60. package/src/components/light-component.ts +42 -12
  61. package/src/components/listener-component.ts +1 -7
  62. package/src/components/particlesystem-component.ts +7 -15
  63. package/src/components/render-component.ts +5 -10
  64. package/src/components/rigidbody-component.ts +23 -16
  65. package/src/components/screen-component.ts +5 -9
  66. package/src/components/script-component.ts +108 -46
  67. package/src/components/script.ts +38 -33
  68. package/src/components/scrollbar-component.ts +6 -16
  69. package/src/components/scrollview-component.ts +16 -11
  70. package/src/components/sound-component.ts +10 -15
  71. package/src/components/sound-slot.ts +30 -20
  72. package/src/entity-base.ts +136 -0
  73. package/src/entity.ts +47 -118
  74. package/src/index.ts +50 -1
  75. package/src/loading-bar.ts +8 -8
  76. package/src/material.ts +65 -39
  77. package/src/model.ts +140 -17
  78. package/src/module.ts +8 -7
  79. package/src/node.ts +715 -0
  80. package/src/parse.ts +62 -17
  81. package/src/scene.ts +12 -9
  82. package/src/sky.ts +50 -10
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcanvas/web-components",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
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
- "playcanvas": "2.21.3",
95
- "publint": "0.3.22",
94
+ "playcanvas": "2.22.0-beta.10",
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,21 @@ 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';
70
+ import type { EntityBaseElement } from './entity-base';
73
71
  import { LoadingBar } from './loading-bar';
74
- import { MaterialElement } from './material';
75
- import { ModuleElement } from './module';
72
+ import type { MaterialElement } from './material';
73
+ import type { ModuleElement } from './module';
76
74
  import { parseBool, parseEnum, parseNumber } from './parse';
77
75
 
76
+ /** The pointer event types the application synthesizes on `<pc-entity>` elements via picking. */
77
+ const pointerEventTypes = ['pointermove', 'pointerdown', 'pointerup', 'pointerenter', 'pointerleave'] as const;
78
+
78
79
  /**
79
80
  * The AppElement interface provides properties and methods for manipulating
80
81
  * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-app/ | `<pc-app>`} elements.
@@ -115,18 +116,33 @@ class AppElement extends AsyncElement {
115
116
 
116
117
  private _bar: LoadingBar | null = null;
117
118
 
118
- private _hierarchyReady = false;
119
+ /**
120
+ * Whether the application has created its initial entity hierarchy. Read by EntityElement to
121
+ * decide whether a newly connected element must create its entity itself or leave it to the
122
+ * boot sweep.
123
+ * @internal
124
+ */
125
+ _hierarchyReady = false;
126
+
127
+ /**
128
+ * Incremented on every connect and disconnect. Boot captures the value on entry and abandons
129
+ * itself wherever it resumes from an await if the value has moved on — so a boot whose
130
+ * element was removed cannot complete against a torn-down element, and a boot whose element
131
+ * was removed and re-inserted (which starts a boot of its own) cannot race the newer one.
132
+ */
133
+ private _bootGeneration = 0;
119
134
 
120
135
  /**
121
136
  * The elements backing this application's entities, keyed by the entity itself. Registered
122
- * by EntityElement at creation and removed when an entity is destroyed, this joins engine
123
- * scene nodes back to their owning elements by identity - never by name.
137
+ * by EntityElement at creation (and NodeElement at binding) and removed when an entity is
138
+ * destroyed or unbound, this joins engine scene nodes back to their owning elements by
139
+ * identity - never by name.
124
140
  */
125
- private _entityElements = new Map<GraphNode, EntityElement>();
141
+ private _entityElements = new Map<GraphNode, EntityBaseElement>();
126
142
 
127
143
  private _picker: Picker | null = null;
128
144
 
129
- private _hasPointerListeners: { [key: string]: boolean } = {
145
+ private _hasPointerListeners: Record<string, boolean> = {
130
146
  pointerenter: false,
131
147
  pointerleave: false,
132
148
  pointerdown: false,
@@ -134,12 +150,12 @@ class AppElement extends AsyncElement {
134
150
  pointermove: false
135
151
  };
136
152
 
137
- private _hoveredEntity: EntityElement | null = null;
153
+ private _hoveredEntity: EntityBaseElement | null = null;
138
154
 
139
155
  // Identifies the newest in-flight hover pick, so out-of-order results can be discarded
140
156
  private _pickToken = 0;
141
157
 
142
- private _pointerHandlers: { [key: string]: EventListener | null } = {
158
+ private _pointerHandlers: Record<string, EventListener | null> = {
143
159
  pointermove: null,
144
160
  pointerdown: null,
145
161
  pointerup: null
@@ -180,9 +196,19 @@ class AppElement extends AsyncElement {
180
196
 
181
197
  // Bind methods to maintain 'this' context
182
198
  this._onWindowResize = this._onWindowResize.bind(this);
199
+
200
+ // Track pointer listeners being added to and removed from descendant entities.
201
+ // Registered once here rather than on every boot - the handlers no-op while there is no
202
+ // canvas, and a re-booted element must not stack a second set.
203
+ pointerEventTypes.forEach((type) => {
204
+ this.addEventListener(`${type}:connect`, () => this._onPointerListenerAdded(type));
205
+ this.addEventListener(`${type}:disconnect`, () => this._onPointerListenerRemoved(type));
206
+ });
183
207
  }
184
208
 
185
209
  async connectedCallback() {
210
+ const generation = ++this._bootGeneration;
211
+
186
212
  // Created before the first await, so the bar is visible while modules and the graphics
187
213
  // device are created, and exists before any disconnect could need to clean it up
188
214
  if (this._loadingBar && !this._bar) {
@@ -193,14 +219,20 @@ class AppElement extends AsyncElement {
193
219
  const moduleElements = this.querySelectorAll<ModuleElement>(':scope > pc-module');
194
220
 
195
221
  // Wait for all modules to load
196
- await Promise.all(Array.from(moduleElements).map(module => module.getLoadPromise()));
222
+ await Promise.all(Array.from(moduleElements).map((module) => module._getLoadPromise()));
223
+
224
+ // The element may have been removed while the modules loaded. Nothing beyond the loading
225
+ // bar exists yet, and disconnectedCallback has already destroyed that.
226
+ if (generation !== this._bootGeneration) {
227
+ return;
228
+ }
197
229
 
198
230
  // Create and append the canvas to the element
199
231
  this._canvas = document.createElement('canvas');
200
232
  this.appendChild(this._canvas);
201
233
 
202
234
  // Configure device types based on backend selection
203
- const backendToDeviceTypes: { [key: string]: string[] } = {
235
+ const backendToDeviceTypes: Record<string, string[]> = {
204
236
  webgpu: ['webgpu', 'webgl2'], // fallback to webgl2 if webgpu not available
205
237
  webgl2: ['webgl2'],
206
238
  null: ['null']
@@ -218,6 +250,14 @@ class AppElement extends AsyncElement {
218
250
  stencil: this._stencilBuffer
219
251
  });
220
252
 
253
+ // The element may have been removed while the device was created. disconnectedCallback
254
+ // has already cleaned up the canvas; the device was created inside the await, so it is
255
+ // this boot's to release.
256
+ if (generation !== this._bootGeneration) {
257
+ device.destroy();
258
+ return;
259
+ }
260
+
221
261
  // Assigned rather than resolved to a number here: the engine caps against the live
222
262
  // window.devicePixelRatio on every resize, so an uncapped Infinity keeps following the
223
263
  // display when a window moves between monitors of differing density.
@@ -299,31 +339,47 @@ class AppElement extends AsyncElement {
299
339
 
300
340
  // Get all pc-asset elements that are direct children of the pc-app element
301
341
  const assetElements = this.querySelectorAll<AssetElement>(':scope > pc-asset');
302
- Array.from(assetElements).forEach((assetElement) => {
303
- assetElement.createAsset();
342
+ for (const assetElement of Array.from(assetElements)) {
343
+ assetElement._createAsset();
304
344
  const asset = assetElement.asset;
305
345
  if (asset) {
306
346
  app.assets.add(asset);
347
+
348
+ // Adding a fileless asset (one built purely from data, such as a sprite)
349
+ // completes it synchronously, dispatching the element's load event - whose
350
+ // listeners may have removed this element. Stop before the next addition
351
+ // reaches the destroyed registry, and before orphan entities are created.
352
+ if (generation !== this._bootGeneration) {
353
+ return;
354
+ }
307
355
  }
308
- });
356
+ }
309
357
 
310
358
  // Get all pc-material elements that are direct children of the pc-app element
311
359
  const materialElements = this.querySelectorAll<MaterialElement>(':scope > pc-material');
312
360
  Array.from(materialElements).forEach((materialElement) => {
313
- materialElement.createMaterial();
361
+ materialElement._createMaterial();
314
362
  });
315
363
 
316
364
  // Create all entities
317
365
  const entityElements = this.querySelectorAll<EntityElement>('pc-entity');
318
366
  Array.from(entityElements).forEach((entityElement) => {
319
- entityElement.createEntity(app);
367
+ entityElement._createEntity(app);
320
368
  });
321
369
 
322
370
  // Build hierarchy
323
371
  entityElements.forEach((entityElement) => {
324
- entityElement.buildHierarchy(app);
372
+ entityElement._buildHierarchy(app);
325
373
  });
326
374
 
375
+ // Building the hierarchy dispatched each entity's ready event synchronously, and a
376
+ // listener may have removed the element. The sweep itself degrades safely - destroying
377
+ // the application nulls every element's entity, so the remaining builds no-op - but the
378
+ // teardown's reset must not be overwritten here.
379
+ if (generation !== this._bootGeneration) {
380
+ return;
381
+ }
382
+
327
383
  this._hierarchyReady = true;
328
384
 
329
385
  // Forward the engine's preload lifecycle as DOM ProgressEvents on this element. The
@@ -343,8 +399,21 @@ class AppElement extends AsyncElement {
343
399
  this._bar?.progress(0, total);
344
400
  this.dispatchEvent(new ProgressEvent('progress', { lengthComputable: true, loaded: 0, total }));
345
401
 
402
+ // The progress dispatch above ran listeners synchronously, and one may have removed the
403
+ // element. The application is already destroyed - it must not be asked to preload.
404
+ if (generation !== this._bootGeneration) {
405
+ return;
406
+ }
407
+
346
408
  // Load assets before starting the application
347
409
  app.preload(() => {
410
+ // The element may have been removed while assets loaded. The application is already
411
+ // destroyed, so it must not be started — and readiness must not be signaled for a
412
+ // boot that no longer owns the element.
413
+ if (generation !== this._bootGeneration) {
414
+ return;
415
+ }
416
+
348
417
  // Scope the counter to this preload pass, so a later app.preload() call by user code
349
418
  // cannot push `loaded` past `total`
350
419
  app.off('preload:progress', onPreloadProgress);
@@ -365,6 +434,10 @@ class AppElement extends AsyncElement {
365
434
  }
366
435
 
367
436
  disconnectedCallback() {
437
+ // Invalidate any boot still in flight, so it abandons itself when it next resumes
438
+ // instead of completing against a torn-down element.
439
+ this._bootGeneration++;
440
+
368
441
  this._optionsLocked = false;
369
442
  this._pickerDestroy();
370
443
 
@@ -379,6 +452,12 @@ class AppElement extends AsyncElement {
379
452
  this._bar?.destroy();
380
453
  this._bar = null;
381
454
 
455
+ // Return the element to its pre-boot state, so re-inserting it boots afresh: descendants
456
+ // must neither see a hierarchy that no longer exists nor resume against a readiness that
457
+ // no longer holds.
458
+ this._hierarchyReady = false;
459
+ this._resetReady();
460
+
382
461
  // Remove event listeners
383
462
  window.removeEventListener('resize', this._onWindowResize);
384
463
 
@@ -389,13 +468,13 @@ class AppElement extends AsyncElement {
389
468
  }
390
469
  }
391
470
 
392
- _onWindowResize() {
471
+ private _onWindowResize() {
393
472
  if (this.app) {
394
473
  this.app.resizeCanvas();
395
474
  }
396
475
  }
397
476
 
398
- _pickerCreate() {
477
+ private _pickerCreate() {
399
478
  const { width, height } = this.app!.graphicsDevice;
400
479
  this._picker = new Picker(this.app!, width, height);
401
480
 
@@ -412,22 +491,20 @@ class AppElement extends AsyncElement {
412
491
  this._pointerHandlers.pointerdown = listener(this._onPointerDown);
413
492
  this._pointerHandlers.pointerup = listener(this._onPointerUp);
414
493
 
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));
494
+ // Attach canvas handlers for listeners registered before this boot (e.g. handlers
495
+ // created from onpointer* attributes when their elements were first upgraded, or
496
+ // listeners carried over from before a re-boot)
497
+ pointerEventTypes.forEach((type) => {
498
+ const anyListeners = Array.from(
499
+ this.querySelectorAll<EntityBaseElement>('pc-entity, pc-node')
500
+ ).some((entity) => entity._hasListeners(type));
424
501
  if (anyListeners) {
425
502
  this._onPointerListenerAdded(type);
426
503
  }
427
504
  });
428
505
  }
429
506
 
430
- _pickerDestroy() {
507
+ private _pickerDestroy() {
431
508
  if (this._canvas) {
432
509
  Object.entries(this._pointerHandlers).forEach(([type, handler]) => {
433
510
  if (handler) {
@@ -437,6 +514,7 @@ class AppElement extends AsyncElement {
437
514
  }
438
515
 
439
516
  this._picker = null;
517
+ this._hoveredEntity = null;
440
518
  this._pointerHandlers = {
441
519
  pointermove: null,
442
520
  pointerdown: null,
@@ -452,14 +530,14 @@ class AppElement extends AsyncElement {
452
530
  }
453
531
 
454
532
  /**
455
- * Registers the element that created an entity. Called by EntityElement when it creates its
456
- * entity.
533
+ * Registers the element that fronts an entity. Called by EntityElement when it creates its
534
+ * entity, and by NodeElement when it binds one.
457
535
  *
458
536
  * @param entity - The entity.
459
- * @param element - The element that created it.
460
- * @ignore
537
+ * @param element - The element that fronts it.
538
+ * @internal
461
539
  */
462
- _registerEntityElement(entity: Entity, element: EntityElement) {
540
+ _registerEntityElement(entity: Entity, element: EntityBaseElement) {
463
541
  this._entityElements.set(entity, element);
464
542
  }
465
543
 
@@ -467,34 +545,35 @@ class AppElement extends AsyncElement {
467
545
  * Removes the registration for a destroyed entity. Called by EntityElement.
468
546
  *
469
547
  * @param entity - The entity.
470
- * @ignore
548
+ * @internal
471
549
  */
472
550
  _unregisterEntityElement(entity: Entity) {
473
551
  this._entityElements.delete(entity);
474
552
  }
475
553
 
476
554
  /**
477
- * Returns the `<pc-entity>` element whose backing entity is `entity`, or `null` if the
478
- * entity was not created by an element of this application - for example, a node inside a
479
- * model's instantiated hierarchy, or an entity created through the engine API.
555
+ * Returns the `<pc-entity>` or `<pc-node>` element whose backing entity is `entity`, or
556
+ * `null` if the entity is not fronted by an element of this application - for example, an
557
+ * unbound node inside a model's instantiated hierarchy, or an entity created through the
558
+ * engine API.
480
559
  *
481
560
  * @param entity - The entity to look up.
482
- * @returns The element backing the entity, or `null`.
561
+ * @returns The element fronting the entity, or `null`.
483
562
  */
484
- elementFromEntity(entity: Entity): EntityElement | null {
563
+ elementFromEntity(entity: Entity): EntityBaseElement | null {
485
564
  return this._entityElements.get(entity) ?? null;
486
565
  }
487
566
 
488
567
  /**
489
568
  * Resolves the element that owns a picked node: the nearest node up the parent chain -
490
- * starting with the node itself - that was created by a `<pc-entity>` of this application.
491
- * A hit inside a model's instantiated hierarchy therefore resolves to the element hosting
492
- * the model.
569
+ * starting with the node itself - that is fronted by a `<pc-entity>` or `<pc-node>` of this
570
+ * application. A hit inside a model's instantiated hierarchy therefore resolves to the
571
+ * nearest bound `<pc-node>`, or failing that the element hosting the model.
493
572
  *
494
573
  * @param node - The picked node, or `null`.
495
574
  * @returns The owning element, or `null`.
496
575
  */
497
- private _elementFromNode(node: GraphNode | null): EntityElement | null {
576
+ private _elementFromNode(node: GraphNode | null): EntityBaseElement | null {
498
577
  while (node !== null) {
499
578
  const element = this._entityElements.get(node);
500
579
  if (element) {
@@ -513,10 +592,10 @@ class AppElement extends AsyncElement {
513
592
  * @param type - The pointer event type a listener is required for.
514
593
  * @returns The nearest listening element, or `null`.
515
594
  */
516
- private _elementWithListener(node: GraphNode | null, type: string): EntityElement | null {
595
+ private _elementWithListener(node: GraphNode | null, type: string): EntityBaseElement | null {
517
596
  while (node !== null) {
518
597
  const element = this._entityElements.get(node);
519
- if (element?.hasListeners(type)) {
598
+ if (element?._hasListeners(type)) {
520
599
  return element;
521
600
  }
522
601
  node = node.parent;
@@ -525,7 +604,7 @@ class AppElement extends AsyncElement {
525
604
  }
526
605
 
527
606
  // New helper to convert CSS coordinates to canvas (picker) coordinates
528
- private _getPickerCoordinates(event: PointerEvent): { x: number, y: number } {
607
+ private _getPickerCoordinates(event: PointerEvent): { x: number; y: number } {
529
608
  // Get the canvas' bounding rectangle in CSS pixels.
530
609
  const canvasRect = this._canvas!.getBoundingClientRect();
531
610
  // Compute scale factors based on canvas actual resolution vs. its CSS display size.
@@ -562,7 +641,7 @@ class AppElement extends AsyncElement {
562
641
  return item instanceof MeshInstance ? item.node : (item as GSplatComponent).entity;
563
642
  }
564
643
 
565
- async _onPointerMove(event: PointerEvent) {
644
+ private async _onPointerMove(event: PointerEvent) {
566
645
  if (!this._picker || !this.app) return;
567
646
 
568
647
  // Moves arrive faster than a pick resolves, so results can land out of order. Only the
@@ -578,10 +657,10 @@ class AppElement extends AsyncElement {
578
657
 
579
658
  // Handle enter/leave events
580
659
  if (this._hoveredEntity !== newHoverEntity) {
581
- if (this._hoveredEntity && this._hoveredEntity.hasListeners('pointerleave')) {
660
+ if (this._hoveredEntity && this._hoveredEntity._hasListeners('pointerleave')) {
582
661
  this._hoveredEntity.dispatchEvent(new PointerEvent('pointerleave', event));
583
662
  }
584
- if (newHoverEntity && newHoverEntity.hasListeners('pointerenter')) {
663
+ if (newHoverEntity && newHoverEntity._hasListeners('pointerenter')) {
585
664
  newHoverEntity.dispatchEvent(new PointerEvent('pointerenter', event));
586
665
  }
587
666
  }
@@ -590,12 +669,12 @@ class AppElement extends AsyncElement {
590
669
  this._hoveredEntity = newHoverEntity;
591
670
 
592
671
  // Handle pointermove event
593
- if (newHoverEntity && newHoverEntity.hasListeners('pointermove')) {
672
+ if (newHoverEntity && newHoverEntity._hasListeners('pointermove')) {
594
673
  newHoverEntity.dispatchEvent(new PointerEvent('pointermove', event));
595
674
  }
596
675
  }
597
676
 
598
- async _onPointerDown(event: PointerEvent) {
677
+ private async _onPointerDown(event: PointerEvent) {
599
678
  if (!this._picker || !this.app) return;
600
679
 
601
680
  const node = await this._pickNode(event);
@@ -607,7 +686,7 @@ class AppElement extends AsyncElement {
607
686
  }
608
687
  }
609
688
 
610
- async _onPointerUp(event: PointerEvent) {
689
+ private async _onPointerUp(event: PointerEvent) {
611
690
  if (!this._picker || !this.app) return;
612
691
 
613
692
  const node = await this._pickNode(event);
@@ -619,34 +698,43 @@ class AppElement extends AsyncElement {
619
698
  }
620
699
  }
621
700
 
622
- _onPointerListenerAdded(type: string) {
701
+ private _onPointerListenerAdded(type: string) {
623
702
  if (!this._hasPointerListeners[type] && this._canvas) {
624
703
  this._hasPointerListeners[type] = true;
625
704
 
626
705
  // For enter/leave events, we need the move handler
627
- const handler = (type === 'pointerenter' || type === 'pointerleave') ?
628
- this._pointerHandlers.pointermove :
629
- this._pointerHandlers[type];
706
+ const handler =
707
+ type === 'pointerenter' || type === 'pointerleave'
708
+ ? this._pointerHandlers.pointermove
709
+ : this._pointerHandlers[type];
630
710
 
631
711
  if (handler) {
632
- this._canvas.addEventListener(type === 'pointerenter' || type === 'pointerleave' ? 'pointermove' : type, handler);
712
+ this._canvas.addEventListener(
713
+ type === 'pointerenter' || type === 'pointerleave' ? 'pointermove' : type,
714
+ handler
715
+ );
633
716
  }
634
717
  }
635
718
  }
636
719
 
637
- _onPointerListenerRemoved(type: string) {
638
- const hasListeners = Array.from(this.querySelectorAll<EntityElement>('pc-entity'))
639
- .some(entity => entity.hasListeners(type));
720
+ private _onPointerListenerRemoved(type: string) {
721
+ const hasListeners = Array.from(
722
+ this.querySelectorAll<EntityBaseElement>('pc-entity, pc-node')
723
+ ).some((entity) => entity._hasListeners(type));
640
724
 
641
725
  if (!hasListeners && this._canvas) {
642
726
  this._hasPointerListeners[type] = false;
643
727
 
644
- const handler = (type === 'pointerenter' || type === 'pointerleave') ?
645
- this._pointerHandlers.pointermove :
646
- this._pointerHandlers[type];
728
+ const handler =
729
+ type === 'pointerenter' || type === 'pointerleave'
730
+ ? this._pointerHandlers.pointermove
731
+ : this._pointerHandlers[type];
647
732
 
648
733
  if (handler) {
649
- this._canvas.removeEventListener(type === 'pointerenter' || type === 'pointerleave' ? 'pointermove' : type, handler);
734
+ this._canvas.removeEventListener(
735
+ type === 'pointerenter' || type === 'pointerleave' ? 'pointermove' : type,
736
+ handler
737
+ );
650
738
  }
651
739
  }
652
740
  }
@@ -660,7 +748,9 @@ class AppElement extends AsyncElement {
660
748
  */
661
749
  private _warnIfBooted(name: string) {
662
750
  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.`);
751
+ console.warn(
752
+ `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.`
753
+ );
664
754
  }
665
755
  }
666
756
 
@@ -735,15 +825,6 @@ class AppElement extends AsyncElement {
735
825
  return this._depthBuffer;
736
826
  }
737
827
 
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
828
  /**
748
829
  * Sets whether the application shows its built-in loading bar while it boots and preloads its
749
830
  * assets. Enabled by default; setting `false` removes the bar immediately, while setting
@@ -844,10 +925,4 @@ class AppElement extends AsyncElement {
844
925
 
845
926
  customElements.define('pc-app', AppElement);
846
927
 
847
- declare global {
848
- interface HTMLElementTagNameMap {
849
- 'pc-app': AppElement;
850
- }
851
- }
852
-
853
928
  export { AppElement };