@playcanvas/web-components 0.16.0 → 0.17.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 (105) hide show
  1. package/dist/app.d.cts +14 -13
  2. package/dist/app.d.ts +14 -13
  3. package/dist/async-element.d.cts +13 -13
  4. package/dist/async-element.d.ts +13 -13
  5. package/dist/components/anim-component.d.cts +33 -5
  6. package/dist/components/anim-component.d.ts +33 -5
  7. package/dist/components/{listener-component.d.cts → audio-listener-component.d.cts} +7 -5
  8. package/dist/components/{listener-component.d.ts → audio-listener-component.d.ts} +7 -5
  9. package/dist/components/button-component.d.cts +5 -1
  10. package/dist/components/button-component.d.ts +5 -1
  11. package/dist/components/camera-component.d.cts +2 -0
  12. package/dist/components/camera-component.d.ts +2 -0
  13. package/dist/components/collision-component.d.cts +5 -1
  14. package/dist/components/collision-component.d.ts +5 -1
  15. package/dist/components/component.d.cts +3 -2
  16. package/dist/components/component.d.ts +3 -2
  17. package/dist/components/element-component.d.cts +7 -0
  18. package/dist/components/element-component.d.ts +7 -0
  19. package/dist/components/gsplat-component.d.cts +2 -0
  20. package/dist/components/gsplat-component.d.ts +2 -0
  21. package/dist/components/joint-component.d.cts +2 -0
  22. package/dist/components/joint-component.d.ts +2 -0
  23. package/dist/components/{layoutchild-component.d.cts → layout-child-component.d.cts} +3 -1
  24. package/dist/components/{layoutchild-component.d.ts → layout-child-component.d.ts} +3 -1
  25. package/dist/components/{layoutgroup-component.d.cts → layout-group-component.d.cts} +3 -1
  26. package/dist/components/{layoutgroup-component.d.ts → layout-group-component.d.ts} +3 -1
  27. package/dist/components/light-component.d.cts +2 -0
  28. package/dist/components/light-component.d.ts +2 -0
  29. package/dist/components/{particlesystem-component.d.cts → particle-system-component.d.cts} +3 -1
  30. package/dist/components/{particlesystem-component.d.ts → particle-system-component.d.ts} +3 -1
  31. package/dist/components/render-component.d.cts +2 -0
  32. package/dist/components/render-component.d.ts +2 -0
  33. package/dist/components/{rigidbody-component.d.cts → rigid-body-component.d.cts} +3 -1
  34. package/dist/components/{rigidbody-component.d.ts → rigid-body-component.d.ts} +3 -1
  35. package/dist/components/screen-component.d.cts +2 -0
  36. package/dist/components/screen-component.d.ts +2 -0
  37. package/dist/components/script-component.d.cts +23 -12
  38. package/dist/components/script-component.d.ts +23 -12
  39. package/dist/components/{script.d.cts → script-instance.d.cts} +7 -7
  40. package/dist/components/{script.d.ts → script-instance.d.ts} +7 -7
  41. package/dist/components/{scrollview-component.d.cts → scroll-view-component.d.cts} +3 -1
  42. package/dist/components/{scrollview-component.d.ts → scroll-view-component.d.ts} +3 -1
  43. package/dist/components/scrollbar-component.d.cts +2 -0
  44. package/dist/components/scrollbar-component.d.ts +2 -0
  45. package/dist/components/sound-component.d.cts +3 -1
  46. package/dist/components/sound-component.d.ts +3 -1
  47. package/dist/components/sound-slot.d.cts +2 -2
  48. package/dist/components/sound-slot.d.ts +2 -2
  49. package/dist/custom-elements.json +1603 -986
  50. package/dist/entity-base.d.cts +6 -5
  51. package/dist/entity-base.d.ts +6 -5
  52. package/dist/entity-owner.d.cts +118 -0
  53. package/dist/entity-owner.d.ts +118 -0
  54. package/dist/entity.d.cts +8 -101
  55. package/dist/entity.d.ts +8 -101
  56. package/dist/index.d.cts +22 -21
  57. package/dist/index.d.ts +22 -21
  58. package/dist/model.d.cts +67 -24
  59. package/dist/model.d.ts +67 -24
  60. package/dist/pwc.cjs +526 -268
  61. package/dist/pwc.cjs.map +1 -1
  62. package/dist/pwc.js +526 -268
  63. package/dist/pwc.js.map +1 -1
  64. package/dist/pwc.min.js +1 -1
  65. package/dist/pwc.min.js.map +1 -1
  66. package/dist/pwc.min.mjs +1 -1
  67. package/dist/pwc.min.mjs.map +1 -1
  68. package/dist/pwc.mjs +523 -266
  69. package/dist/pwc.mjs.map +1 -1
  70. package/dist/vscode.html-custom-data.json +145 -90
  71. package/dist/{module.d.cts → wasm.d.cts} +6 -6
  72. package/dist/{module.d.ts → wasm.d.ts} +6 -6
  73. package/dist/web-types.json +378 -250
  74. package/package.json +7 -7
  75. package/src/app.ts +45 -35
  76. package/src/async-element.ts +14 -14
  77. package/src/components/anim-component.ts +84 -14
  78. package/src/components/{listener-component.ts → audio-listener-component.ts} +8 -6
  79. package/src/components/button-component.ts +5 -1
  80. package/src/components/camera-component.ts +2 -0
  81. package/src/components/collision-component.ts +5 -1
  82. package/src/components/component.ts +4 -3
  83. package/src/components/element-component.ts +7 -0
  84. package/src/components/gsplat-component.ts +2 -0
  85. package/src/components/joint-component.ts +2 -0
  86. package/src/components/{layoutchild-component.ts → layout-child-component.ts} +4 -2
  87. package/src/components/{layoutgroup-component.ts → layout-group-component.ts} +4 -2
  88. package/src/components/light-component.ts +2 -0
  89. package/src/components/{particlesystem-component.ts → particle-system-component.ts} +4 -2
  90. package/src/components/render-component.ts +2 -0
  91. package/src/components/{rigidbody-component.ts → rigid-body-component.ts} +4 -2
  92. package/src/components/screen-component.ts +2 -0
  93. package/src/components/script-component.ts +63 -42
  94. package/src/components/{script.ts → script-instance.ts} +14 -14
  95. package/src/components/{scrollview-component.ts → scroll-view-component.ts} +4 -2
  96. package/src/components/scrollbar-component.ts +2 -0
  97. package/src/components/sound-component.ts +4 -2
  98. package/src/components/sound-slot.ts +4 -4
  99. package/src/entity-base.ts +6 -5
  100. package/src/entity-owner.ts +298 -0
  101. package/src/entity.ts +15 -270
  102. package/src/index.ts +26 -24
  103. package/src/model.ts +159 -72
  104. package/src/node.ts +9 -5
  105. package/src/{module.ts → wasm.ts} +8 -8
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcanvas/web-components",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "author": "PlayCanvas <support@playcanvas.com>",
5
5
  "homepage": "https://playcanvas.com",
6
6
  "description": "Web Components for the PlayCanvas Engine",
@@ -91,25 +91,25 @@
91
91
  "@rollup/plugin-terser": "1.0.0",
92
92
  "@rollup/plugin-typescript": "12.3.0",
93
93
  "@tweenjs/tween.js": "25.0.0",
94
- "@vitest/coverage-v8": "4.1.10",
95
- "concurrently": "10.0.4",
94
+ "@vitest/coverage-v8": "4.1.11",
95
+ "concurrently": "10.0.5",
96
96
  "custom-element-jet-brains-integration": "1.7.0",
97
97
  "custom-element-vs-code-integration": "1.5.0",
98
98
  "earcut": "3.2.3",
99
99
  "eslint": "9.39.5",
100
100
  "globals": "17.11.0",
101
101
  "jsdom": "30.0.1",
102
- "mediabunny": "1.53.1",
102
+ "mediabunny": "1.55.2",
103
103
  "opentype.js": "2.0.0",
104
104
  "playcanvas": "2.21.4",
105
105
  "prettier": "3.9.6",
106
- "publint": "0.3.23",
107
- "rollup": "4.62.4",
106
+ "publint": "0.3.24",
107
+ "rollup": "4.62.5",
108
108
  "serve": "14.2.6",
109
109
  "tslib": "2.8.1",
110
110
  "typedoc": "0.28.20",
111
111
  "typedoc-plugin-mdn-links": "5.1.1",
112
112
  "typescript": "6.0.3",
113
- "vitest": "4.1.10"
113
+ "vitest": "4.1.11"
114
114
  }
115
115
  }
package/src/app.ts CHANGED
@@ -66,16 +66,23 @@ import {
66
66
 
67
67
  import type { AssetElement } from './asset';
68
68
  import { AsyncElement } from './async-element';
69
- import type { EntityElement } from './entity';
70
69
  import type { EntityBaseElement } from './entity-base';
70
+ import type { EntityOwnerElement } from './entity-owner';
71
71
  import { LoadingBar } from './loading-bar';
72
72
  import type { MaterialElement } from './material';
73
- import type { ModuleElement } from './module';
74
73
  import { parseBool, parseEnum, parseNumber } from './parse';
74
+ import type { WasmElement } from './wasm';
75
75
 
76
76
  /** The pointer event types the application synthesizes on `<pc-entity>` elements via picking. */
77
77
  const pointerEventTypes = ['pointermove', 'pointerdown', 'pointerup', 'pointerenter', 'pointerleave'] as const;
78
78
 
79
+ /**
80
+ * The event types whose listeners make an element a hover target. Hover resolution walks past
81
+ * elements listening for none of them, so a silent element never swallows an ancestor's
82
+ * enter/leave pair.
83
+ */
84
+ const hoverEventTypes = ['pointerenter', 'pointerleave', 'pointermove'] as const;
85
+
79
86
  /**
80
87
  * Gives `pc-app` the sizing contract of a replaced element (`<video>`, `<img>`): a block-level
81
88
  * box that the page's CSS sizes, defaulting to the canvas's own 300x150 intrinsic size, with the
@@ -163,9 +170,9 @@ class AppElement extends AsyncElement {
163
170
 
164
171
  /**
165
172
  * The elements backing this application's entities, keyed by the entity itself. Registered
166
- * by EntityElement at creation (and NodeElement at binding) and removed when an entity is
167
- * destroyed or unbound, this joins engine scene nodes back to their owning elements by
168
- * identity - never by name.
173
+ * by entity-owning elements at creation (pc-entity, and pc-model for its host) and by
174
+ * NodeElement at binding, and removed when an entity is destroyed or unbound, this joins
175
+ * engine scene nodes back to their owning elements by identity - never by name.
169
176
  */
170
177
  private _entityElements = new Map<GraphNode, EntityBaseElement>();
171
178
 
@@ -260,7 +267,7 @@ class AppElement extends AsyncElement {
260
267
  // there, leaving the element permanently unready: no canvas, no entities, no application.
261
268
  //
262
269
  // Upgrading is the fix here rather than skipping whatever has not upgraded, because a
263
- // <pc-module> is the one child that nothing else ever builds on its own behalf - skipping
270
+ // <pc-wasm> is the one child that nothing else ever builds on its own behalf - skipping
264
271
  // it would drop the wasm module the app asked for, silently and only for cloned apps.
265
272
  // Upgrading runs each descendant's connectedCallback synchronously, a few lines earlier
266
273
  // than the parser's path runs them but into the same state they see there: no application
@@ -270,11 +277,11 @@ class AppElement extends AsyncElement {
270
277
  // is left completely untouched.
271
278
  customElements.upgrade(this);
272
279
 
273
- // Get all pc-module elements that are direct children of the pc-app element
274
- const moduleElements = this.querySelectorAll<ModuleElement>(':scope > pc-module');
280
+ // Get all pc-wasm elements that are direct children of the pc-app element
281
+ const wasmElements = this.querySelectorAll<WasmElement>(':scope > pc-wasm');
275
282
 
276
283
  // Wait for all modules to load
277
- await Promise.all(Array.from(moduleElements).map((module) => module._getLoadPromise()));
284
+ await Promise.all(Array.from(wasmElements).map((element) => element._getLoadPromise()));
278
285
 
279
286
  // The element may have been removed while the modules loaded. Nothing beyond the loading
280
287
  // bar exists yet, and disconnectedCallback has already destroyed that.
@@ -462,15 +469,16 @@ class AppElement extends AsyncElement {
462
469
  materialElement._createMaterial();
463
470
  });
464
471
 
465
- // Create all entities
466
- const entityElements = this.querySelectorAll<EntityElement>('pc-entity');
467
- Array.from(entityElements).forEach((entityElement) => {
468
- entityElement._createEntity(app);
472
+ // Create all entities. pc-model joins the sweep because it owns a host entity of its
473
+ // own; its instantiated content arrives later, beneath that host.
474
+ const ownerElements = this.querySelectorAll<EntityOwnerElement>('pc-entity, pc-model');
475
+ Array.from(ownerElements).forEach((ownerElement) => {
476
+ ownerElement._createEntity(app);
469
477
  });
470
478
 
471
479
  // Build hierarchy
472
- entityElements.forEach((entityElement) => {
473
- entityElement._buildHierarchy(app);
480
+ ownerElements.forEach((ownerElement) => {
481
+ ownerElement._buildHierarchy(app);
474
482
  });
475
483
 
476
484
  // Building the hierarchy dispatched each entity's ready event synchronously, and a
@@ -602,9 +610,9 @@ class AppElement extends AsyncElement {
602
610
  // created from onpointer* attributes when their elements were first upgraded, or
603
611
  // listeners carried over from before a re-boot)
604
612
  pointerEventTypes.forEach((type) => {
605
- const anyListeners = Array.from(this.querySelectorAll<EntityBaseElement>('pc-entity, pc-node')).some(
606
- (entity) => entity._hasListeners(type)
607
- );
613
+ const anyListeners = Array.from(
614
+ this.querySelectorAll<EntityBaseElement>('pc-entity, pc-model, pc-node')
615
+ ).some((entity) => entity._hasListeners(type));
608
616
  if (anyListeners) {
609
617
  this._onPointerListenerAdded(type);
610
618
  }
@@ -659,10 +667,10 @@ class AppElement extends AsyncElement {
659
667
  }
660
668
 
661
669
  /**
662
- * Returns the `<pc-entity>` or `<pc-node>` element whose backing entity is `entity`, or
663
- * `null` if the entity is not fronted by an element of this application - for example, an
664
- * unbound node inside a model's instantiated hierarchy, or an entity created through the
665
- * engine API.
670
+ * Returns the `<pc-entity>`, `<pc-model>` or `<pc-node>` element whose backing entity is
671
+ * `entity`, or `null` if the entity is not fronted by an element of this application - for
672
+ * example, an unbound node inside a model's instantiated hierarchy, or an entity created
673
+ * through the engine API.
666
674
  *
667
675
  * @param entity - The entity to look up.
668
676
  * @returns The element fronting the entity, or `null`.
@@ -672,18 +680,19 @@ class AppElement extends AsyncElement {
672
680
  }
673
681
 
674
682
  /**
675
- * Resolves the element that owns a picked node: the nearest node up the parent chain -
676
- * starting with the node itself - that is fronted by a `<pc-entity>` or `<pc-node>` of this
677
- * application. A hit inside a model's instantiated hierarchy therefore resolves to the
678
- * nearest bound `<pc-node>`, or failing that the element hosting the model.
683
+ * Resolves the element that owns hover for a picked node: the nearest node up the parent
684
+ * chain - starting with the node itself - whose element listens for any of the hover event
685
+ * types. Skipping silent elements matches {@link _elementWithListener}, so a registered
686
+ * element with no hover listeners (a `<pc-model>` host, a plain child entity) is transparent
687
+ * to hover rather than swallowing a listening ancestor's enter/leave pair.
679
688
  *
680
689
  * @param node - The picked node, or `null`.
681
- * @returns The owning element, or `null`.
690
+ * @returns The hover-owning element, or `null`.
682
691
  */
683
- private _elementFromNode(node: GraphNode | null): EntityBaseElement | null {
692
+ private _hoverTarget(node: GraphNode | null): EntityBaseElement | null {
684
693
  while (node !== null) {
685
694
  const element = this._entityElements.get(node);
686
- if (element) {
695
+ if (element && hoverEventTypes.some((type) => element._hasListeners(type))) {
687
696
  return element;
688
697
  }
689
698
  node = node.parent;
@@ -831,9 +840,10 @@ class AppElement extends AsyncElement {
831
840
  const node = await this._pickNode(event);
832
841
  if (token !== this._pickToken || !this._picker) return;
833
842
 
834
- // The hovered element is the nearest one up the node's parent chain, listening or not -
835
- // dispatch is gated per event type below
836
- const newHoverEntity = this._elementFromNode(node);
843
+ // The hovered element is the nearest one up the node's parent chain with a hover
844
+ // listener - the nearest-listener rule down/up use. Dispatch is still gated per event
845
+ // type below: having any hover listener selects the target, each event needs its own.
846
+ const newHoverEntity = this._hoverTarget(node);
837
847
 
838
848
  // Handle enter/leave events
839
849
  if (this._hoveredEntity !== newHoverEntity) {
@@ -898,9 +908,9 @@ class AppElement extends AsyncElement {
898
908
  }
899
909
 
900
910
  private _onPointerListenerRemoved(type: string) {
901
- const hasListeners = Array.from(this.querySelectorAll<EntityBaseElement>('pc-entity, pc-node')).some((entity) =>
902
- entity._hasListeners(type)
903
- );
911
+ const hasListeners = Array.from(
912
+ this.querySelectorAll<EntityBaseElement>('pc-entity, pc-model, pc-node')
913
+ ).some((entity) => entity._hasListeners(type));
904
914
 
905
915
  if (!hasListeners && this._canvas) {
906
916
  this._hasPointerListeners[type] = false;
@@ -33,13 +33,13 @@ class AsyncElement extends HTMLElement {
33
33
  }
34
34
 
35
35
  /**
36
- * The nearest ancestor element that fronts an entity — `<pc-entity>` or `<pc-node>` or
37
- * `null` if this element has no such ancestor. The search starts at the parent, so an element
38
- * never resolves to itself.
36
+ * The nearest ancestor element that fronts an entity — `<pc-entity>`, `<pc-model>` or
37
+ * `<pc-node>` — or `null` if this element has no such ancestor. The search starts at the
38
+ * parent, so an element never resolves to itself.
39
39
  * @returns The closest entity-fronting element, or `null`.
40
40
  */
41
41
  get closestEntity(): EntityBaseElement | null {
42
- return (this.parentElement?.closest('pc-entity, pc-node') as EntityBaseElement | null) ?? null;
42
+ return (this.parentElement?.closest('pc-entity, pc-model, pc-node') as EntityBaseElement | null) ?? null;
43
43
  }
44
44
 
45
45
  /**
@@ -95,11 +95,11 @@ type AsyncElementTagName = {
95
95
 
96
96
  /**
97
97
  * Waits for the first element matching the given tag name to be fully initialized. Note that the
98
- * promise never settles if the element cannot finish initializing (for example, a `<pc-script>`
99
- * that is not a direct child of `<pc-scripts>`, or a `<pc-app>` that could not create a graphics
100
- * device — listen for its `error` event instead). A component element outside a `<pc-entity>` is
101
- * the exception: it still becomes ready, but its `component` is `null`. Either way, a misplaced
102
- * element logs a warning naming the parent it requires.
98
+ * promise never settles if the element cannot finish initializing (for example, a `<pc-script-instance>`
99
+ * that is not a direct child of `<pc-script>`, or a `<pc-app>` that could not create a graphics
100
+ * device — listen for its `error` event instead). A component element outside an entity-fronting
101
+ * element is the exception: it still becomes ready, but its `component` is `null`. Either way, a
102
+ * misplaced element logs a warning naming the parent it requires.
103
103
  * @param target - The tag name of the element to wait for (e.g. `'pc-app'`).
104
104
  * @returns A promise that resolves with the element once it's ready.
105
105
  * @example
@@ -120,11 +120,11 @@ function whenReady<K extends AsyncElementTagName>(target: K): Promise<HTMLElemen
120
120
  function whenReady<T extends AsyncElement>(target: T): Promise<T>;
121
121
  /**
122
122
  * Waits for the first element matching the given CSS selector to be fully initialized. Note that
123
- * the promise never settles if the element cannot finish initializing (for example, a `<pc-script>`
124
- * that is not a direct child of `<pc-scripts>`, or a `<pc-app>` that could not create a graphics
125
- * device — listen for its `error` event instead). A component element outside a `<pc-entity>` is
126
- * the exception: it still becomes ready, but its `component` is `null`. Either way, a misplaced
127
- * element logs a warning naming the parent it requires.
123
+ * the promise never settles if the element cannot finish initializing (for example, a `<pc-script-instance>`
124
+ * that is not a direct child of `<pc-script>`, or a `<pc-app>` that could not create a graphics
125
+ * device — listen for its `error` event instead). A component element outside an entity-fronting
126
+ * element is the exception: it still becomes ready, but its `component` is `null`. Either way, a
127
+ * misplaced element logs a warning naming the parent it requires.
128
128
  * @param target - A CSS selector matching the element to wait for (e.g. `'#my-app'`).
129
129
  * @returns A promise that resolves with the element once it's ready.
130
130
  * @example
@@ -1,4 +1,4 @@
1
- import type { AnimComponent, Asset, ContainerResource } from 'playcanvas';
1
+ import type { AnimComponent, Asset, ContainerResource, Entity } from 'playcanvas';
2
2
  import { ANIM_CONTROL_STATES, AnimTrack } from 'playcanvas';
3
3
 
4
4
  import { AssetElement } from '../asset';
@@ -46,6 +46,8 @@ type PlaybackState = {
46
46
  * the underlying {@link AnimComponent} (via {@link component}) for playback state beyond what
47
47
  * this element exposes.
48
48
  *
49
+ * Engine component: {@link AnimComponent} (`anim`).
50
+ *
49
51
  * @category Components
50
52
  */
51
53
  class AnimComponentElement extends ComponentElement {
@@ -71,6 +73,12 @@ class AnimComponentElement extends ComponentElement {
71
73
  */
72
74
  private _clip = '';
73
75
 
76
+ /**
77
+ * The binding root this element last assigned through {@link _applyRootBone}, distinguishing
78
+ * its own writes from a `rootBone` assigned through the engine API — which is left alone.
79
+ */
80
+ private _managedRootBone: Entity | null = null;
81
+
74
82
  /**
75
83
  * The element the model-readiness listener is attached to, held so disconnection can detach
76
84
  * it after `closestEntity` no longer resolves.
@@ -101,28 +109,76 @@ class AnimComponentElement extends ComponentElement {
101
109
  private _warnedClip: string | null = null;
102
110
 
103
111
  /**
104
- * Rebinds when a model under the host announces readiness. The engine resolves each curve
105
- * once, at the first tick after assignment, and never retries and its mesh-instance
112
+ * Rebinds when a model that is not this component's host announces readiness a sibling or
113
+ * deeper model whose content changed under the assigned clips. The engine resolves each
114
+ * curve once, at the first tick after assignment, and never retries — and its mesh-instance
106
115
  * broadcast fires before an instantiated hierarchy is parented, so a model that loads after
107
- * the clips were assigned would otherwise stay silently unbound. A re-instantiation of the
108
- * implicit clip source (the parent `<pc-model>`) means a new container, so the clip set
109
- * refreshes instead unless every clip declares its own asset, where a rebind suffices.
116
+ * the clips were assigned would otherwise stay silently unbound. The host model's own cycles
117
+ * are excluded: those re-enter through {@link initComponent}, which refreshes the whole clip
118
+ * set against the new container.
110
119
  */
111
120
  private _onModelReady = (event: Event) => {
112
121
  if (!(event.target instanceof ModelElement) || !this.component) {
113
122
  return;
114
123
  }
115
- if (event.target === this.parentElement) {
116
- const implicit = this._autoAssigned ||
117
- [...this._assignedClips.values()].some(clip => !clip.asset);
118
- if (implicit) {
119
- this._refreshClips();
120
- return;
121
- }
124
+ // The host model's own readiness cycle already re-initialized this component through
125
+ // ComponentElement's host-ready listener, which is attached first and so has run by now.
126
+ // Acting here too would resolve every track twice and capture the playhead-restore
127
+ // snapshot mid-rebuild.
128
+ if (event.target === this._modelListenerTarget) {
129
+ return;
122
130
  }
131
+ // A model cycle can replace the skeleton source's host entity (a rebuild under a
132
+ // retargeting pc-node), so the binding root is re-asserted before the rebind.
133
+ this._applyRootBone();
123
134
  this.component.rebind();
124
135
  };
125
136
 
137
+ /**
138
+ * The model whose host entity scopes this component's curve binding: the parent `pc-model`,
139
+ * or the sole `pc-model` among the parent's direct children (the arrangement where clips
140
+ * live in a library asset beside the skeleton). `null` when there is no such model, or more
141
+ * than one — an ambiguous skeleton is left to the engine's name-based resolution.
142
+ */
143
+ private _skeletonSource(): ModelElement | null {
144
+ const parent = this.parentElement;
145
+ if (parent instanceof ModelElement) {
146
+ return parent;
147
+ }
148
+ const models = parent ? parent.querySelectorAll(':scope > pc-model') : null;
149
+ return models?.length === 1 && models[0] instanceof ModelElement ? models[0] : null;
150
+ }
151
+
152
+ /**
153
+ * Keeps the component's binding root pointing at the skeleton source's host entity. The host
154
+ * wraps the instantiated content, so left at its default — the component's own entity — the
155
+ * engine binder mis-resolves curves that target the asset's root node: its fallback treats
156
+ * the graph as the asset root once the root is no longer a direct child.
157
+ *
158
+ * Authoritative in both directions for values this element assigned: a source appearing pins
159
+ * its host, and a source dissolving (the model gone, or a second model making the skeleton
160
+ * ambiguous) clears the pin rather than leaving it on a stale host. A root assigned through
161
+ * the engine API is never overwritten — the user's choice outranks the managed default.
162
+ * Writes are skipped while unchanged, because the engine setter itself triggers a rebind.
163
+ */
164
+ private _applyRootBone() {
165
+ const component = this.component;
166
+
167
+ // A non-null root this element did not assign came through the engine API. A fresh
168
+ // component starts at null, which is always reclaimable.
169
+ if (component.rootBone !== null && component.rootBone !== this._managedRootBone) {
170
+ return;
171
+ }
172
+
173
+ const host = this._skeletonSource()?.entity ?? null;
174
+ if (component.rootBone !== host) {
175
+ // The engine setter accepts null - restoring the component's own entity as the
176
+ // binding graph - but its declared type does not
177
+ component.rootBone = host as Entity;
178
+ }
179
+ this._managedRootBone = host;
180
+ }
181
+
126
182
  /** @ignore */
127
183
  constructor() {
128
184
  super('anim');
@@ -151,7 +207,16 @@ class AnimComponentElement extends ComponentElement {
151
207
  this._modelListenerTarget = host;
152
208
  }
153
209
 
154
- this._applyClips();
210
+ this._applyRootBone();
211
+
212
+ if (this.component.baseLayer) {
213
+ // The component survived the host's readiness cycle (a pc-model reloading content on
214
+ // its stable host entity). A loaded graph cannot be reassigned in place, so drop it
215
+ // and reassign from the current source, restoring the active clip and playhead.
216
+ this._refreshClips();
217
+ } else {
218
+ this._applyClips();
219
+ }
155
220
  }
156
221
 
157
222
  disconnectedCallback() {
@@ -163,6 +228,7 @@ class AnimComponentElement extends ComponentElement {
163
228
  this._sourceGeneration++;
164
229
  this._assignedClips.clear();
165
230
  this._autoAssigned = false;
231
+ this._managedRootBone = null;
166
232
 
167
233
  super.disconnectedCallback();
168
234
  }
@@ -353,6 +419,10 @@ class AnimComponentElement extends ComponentElement {
353
419
  if (!component) {
354
420
  return;
355
421
  }
422
+ // A clip-set change is also a chance for the skeleton source to have changed shape (a
423
+ // clip child appearing or leaving can accompany a model coming or going) - re-derive the
424
+ // binding root before the reassignment binds against it.
425
+ this._applyRootBone();
356
426
  const layer = component.baseLayer;
357
427
  const restore = layer ? {
358
428
  state: layer.activeState,
@@ -3,14 +3,16 @@ import type { AudioListenerComponent } from 'playcanvas';
3
3
  import { ComponentElement } from './component';
4
4
 
5
5
  /**
6
- * The ListenerComponentElement interface provides properties and methods for manipulating
7
- * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-listener/ | `<pc-listener>`} elements.
8
- * The ListenerComponentElement interface also inherits the properties and methods of the
6
+ * The AudioListenerComponentElement interface provides properties and methods for manipulating
7
+ * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-audio-listener/ | `<pc-audio-listener>`} elements.
8
+ * The AudioListenerComponentElement interface also inherits the properties and methods of the
9
9
  * {@link HTMLElement} interface.
10
10
  *
11
+ * Engine component: {@link AudioListenerComponent} (`audiolistener`).
12
+ *
11
13
  * @category Components
12
14
  */
13
- class ListenerComponentElement extends ComponentElement {
15
+ class AudioListenerComponentElement extends ComponentElement {
14
16
  /** @ignore */
15
17
  constructor() {
16
18
  super('audiolistener');
@@ -25,6 +27,6 @@ class ListenerComponentElement extends ComponentElement {
25
27
  }
26
28
  }
27
29
 
28
- customElements.define('pc-listener', ListenerComponentElement);
30
+ customElements.define('pc-audio-listener', AudioListenerComponentElement);
29
31
 
30
- export { ListenerComponentElement };
32
+ export { AudioListenerComponentElement };
@@ -17,6 +17,8 @@ const transitionModes = new Map<'tint' | 'sprite', number>([
17
17
  * The ButtonComponentElement interface also inherits the properties and methods of the
18
18
  * {@link HTMLElement} interface.
19
19
  *
20
+ * Engine component: {@link ButtonComponent} (`button`).
21
+ *
20
22
  * @category Components
21
23
  */
22
24
  class ButtonComponentElement extends ComponentElement {
@@ -121,7 +123,9 @@ class ButtonComponentElement extends ComponentElement {
121
123
 
122
124
  /**
123
125
  * Sets the reference (CSS selector, element id or entity name) to the `<pc-entity>` whose image
124
- * element is used for visual transitions. Defaults to the button's own entity.
126
+ * element is used for visual transitions. Defaults to the button's own entity — inside a
127
+ * `<pc-model>`, that is the model's host entity, so supply an explicit reference to target a
128
+ * UI entity instead.
125
129
  * @param value - The image entity reference.
126
130
  */
127
131
  set image(value: string) {
@@ -41,6 +41,8 @@ const tonemaps = new Map<'none' | 'linear' | 'filmic' | 'hejl' | 'aces' | 'aces2
41
41
  * The CameraComponentElement interface also inherits the properties and methods of the
42
42
  * {@link HTMLElement} interface.
43
43
  *
44
+ * Engine component: {@link CameraComponent} (`camera`).
45
+ *
44
46
  * @category Components
45
47
  */
46
48
  class CameraComponentElement extends ComponentElement {
@@ -17,6 +17,8 @@ import { ComponentElement } from './component';
17
17
  * retargets or rebinds picks up the new node's geometry. An entity with no asset-backed render
18
18
  * component warns, and the collider has no shape.
19
19
  *
20
+ * Engine component: {@link CollisionComponent} (`collision`).
21
+ *
20
22
  * @category Components
21
23
  */
22
24
  class CollisionComponentElement extends ComponentElement {
@@ -64,7 +66,9 @@ class CollisionComponentElement extends ComponentElement {
64
66
  * no attribute to supply it - so the host's visible geometry, the meaning a mesh collider
65
67
  * on a glTF node carries, fills the gap. Runs on every application (so a rebound `pc-node`
66
68
  * recomputes it) and on a runtime switch to `type="mesh"`; an explicitly assigned
67
- * `renderAsset` is never overwritten.
69
+ * `renderAsset` is never overwritten. A `pc-model` host entity never carries a render
70
+ * component (the instantiated content beneath it does), so a mesh collider that should take
71
+ * an asset's geometry belongs on a bound `pc-node`.
68
72
  */
69
73
  private _applyMeshGeometryDefault() {
70
74
  const component = this.component;
@@ -109,7 +109,7 @@ class ComponentElement extends AsyncElement {
109
109
  // It still becomes ready (with a null `component`), so warn rather than fail silently
110
110
  const label = this.id ? ` '${this.id}'` : '';
111
111
  console.warn(
112
- `${this.tagName.toLowerCase()}${label} must be a descendant of pc-entity - component not added`
112
+ `${this.tagName.toLowerCase()}${label} must be a descendant of pc-entity, pc-model or pc-node - component not added`
113
113
  );
114
114
  return;
115
115
  }
@@ -214,8 +214,9 @@ class ComponentElement extends AsyncElement {
214
214
 
215
215
  /**
216
216
  * The PlayCanvas component instance. `null` until the element is ready, and also for an
217
- * element that is not a descendant of a `<pc-entity>` — await {@link whenReady} or the
218
- * element's `ready()` promise before accessing it.
217
+ * element that is not a descendant of an entity-fronting element (`<pc-entity>`,
218
+ * `<pc-model>` or `<pc-node>`) — await {@link whenReady} or the element's `ready()` promise
219
+ * before accessing it.
219
220
  * @returns The component instance, or `null`.
220
221
  */
221
222
  get component(): Component | null {
@@ -12,6 +12,13 @@ import { ComponentElement } from './component';
12
12
  * The ElementComponentElement interface also inherits the properties and methods of the
13
13
  * {@link HTMLElement} interface.
14
14
  *
15
+ * Despite the name, this is not a base class or a generic wrapper: it is the engine's 2D UI
16
+ * component, which gives its host entity a rectangle in a `<pc-screen>` hierarchy that draws
17
+ * either an image, a line of text or nothing (`type="image"`, `"text"` or `"group"`). The tag
18
+ * spells the engine component it adds, as every component element does.
19
+ *
20
+ * Engine component: {@link ElementComponent} (`element`).
21
+ *
15
22
  * @category Components
16
23
  */
17
24
  class ElementComponentElement extends ComponentElement {
@@ -11,6 +11,8 @@ import { ComponentElement } from './component';
11
11
  * The GSplatComponentElement interface also inherits the properties and methods of the
12
12
  * {@link HTMLElement} interface.
13
13
  *
14
+ * Engine component: {@link GSplatComponent} (`gsplat`).
15
+ *
14
16
  * @category Components
15
17
  */
16
18
  class GSplatComponentElement extends ComponentElement {
@@ -26,6 +26,8 @@ type MotionMode = 'locked' | 'limited' | 'free';
26
26
  * constraint breaks. A broken joint no longer constrains its bodies; calling `refreshFrames()` on
27
27
  * the underlying component re-attaches it. Bubbles and is composed.
28
28
  *
29
+ * Engine component: {@link JointComponent} (`joint`).
30
+ *
29
31
  * @category Components
30
32
  */
31
33
  class JointComponentElement extends ComponentElement {
@@ -6,10 +6,12 @@ import { ComponentElement } from './component';
6
6
 
7
7
  /**
8
8
  * The LayoutChildComponentElement interface provides properties and methods for manipulating
9
- * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-layoutchild/ | `<pc-layoutchild>`} elements.
9
+ * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-layout-child/ | `<pc-layout-child>`} elements.
10
10
  * The LayoutChildComponentElement interface also inherits the properties and methods of the
11
11
  * {@link HTMLElement} interface.
12
12
  *
13
+ * Engine component: {@link LayoutChildComponent} (`layoutchild`).
14
+ *
13
15
  * @category Components
14
16
  */
15
17
  class LayoutChildComponentElement extends ComponentElement {
@@ -229,6 +231,6 @@ class LayoutChildComponentElement extends ComponentElement {
229
231
  }
230
232
  }
231
233
 
232
- customElements.define('pc-layoutchild', LayoutChildComponentElement);
234
+ customElements.define('pc-layout-child', LayoutChildComponentElement);
233
235
 
234
236
  export { LayoutChildComponentElement };
@@ -28,10 +28,12 @@ const fittings = new Map<'none' | 'stretch' | 'shrink' | 'both', number>([
28
28
 
29
29
  /**
30
30
  * The LayoutGroupComponentElement interface provides properties and methods for manipulating
31
- * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-layoutgroup/ | `<pc-layoutgroup>`} elements.
31
+ * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-layout-group/ | `<pc-layout-group>`} elements.
32
32
  * The LayoutGroupComponentElement interface also inherits the properties and methods of the
33
33
  * {@link HTMLElement} interface.
34
34
  *
35
+ * Engine component: {@link LayoutGroupComponent} (`layoutgroup`).
36
+ *
35
37
  * @category Components
36
38
  */
37
39
  class LayoutGroupComponentElement extends ComponentElement {
@@ -304,6 +306,6 @@ class LayoutGroupComponentElement extends ComponentElement {
304
306
  }
305
307
  }
306
308
 
307
- customElements.define('pc-layoutgroup', LayoutGroupComponentElement);
309
+ customElements.define('pc-layout-group', LayoutGroupComponentElement);
308
310
 
309
311
  export { LayoutGroupComponentElement };
@@ -37,6 +37,8 @@ const shadowTypes = new Map<
37
37
  * The LightComponentElement interface also inherits the properties and methods of the
38
38
  * {@link HTMLElement} interface.
39
39
  *
40
+ * Engine component: {@link LightComponent} (`light`).
41
+ *
40
42
  * @category Components
41
43
  */
42
44
  class LightComponentElement extends ComponentElement {
@@ -6,10 +6,12 @@ import { ComponentElement } from './component';
6
6
 
7
7
  /**
8
8
  * The ParticleSystemComponentElement interface provides properties and methods for manipulating
9
- * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-particles/ | `<pc-particles>`} elements.
9
+ * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-particle-system/ | `<pc-particle-system>`} elements.
10
10
  * The ParticleSystemComponentElement interface also inherits the properties and methods of the
11
11
  * {@link HTMLElement} interface.
12
12
  *
13
+ * Engine component: {@link ParticleSystemComponent} (`particlesystem`).
14
+ *
13
15
  * @category Components
14
16
  */
15
17
  class ParticleSystemComponentElement extends ComponentElement {
@@ -147,6 +149,6 @@ class ParticleSystemComponentElement extends ComponentElement {
147
149
  }
148
150
  }
149
151
 
150
- customElements.define('pc-particles', ParticleSystemComponentElement);
152
+ customElements.define('pc-particle-system', ParticleSystemComponentElement);
151
153
 
152
154
  export { ParticleSystemComponentElement };
@@ -15,6 +15,8 @@ import { ComponentElement } from './component';
15
15
  * to `box`). It does not cover the engine's `asset` render type, since there is no way to supply
16
16
  * a render asset here — use `pc-model` for glTF content instead.
17
17
  *
18
+ * Engine component: {@link RenderComponent} (`render`).
19
+ *
18
20
  * @category Components
19
21
  */
20
22
  class RenderComponentElement extends ComponentElement {