@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/dist/pwc.mjs CHANGED
@@ -27,13 +27,13 @@ class AsyncElement extends HTMLElement {
27
27
  return this.parentElement?.closest('pc-app') ?? null;
28
28
  }
29
29
  /**
30
- * The nearest ancestor element that fronts an entity — `<pc-entity>` or `<pc-node>` or
31
- * `null` if this element has no such ancestor. The search starts at the parent, so an element
32
- * never resolves to itself.
30
+ * The nearest ancestor element that fronts an entity — `<pc-entity>`, `<pc-model>` or
31
+ * `<pc-node>` — or `null` if this element has no such ancestor. The search starts at the
32
+ * parent, so an element never resolves to itself.
33
33
  * @returns The closest entity-fronting element, or `null`.
34
34
  */
35
35
  get closestEntity() {
36
- return this.parentElement?.closest('pc-entity, pc-node') ?? null;
36
+ return this.parentElement?.closest('pc-entity, pc-model, pc-node') ?? null;
37
37
  }
38
38
  /**
39
39
  * Called when the element is fully initialized and ready. Subclasses should call this when
@@ -107,9 +107,9 @@ async function whenReady(target) {
107
107
  }
108
108
 
109
109
  /**
110
- * The ModuleElement interface provides properties and methods for manipulating
111
- * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-module/ | `<pc-module>`}
112
- * elements. The ModuleElement interface also inherits the properties and methods of the
110
+ * The WasmElement interface provides properties and methods for manipulating
111
+ * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-wasm/ | `<pc-wasm>`}
112
+ * elements. The WasmElement interface also inherits the properties and methods of the
113
113
  * {@link AsyncElement} interface.
114
114
  *
115
115
  * The attributes are read once, when the module starts loading - on the element's first
@@ -118,7 +118,7 @@ async function whenReady(target) {
118
118
  * modules configure engine-global state that never unloads, so readiness is not re-armed by
119
119
  * removing the element, and a re-inserted element does not load again.
120
120
  *
121
- * A `<pc-module>` without a `name` warns and never becomes ready; a containing `<pc-app>` still
121
+ * A `<pc-wasm>` without a `name` warns and never becomes ready; a containing `<pc-app>` still
122
122
  * boots.
123
123
  *
124
124
  * @attribute {string} name - The name of the WebAssembly module to configure, e.g. `Basis` or
@@ -128,7 +128,7 @@ async function whenReady(target) {
128
128
  * @attribute {string} fallback - The URL of the module's asm.js fallback script, used when
129
129
  * WebAssembly is unavailable.
130
130
  */
131
- class ModuleElement extends AsyncElement {
131
+ class WasmElement extends AsyncElement {
132
132
  _loadPromise = null;
133
133
  connectedCallback() {
134
134
  this._getLoadPromise();
@@ -136,7 +136,7 @@ class ModuleElement extends AsyncElement {
136
136
  async _loadModule() {
137
137
  const name = this.getAttribute('name');
138
138
  if (!name) {
139
- console.warn("pc-module requires a 'name' attribute - no module was configured");
139
+ console.warn("pc-wasm requires a 'name' attribute - no module was configured");
140
140
  return;
141
141
  }
142
142
  const config = {
@@ -171,7 +171,7 @@ class ModuleElement extends AsyncElement {
171
171
  return this._loadPromise;
172
172
  }
173
173
  }
174
- customElements.define('pc-module', ModuleElement);
174
+ customElements.define('pc-wasm', WasmElement);
175
175
 
176
176
  /** Covers the 0.2s opacity transition; jsdom never fires transitionend, so removal is timed. */
177
177
  const REMOVAL_DELAY_MS = 250;
@@ -728,6 +728,12 @@ const getEntity = (ref) => {
728
728
 
729
729
  /** The pointer event types the application synthesizes on `<pc-entity>` elements via picking. */
730
730
  const pointerEventTypes = ['pointermove', 'pointerdown', 'pointerup', 'pointerenter', 'pointerleave'];
731
+ /**
732
+ * The event types whose listeners make an element a hover target. Hover resolution walks past
733
+ * elements listening for none of them, so a silent element never swallows an ancestor's
734
+ * enter/leave pair.
735
+ */
736
+ const hoverEventTypes = ['pointerenter', 'pointerleave', 'pointermove'];
731
737
  /**
732
738
  * Gives `pc-app` the sizing contract of a replaced element (`<video>`, `<img>`): a block-level
733
739
  * box that the page's CSS sizes, defaulting to the canvas's own 300x150 intrinsic size, with the
@@ -802,9 +808,9 @@ class AppElement extends AsyncElement {
802
808
  _bootGeneration = 0;
803
809
  /**
804
810
  * The elements backing this application's entities, keyed by the entity itself. Registered
805
- * by EntityElement at creation (and NodeElement at binding) and removed when an entity is
806
- * destroyed or unbound, this joins engine scene nodes back to their owning elements by
807
- * identity - never by name.
811
+ * by entity-owning elements at creation (pc-entity, and pc-model for its host) and by
812
+ * NodeElement at binding, and removed when an entity is destroyed or unbound, this joins
813
+ * engine scene nodes back to their owning elements by identity - never by name.
808
814
  */
809
815
  _entityElements = new Map();
810
816
  _picker = null;
@@ -883,7 +889,7 @@ class AppElement extends AsyncElement {
883
889
  // there, leaving the element permanently unready: no canvas, no entities, no application.
884
890
  //
885
891
  // Upgrading is the fix here rather than skipping whatever has not upgraded, because a
886
- // <pc-module> is the one child that nothing else ever builds on its own behalf - skipping
892
+ // <pc-wasm> is the one child that nothing else ever builds on its own behalf - skipping
887
893
  // it would drop the wasm module the app asked for, silently and only for cloned apps.
888
894
  // Upgrading runs each descendant's connectedCallback synchronously, a few lines earlier
889
895
  // than the parser's path runs them but into the same state they see there: no application
@@ -892,10 +898,10 @@ class AppElement extends AsyncElement {
892
898
  // as any other disconnect is. An already-upgraded subtree - every other insertion path -
893
899
  // is left completely untouched.
894
900
  customElements.upgrade(this);
895
- // Get all pc-module elements that are direct children of the pc-app element
896
- const moduleElements = this.querySelectorAll(':scope > pc-module');
901
+ // Get all pc-wasm elements that are direct children of the pc-app element
902
+ const wasmElements = this.querySelectorAll(':scope > pc-wasm');
897
903
  // Wait for all modules to load
898
- await Promise.all(Array.from(moduleElements).map((module) => module._getLoadPromise()));
904
+ await Promise.all(Array.from(wasmElements).map((element) => element._getLoadPromise()));
899
905
  // The element may have been removed while the modules loaded. Nothing beyond the loading
900
906
  // bar exists yet, and disconnectedCallback has already destroyed that.
901
907
  if (generation !== this._bootGeneration) {
@@ -1065,14 +1071,15 @@ class AppElement extends AsyncElement {
1065
1071
  Array.from(materialElements).forEach((materialElement) => {
1066
1072
  materialElement._createMaterial();
1067
1073
  });
1068
- // Create all entities
1069
- const entityElements = this.querySelectorAll('pc-entity');
1070
- Array.from(entityElements).forEach((entityElement) => {
1071
- entityElement._createEntity(app);
1074
+ // Create all entities. pc-model joins the sweep because it owns a host entity of its
1075
+ // own; its instantiated content arrives later, beneath that host.
1076
+ const ownerElements = this.querySelectorAll('pc-entity, pc-model');
1077
+ Array.from(ownerElements).forEach((ownerElement) => {
1078
+ ownerElement._createEntity(app);
1072
1079
  });
1073
1080
  // Build hierarchy
1074
- entityElements.forEach((entityElement) => {
1075
- entityElement._buildHierarchy(app);
1081
+ ownerElements.forEach((ownerElement) => {
1082
+ ownerElement._buildHierarchy(app);
1076
1083
  });
1077
1084
  // Building the hierarchy dispatched each entity's ready event synchronously, and a
1078
1085
  // listener may have removed the element. The sweep itself degrades safely - destroying
@@ -1183,7 +1190,7 @@ class AppElement extends AsyncElement {
1183
1190
  // created from onpointer* attributes when their elements were first upgraded, or
1184
1191
  // listeners carried over from before a re-boot)
1185
1192
  pointerEventTypes.forEach((type) => {
1186
- const anyListeners = Array.from(this.querySelectorAll('pc-entity, pc-node')).some((entity) => entity._hasListeners(type));
1193
+ const anyListeners = Array.from(this.querySelectorAll('pc-entity, pc-model, pc-node')).some((entity) => entity._hasListeners(type));
1187
1194
  if (anyListeners) {
1188
1195
  this._onPointerListenerAdded(type);
1189
1196
  }
@@ -1233,10 +1240,10 @@ class AppElement extends AsyncElement {
1233
1240
  this._entityElements.delete(entity);
1234
1241
  }
1235
1242
  /**
1236
- * Returns the `<pc-entity>` or `<pc-node>` element whose backing entity is `entity`, or
1237
- * `null` if the entity is not fronted by an element of this application - for example, an
1238
- * unbound node inside a model's instantiated hierarchy, or an entity created through the
1239
- * engine API.
1243
+ * Returns the `<pc-entity>`, `<pc-model>` or `<pc-node>` element whose backing entity is
1244
+ * `entity`, or `null` if the entity is not fronted by an element of this application - for
1245
+ * example, an unbound node inside a model's instantiated hierarchy, or an entity created
1246
+ * through the engine API.
1240
1247
  *
1241
1248
  * @param entity - The entity to look up.
1242
1249
  * @returns The element fronting the entity, or `null`.
@@ -1245,18 +1252,19 @@ class AppElement extends AsyncElement {
1245
1252
  return this._entityElements.get(entity) ?? null;
1246
1253
  }
1247
1254
  /**
1248
- * Resolves the element that owns a picked node: the nearest node up the parent chain -
1249
- * starting with the node itself - that is fronted by a `<pc-entity>` or `<pc-node>` of this
1250
- * application. A hit inside a model's instantiated hierarchy therefore resolves to the
1251
- * nearest bound `<pc-node>`, or failing that the element hosting the model.
1255
+ * Resolves the element that owns hover for a picked node: the nearest node up the parent
1256
+ * chain - starting with the node itself - whose element listens for any of the hover event
1257
+ * types. Skipping silent elements matches {@link _elementWithListener}, so a registered
1258
+ * element with no hover listeners (a `<pc-model>` host, a plain child entity) is transparent
1259
+ * to hover rather than swallowing a listening ancestor's enter/leave pair.
1252
1260
  *
1253
1261
  * @param node - The picked node, or `null`.
1254
- * @returns The owning element, or `null`.
1262
+ * @returns The hover-owning element, or `null`.
1255
1263
  */
1256
- _elementFromNode(node) {
1264
+ _hoverTarget(node) {
1257
1265
  while (node !== null) {
1258
1266
  const element = this._entityElements.get(node);
1259
- if (element) {
1267
+ if (element && hoverEventTypes.some((type) => element._hasListeners(type))) {
1260
1268
  return element;
1261
1269
  }
1262
1270
  node = node.parent;
@@ -1392,9 +1400,10 @@ class AppElement extends AsyncElement {
1392
1400
  const node = await this._pickNode(event);
1393
1401
  if (token !== this._pickToken || !this._picker)
1394
1402
  return;
1395
- // The hovered element is the nearest one up the node's parent chain, listening or not -
1396
- // dispatch is gated per event type below
1397
- const newHoverEntity = this._elementFromNode(node);
1403
+ // The hovered element is the nearest one up the node's parent chain with a hover
1404
+ // listener - the nearest-listener rule down/up use. Dispatch is still gated per event
1405
+ // type below: having any hover listener selects the target, each event needs its own.
1406
+ const newHoverEntity = this._hoverTarget(node);
1398
1407
  // Handle enter/leave events
1399
1408
  if (this._hoveredEntity !== newHoverEntity) {
1400
1409
  if (this._hoveredEntity && this._hoveredEntity._hasListeners('pointerleave')) {
@@ -1446,7 +1455,7 @@ class AppElement extends AsyncElement {
1446
1455
  }
1447
1456
  }
1448
1457
  _onPointerListenerRemoved(type) {
1449
- const hasListeners = Array.from(this.querySelectorAll('pc-entity, pc-node')).some((entity) => entity._hasListeners(type));
1458
+ const hasListeners = Array.from(this.querySelectorAll('pc-entity, pc-model, pc-node')).some((entity) => entity._hasListeners(type));
1450
1459
  if (!hasListeners && this._canvas) {
1451
1460
  this._hasPointerListeners[type] = false;
1452
1461
  const handler = type === 'pointerenter' || type === 'pointerleave'
@@ -1637,11 +1646,12 @@ const POINTER_ATTRIBUTES = [
1637
1646
  'onpointermove'
1638
1647
  ];
1639
1648
  /**
1640
- * The base class for elements that front an engine {@link Entity}: `<pc-entity>`, which creates
1641
- * one, and `<pc-node>`, which binds to one inside a model's instantiated hierarchy. It carries
1642
- * what both need — the `entity` contract, registration with the owning application (which joins
1643
- * picked scene nodes back to elements by identity, never by name), and the pointer listener
1644
- * bookkeeping that lets the application lazily attach its canvas handlers.
1649
+ * The base class for elements that front an engine {@link Entity}: `<pc-entity>` and
1650
+ * `<pc-model>`, which create one, and `<pc-node>`, which binds to one inside a model's
1651
+ * instantiated hierarchy. It carries what all of them need — the `entity` contract, registration
1652
+ * with the owning application (which joins picked scene nodes back to elements by identity,
1653
+ * never by name), and the pointer listener bookkeeping that lets the application lazily attach
1654
+ * its canvas handlers.
1645
1655
  */
1646
1656
  class EntityBaseElement extends AsyncElement {
1647
1657
  _entity = null;
@@ -1744,17 +1754,17 @@ class EntityBaseElement extends AsyncElement {
1744
1754
  }
1745
1755
 
1746
1756
  /**
1747
- * Creates and parents the entities of every descendant `<pc-entity>` of `root`, in two passes so
1748
- * that no parent's existence depends on document order. Called wherever a subtree could not build
1749
- * itself: an element inserted into an application that is already running, and a `<pc-node>` whose
1750
- * children waited for it to bind.
1757
+ * Creates and parents the entities of every descendant entity-owning element of `root`, in two
1758
+ * passes so that no parent's existence depends on document order. Called wherever a subtree could
1759
+ * not build itself: an element inserted into an application that is already running, and a
1760
+ * `<pc-node>` whose children waited for it to bind.
1751
1761
  *
1752
1762
  * Descendants that are not yet custom elements are skipped, because there is nothing useful to do
1753
1763
  * for them and reaching for `_createEntity` would throw. A subtree cloned from a `<template>`
1754
1764
  * arrives entirely unupgraded — template content lives in an inert document, where custom element
1755
1765
  * definitions are never looked up — and appending the clone upgrades its elements in tree order,
1756
1766
  * an element before its descendants. So a sweep from an element's own `connectedCallback` sees
1757
- * plain `HTMLElement`s below it. Each becomes an `EntityElement` moments later and its own
1767
+ * plain `HTMLElement`s below it. Each becomes an entity-owning element moments later and its own
1758
1768
  * `connectedCallback` creates and parents it, by which time the ancestor it parents under has its
1759
1769
  * entity — the same guarantee tree order gives this sweep.
1760
1770
  *
@@ -1763,35 +1773,18 @@ class EntityBaseElement extends AsyncElement {
1763
1773
  * @internal
1764
1774
  */
1765
1775
  const buildDescendantEntities = (root, app) => {
1766
- const children = Array.from(root.querySelectorAll('pc-entity')).filter((child) => child instanceof EntityElement);
1776
+ const children = Array.from(root.querySelectorAll('pc-entity, pc-model')).filter((child) => child instanceof EntityOwnerElement);
1767
1777
  children.forEach((child) => child._createEntity(app));
1768
1778
  children.forEach((child) => child._buildHierarchy(app));
1769
1779
  };
1770
1780
  /**
1771
- * The EntityElement interface provides properties and methods for manipulating
1772
- * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-entity/ | `<pc-entity>`} elements.
1773
- * The EntityElement interface also inherits the properties and methods of the
1774
- * {@link HTMLElement} interface.
1775
- *
1776
- * The pointer events below are dispatched by the containing `<pc-app>` element when the pointer
1777
- * intersects this entity's geometry. They are only generated while the entity has a listener for
1778
- * them, registered either with {@link addEventListener} or with the matching inline `onpointer*`
1779
- * attribute.
1780
- *
1781
- * @attribute {string} onpointerenter - Script to run when the pointer moves onto the entity.
1782
- * @attribute {string} onpointerleave - Script to run when the pointer moves off the entity.
1783
- * @attribute {string} onpointermove - Script to run when the pointer moves over the entity.
1784
- * @attribute {string} onpointerdown - Script to run when a pointer button is pressed over the
1785
- * entity.
1786
- * @attribute {string} onpointerup - Script to run when a pointer button is released over the
1787
- * entity.
1788
- * @fires {PointerEvent} pointerenter - Fired when the pointer moves onto the entity.
1789
- * @fires {PointerEvent} pointerleave - Fired when the pointer moves off the entity.
1790
- * @fires {PointerEvent} pointermove - Fired when the pointer moves over the entity.
1791
- * @fires {PointerEvent} pointerdown - Fired when a pointer button is pressed over the entity.
1792
- * @fires {PointerEvent} pointerup - Fired when a pointer button is released over the entity.
1781
+ * The base class for elements that create and own their backing entity: `<pc-entity>` and
1782
+ * `<pc-model>`, whose host entity carries the same authored properties. It carries the cached
1783
+ * property state, entity creation and parenting, and the reset that follows the entity's
1784
+ * destruction. `<pc-node>` sits outside this class: it borrows an entity a model instantiated,
1785
+ * and its properties are nullable overrides rather than owned values.
1793
1786
  */
1794
- class EntityElement extends EntityBaseElement {
1787
+ class EntityOwnerElement extends EntityBaseElement {
1795
1788
  /**
1796
1789
  * Whether the entity is enabled.
1797
1790
  */
@@ -1817,7 +1810,8 @@ class EntityElement extends EntityBaseElement {
1817
1810
  */
1818
1811
  _tags = [];
1819
1812
  /**
1820
- * Whether the hierarchy has been built for this entity.
1813
+ * Whether the hierarchy has been built for this entity — set once {@link _buildHierarchy} has
1814
+ * parented it. Read by subclasses that gate work on the entity being in the scene graph.
1821
1815
  */
1822
1816
  _built = false;
1823
1817
  /**
@@ -1870,10 +1864,10 @@ class EntityElement extends EntityBaseElement {
1870
1864
  this._resetReady();
1871
1865
  }
1872
1866
  /**
1873
- * Parents the backing entity: under the entity of the nearest ancestor `<pc-entity>` or
1874
- * `<pc-node>` when there is one, and under the application root otherwise. Called by the
1875
- * containing `<pc-app>` element once a sweep has created every entity, so a parent's
1876
- * existence never depends on document order.
1867
+ * Parents the backing entity: under the entity of the nearest ancestor `<pc-entity>`,
1868
+ * `<pc-model>` or `<pc-node>` when there is one, and under the application root otherwise.
1869
+ * Called by the containing `<pc-app>` element once a sweep has created every entity, so a
1870
+ * parent's existence never depends on document order.
1877
1871
  *
1878
1872
  * @param app - The application whose root adopts parentless entities.
1879
1873
  * @internal
@@ -1888,6 +1882,13 @@ class EntityElement extends EntityBaseElement {
1888
1882
  if (closestEntity && !closestEntity.entity) {
1889
1883
  return;
1890
1884
  }
1885
+ // An owner that exists but is not yet parented is itself deferred behind an unresolved
1886
+ // node further up. Building beneath it would announce readiness for an entity that is
1887
+ // not in the scene graph; stay unbuilt with it - the node's bind sweeps the whole
1888
+ // subtree, ancestors before descendants.
1889
+ if (closestEntity instanceof EntityOwnerElement && !closestEntity._built) {
1890
+ return;
1891
+ }
1891
1892
  this._built = true;
1892
1893
  if (closestEntity?.entity) {
1893
1894
  closestEntity.entity.addChild(this.entity);
@@ -1895,35 +1896,16 @@ class EntityElement extends EntityBaseElement {
1895
1896
  else {
1896
1897
  app.root.addChild(this.entity);
1897
1898
  }
1898
- this._onReady();
1899
+ this._onBuilt();
1899
1900
  }
1900
- connectedCallback() {
1901
- // Wait for app to be ready
1902
- const closestApp = this.closestApp;
1903
- if (!closestApp) {
1904
- // An entity outside an application is inert and never becomes ready, so awaiting it
1905
- // hangs. Warn rather than fail silently, naming the parent it requires, as every other
1906
- // misplaced element does.
1907
- const name = this.getAttribute('name');
1908
- const label = name ? ` '${name}'` : '';
1909
- console.warn(`pc-entity${label} must be a descendant of pc-app - entity not created`);
1910
- return;
1911
- }
1912
- // If app is already running, create entity immediately
1913
- if (closestApp._hierarchyReady) {
1914
- const app = closestApp.app;
1915
- this._createEntity(app);
1916
- this._buildHierarchy(app);
1917
- // Handle any child entities that might exist
1918
- buildDescendantEntities(this, app);
1919
- }
1920
- }
1921
- disconnectedCallback() {
1922
- // Destroying the entity destroys its whole subtree, and the engine fires 'destroy' for
1923
- // every entity in it - so _onEntityDestroy resets this element AND every descendant
1924
- // element before the descendants' own disconnectedCallbacks run. Their entities are null
1925
- // by then, making this call a no-op for them.
1926
- this._entity?.destroy();
1901
+ /**
1902
+ * Called by {@link _buildHierarchy} once the backing entity has been parented — exactly once
1903
+ * per build cycle. The default announces readiness, which is what a parented `<pc-entity>`
1904
+ * means; `<pc-model>` overrides it to start loading content instead, because its readiness
1905
+ * tracks the content settling rather than the host entering the scene graph.
1906
+ */
1907
+ _onBuilt() {
1908
+ this._onReady();
1927
1909
  }
1928
1910
  /**
1929
1911
  * Sets the enabled state of the entity.
@@ -2028,6 +2010,70 @@ class EntityElement extends EntityBaseElement {
2028
2010
  get tags() {
2029
2011
  return this._tags;
2030
2012
  }
2013
+ }
2014
+
2015
+ /**
2016
+ * The EntityElement interface provides properties and methods for manipulating
2017
+ * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-entity/ | `<pc-entity>`} elements.
2018
+ * The EntityElement interface also inherits the properties and methods of the
2019
+ * {@link HTMLElement} interface.
2020
+ *
2021
+ * The pointer events below are dispatched by the containing `<pc-app>` element when the pointer
2022
+ * intersects this entity's geometry. They are only generated while the entity has a listener for
2023
+ * them, registered either with {@link addEventListener} or with the matching inline `onpointer*`
2024
+ * attribute.
2025
+ *
2026
+ * @attribute {boolean} enabled - The enabled state of the entity.
2027
+ * @attribute {string} name - The name of the entity.
2028
+ * @attribute {string} position - The position of the entity.
2029
+ * @attribute {string} rotation - The rotation of the entity.
2030
+ * @attribute {string} scale - The scale of the entity.
2031
+ * @attribute {string} tags - The tags of the entity.
2032
+ * @attribute {string} onpointerenter - Script to run when the pointer moves onto the entity.
2033
+ * @attribute {string} onpointerleave - Script to run when the pointer moves off the entity.
2034
+ * @attribute {string} onpointermove - Script to run when the pointer moves over the entity.
2035
+ * @attribute {string} onpointerdown - Script to run when a pointer button is pressed over the
2036
+ * entity.
2037
+ * @attribute {string} onpointerup - Script to run when a pointer button is released over the
2038
+ * entity.
2039
+ * @fires {PointerEvent} pointerenter - Fired when the pointer moves onto the entity.
2040
+ * @fires {PointerEvent} pointerleave - Fired when the pointer moves off the entity.
2041
+ * @fires {PointerEvent} pointermove - Fired when the pointer moves over the entity.
2042
+ * @fires {PointerEvent} pointerdown - Fired when a pointer button is pressed over the entity.
2043
+ * @fires {PointerEvent} pointerup - Fired when a pointer button is released over the entity.
2044
+ */
2045
+ class EntityElement extends EntityOwnerElement {
2046
+ connectedCallback() {
2047
+ // Wait for app to be ready
2048
+ const closestApp = this.closestApp;
2049
+ if (!closestApp) {
2050
+ // An entity outside an application is inert and never becomes ready, so awaiting it
2051
+ // hangs. Warn rather than fail silently, naming the parent it requires, as every other
2052
+ // misplaced element does.
2053
+ const name = this.getAttribute('name');
2054
+ const label = name ? ` '${name}'` : '';
2055
+ console.warn(`pc-entity${label} must be a descendant of pc-app - entity not created`);
2056
+ return;
2057
+ }
2058
+ // If app is already running, create entity immediately
2059
+ if (closestApp._hierarchyReady) {
2060
+ const app = closestApp.app;
2061
+ this._createEntity(app);
2062
+ this._buildHierarchy(app);
2063
+ // Handle any child entities that might exist. A build that deferred (an unresolved
2064
+ // pc-node above) defers the whole subtree with it - the node's bind sweeps it.
2065
+ if (this._built) {
2066
+ buildDescendantEntities(this, app);
2067
+ }
2068
+ }
2069
+ }
2070
+ disconnectedCallback() {
2071
+ // Destroying the entity destroys its whole subtree, and the engine fires 'destroy' for
2072
+ // every entity in it - so _onEntityDestroy resets this element AND every descendant
2073
+ // element before the descendants' own disconnectedCallbacks run. Their entities are null
2074
+ // by then, making this call a no-op for them.
2075
+ this._entity?.destroy();
2076
+ }
2031
2077
  static get observedAttributes() {
2032
2078
  return ['enabled', 'name', 'position', 'rotation', 'scale', 'tags', ...POINTER_ATTRIBUTES];
2033
2079
  }
@@ -2965,14 +3011,42 @@ const formatHierarchy = (root, counts) => {
2965
3011
  * The ModelElement interface also inherits the properties and methods of the
2966
3012
  * {@link HTMLElement} interface.
2967
3013
  *
2968
- * The element becomes ready once its container asset has loaded and the instantiated hierarchy has
2969
- * been added to the scene `entity` is non-null by then. A failed load also settles readiness,
2970
- * with `entity` remaining `null`: readiness means the load settled, not that it succeeded — listen
2971
- * for `error`, or check `entity`, to tell the outcomes apart. Changing `asset` re-arms readiness
2972
- * and instantiates anew, so a `ready()` obtained after the change resolves against the new
2973
- * hierarchy. A `pc-model` outside a `pc-app`, or referencing an unknown asset id, warns and never
2974
- * becomes ready.
3014
+ * The element creates and fronts a stable host entity: `entity` is that host, created when the
3015
+ * application builds its hierarchy and kept across `asset` changes, so the element's transform
3016
+ * and tags are instance placement that composes with whatever transform the asset authored on
3017
+ * its root. The instantiated content is parented beneath the host and exposed as
3018
+ * {@link contentEntity}.
3019
+ *
3020
+ * The element becomes ready once its current asset selection has settled: the container asset
3021
+ * has loaded and its content root has been parented beneath the host, the load has failed
3022
+ * (`contentEntity` stays `null` — listen for `error`, or check `contentEntity`, to tell the
3023
+ * outcomes apart), or no asset is assigned at all. Changing `asset` re-arms readiness and
3024
+ * instantiates anew, so a `ready()` obtained after the change resolves against the new content.
3025
+ * A `pc-model` outside a `pc-app`, or referencing an unknown asset id, warns and never becomes
3026
+ * ready.
3027
+ *
3028
+ * The pointer events below are dispatched by the containing `<pc-app>` element when the pointer
3029
+ * intersects the model's geometry, exactly as for `<pc-entity>` — a hit on a content node that no
3030
+ * `pc-node` fronts resolves to this element.
2975
3031
  *
3032
+ * @attribute {boolean} enabled - The enabled state of the model.
3033
+ * @attribute {string} name - The name of the model.
3034
+ * @attribute {string} position - The position of the model.
3035
+ * @attribute {string} rotation - The rotation of the model.
3036
+ * @attribute {string} scale - The scale of the model.
3037
+ * @attribute {string} tags - The tags of the model.
3038
+ * @attribute {string} onpointerenter - Script to run when the pointer moves onto the model.
3039
+ * @attribute {string} onpointerleave - Script to run when the pointer moves off the model.
3040
+ * @attribute {string} onpointermove - Script to run when the pointer moves over the model.
3041
+ * @attribute {string} onpointerdown - Script to run when a pointer button is pressed over the
3042
+ * model.
3043
+ * @attribute {string} onpointerup - Script to run when a pointer button is released over the
3044
+ * model.
3045
+ * @fires {PointerEvent} pointerenter - Fired when the pointer moves onto the model.
3046
+ * @fires {PointerEvent} pointerleave - Fired when the pointer moves off the model.
3047
+ * @fires {PointerEvent} pointermove - Fired when the pointer moves over the model.
3048
+ * @fires {PointerEvent} pointerdown - Fired when a pointer button is pressed over the model.
3049
+ * @fires {PointerEvent} pointerup - Fired when a pointer button is released over the model.
2976
3050
  * @fires {Event} load - Fired each time a container asset finishes instantiating, including
2977
3051
  * re-instantiation after `asset` changes. Does not bubble — listen on this element, or use a
2978
3052
  * capture-phase listener on an ancestor.
@@ -2980,14 +3054,14 @@ const formatHierarchy = (root, counts) => {
2980
3054
  * error in `message`. Does not bubble. The element still becomes ready — readiness means the load
2981
3055
  * settled, not that it succeeded.
2982
3056
  */
2983
- class ModelElement extends AsyncElement {
3057
+ class ModelElement extends EntityOwnerElement {
2984
3058
  _asset = '';
2985
- _entity = null;
3059
+ _contentEntity = null;
2986
3060
  /**
2987
- * Incremented on every new load and on disconnect, and captured by a load when it starts. A
2988
- * load that resumes from an await or a load callback abandons itself if the value has moved
2989
- * on, so a superseded load can neither instantiate a second entity nor parent one that has
2990
- * since been destroyed.
3061
+ * Incremented on every new load, on disconnect, and when the host entity dies, and captured
3062
+ * by a load when it starts. A load that resumes from an await or a load callback abandons
3063
+ * itself if the value has moved on, so a superseded load can neither instantiate a second
3064
+ * content root nor parent one under a host a newer cycle has already replaced.
2991
3065
  */
2992
3066
  _loadGeneration = 0;
2993
3067
  /**
@@ -2998,12 +3072,13 @@ class ModelElement extends AsyncElement {
2998
3072
  _loadHandle = null;
2999
3073
  _errorHandle = null;
3000
3074
  /**
3001
- * The root entity of the instantiated model. `null` until the container asset has loaded
3002
- * and been instantiated, and again once the element has been removed from the document.
3003
- * @returns The model's root entity, or `null`.
3075
+ * The root entity of the instantiated model content, parented beneath the host entity.
3076
+ * `null` until the container asset has loaded and been instantiated, after a failed load,
3077
+ * and again once the element has been removed from the document.
3078
+ * @returns The content root entity, or `null`.
3004
3079
  */
3005
- get entity() {
3006
- return this._entity;
3080
+ get contentEntity() {
3081
+ return this._contentEntity;
3007
3082
  }
3008
3083
  /**
3009
3084
  * Returns a snapshot of the instantiated node tree, or `null` while there is none (the
@@ -3015,12 +3090,13 @@ class ModelElement extends AsyncElement {
3015
3090
  * is the printable form.
3016
3091
  *
3017
3092
  * The snapshot is plain data, computed afresh each call: it does not follow later changes
3018
- * to the hierarchy, and mutating it changes nothing.
3093
+ * to the hierarchy, and mutating it changes nothing. It covers the instantiated content
3094
+ * only — the host entity the element fronts is not part of the asset's node tree.
3019
3095
  *
3020
3096
  * @returns The root of the instantiated node tree, or `null`.
3021
3097
  */
3022
3098
  hierarchy() {
3023
- const root = this._entity;
3099
+ const root = this._contentEntity;
3024
3100
  if (!root) {
3025
3101
  return null;
3026
3102
  }
@@ -3060,18 +3136,53 @@ class ModelElement extends AsyncElement {
3060
3136
  // A model outside an application is inert and never becomes ready, so awaiting it hangs.
3061
3137
  // Warn rather than fail silently, naming the parent it requires, as every other misplaced
3062
3138
  // element does.
3063
- if (!this.closestApp) {
3139
+ const closestApp = this.closestApp;
3140
+ if (!closestApp) {
3064
3141
  const label = this._asset ? ` '${this._asset}'` : '';
3065
3142
  console.warn(`pc-model${label} must be a descendant of pc-app - model not created`);
3066
3143
  return;
3067
3144
  }
3068
- this._loadModel();
3145
+ // If the app is already running, create the host immediately; during a boot, the app's
3146
+ // own sweep does it. Either way, _onBuilt starts the content load once the host is
3147
+ // parented.
3148
+ if (closestApp._hierarchyReady) {
3149
+ const app = closestApp.app;
3150
+ this._createEntity(app);
3151
+ this._buildHierarchy(app);
3152
+ // A build that deferred (an unresolved pc-node above) defers the whole subtree with
3153
+ // it - the node's bind sweeps it.
3154
+ if (this._built) {
3155
+ buildDescendantEntities(this, app);
3156
+ }
3157
+ }
3069
3158
  }
3070
3159
  disconnectedCallback() {
3160
+ // Destroying the host destroys the instantiated content with it, and the destroy hook
3161
+ // resets the element. The generation guard comes first so a load suspended on an await
3162
+ // cannot resume against the torn-down element.
3071
3163
  this._loadGeneration++;
3072
3164
  this._detachLoadHandlers();
3073
- this._unloadModel();
3074
- this._resetReady();
3165
+ this._entity?.destroy();
3166
+ }
3167
+ /**
3168
+ * Starts (or restarts) the content load once the host has been parented. Readiness is not
3169
+ * announced here — it tracks the content settling, not the host entering the scene graph.
3170
+ */
3171
+ _onBuilt() {
3172
+ this._loadContent();
3173
+ }
3174
+ /**
3175
+ * Extends the owner reset for the content: the engine's destroy cascade has already taken
3176
+ * the content root down with the host subtree, so only the reference and the in-flight load
3177
+ * are dropped here. The next build re-creates the host and re-instantiates the content.
3178
+ *
3179
+ * @param entity - The host entity that was destroyed.
3180
+ */
3181
+ _onEntityDestroy(entity) {
3182
+ this._loadGeneration++;
3183
+ this._detachLoadHandlers();
3184
+ this._contentEntity = null;
3185
+ super._onEntityDestroy(entity);
3075
3186
  }
3076
3187
  _detachLoadHandlers() {
3077
3188
  this._loadHandle?.off();
@@ -3080,51 +3191,34 @@ class ModelElement extends AsyncElement {
3080
3191
  this._errorHandle = null;
3081
3192
  }
3082
3193
  /**
3083
- * Resolves readiness and dispatches the `load` event. Called once the instantiated hierarchy
3084
- * has been parented — readiness means "in the scene graph", matching `pc-entity`, so a ready
3085
- * model's entity always has world transforms.
3194
+ * Resolves readiness and dispatches the `load` event. Called once the instantiated content
3195
+ * has been parented beneath the host the host itself is already in the scene graph by
3196
+ * then, so a ready model's content always has world transforms.
3086
3197
  */
3087
3198
  _announceLoad() {
3088
3199
  this._onReady();
3089
3200
  this.dispatchEvent(new Event('load'));
3090
3201
  }
3091
3202
  _instantiate(container) {
3092
- const generation = this._loadGeneration;
3093
- const entity = container.instantiateRenderEntity();
3094
- this._entity = entity;
3095
- // The parent's readiness re-arms when it is torn down, so these can resume in a later
3096
- // connection cycle. The entity is captured above and the generation re-checked, so a
3097
- // stale resume cannot parent an entity a newer cycle has already destroyed.
3098
- const parentEntityElement = this.closestEntity;
3099
- if (parentEntityElement) {
3100
- parentEntityElement.ready().then(() => {
3101
- if (generation !== this._loadGeneration) {
3102
- return;
3103
- }
3104
- parentEntityElement.entity.addChild(entity);
3105
- this._announceLoad();
3106
- });
3107
- }
3108
- else {
3109
- const appElement = this.closestApp;
3110
- if (appElement) {
3111
- appElement.ready().then(() => {
3112
- if (generation !== this._loadGeneration) {
3113
- return;
3114
- }
3115
- appElement.app.root.addChild(entity);
3116
- this._announceLoad();
3117
- });
3118
- }
3119
- }
3120
- }
3121
- async _loadModel() {
3122
- this._unloadModel();
3203
+ const content = container.instantiateRenderEntity();
3204
+ this._contentEntity = content;
3205
+ this._entity.addChild(content);
3206
+ this._announceLoad();
3207
+ }
3208
+ _destroyContent() {
3209
+ this._contentEntity?.destroy();
3210
+ this._contentEntity = null;
3211
+ }
3212
+ async _loadContent() {
3213
+ // The old content goes down synchronously, so a reader that checks after an asset change
3214
+ // never sees the outgoing hierarchy. The host survives - components and child entities
3215
+ // attached to it carry over to the new content.
3216
+ this._destroyContent();
3123
3217
  // Supersede any load already in flight - only the newest load may instantiate
3124
3218
  const generation = ++this._loadGeneration;
3125
3219
  this._detachLoadHandlers();
3126
3220
  // Re-arm readiness so a waiter obtained after an asset change resolves against the new
3127
- // hierarchy. A no-op on first connection, where readiness is still pending.
3221
+ // content. A no-op on first connection, where readiness is still pending.
3128
3222
  this._resetReady();
3129
3223
  const appElement = this.closestApp;
3130
3224
  if (!appElement) {
@@ -3136,14 +3230,23 @@ class ModelElement extends AsyncElement {
3136
3230
  if (generation !== this._loadGeneration) {
3137
3231
  return;
3138
3232
  }
3233
+ // The host may not be parented yet - a model under a pc-node that has not bound, reached
3234
+ // through the asset setter. Nothing settles here: _onBuilt re-runs this load once the
3235
+ // host builds.
3236
+ if (!this._entity || !this._built) {
3237
+ return;
3238
+ }
3239
+ if (this._asset === '') {
3240
+ // No asset assigned is a settled selection: the element is a usable host (components
3241
+ // attach, waiters resolve) with no content. Assigning an asset later re-arms.
3242
+ this._onReady();
3243
+ return;
3244
+ }
3139
3245
  const asset = useAsset(this._asset);
3140
3246
  if (!asset) {
3141
- // An empty id is a legitimate transient (the asset may be assigned later); a
3142
- // non-empty one that resolves to nothing is a dead end - say so rather than staying
3247
+ // A non-empty id that resolves to nothing is a dead end - say so rather than staying
3143
3248
  // silently pending.
3144
- if (this._asset) {
3145
- console.warn(`pc-model could not find asset '${this._asset}' - model not created`);
3146
- }
3249
+ console.warn(`pc-model could not find asset '${this._asset}' - model not created`);
3147
3250
  return;
3148
3251
  }
3149
3252
  if (asset.loaded) {
@@ -3165,7 +3268,7 @@ class ModelElement extends AsyncElement {
3165
3268
  if (generation !== this._loadGeneration) {
3166
3269
  return;
3167
3270
  }
3168
- // A failed load settles readiness with a null entity, mirroring pc-asset:
3271
+ // A failed load settles readiness with a null contentEntity, mirroring pc-asset:
3169
3272
  // readiness means the load settled, not that it succeeded.
3170
3273
  this.dispatchEvent(new ErrorEvent('error', {
3171
3274
  message: err instanceof Error ? err.message : String(err)
@@ -3174,10 +3277,6 @@ class ModelElement extends AsyncElement {
3174
3277
  });
3175
3278
  }
3176
3279
  }
3177
- _unloadModel() {
3178
- this._entity?.destroy();
3179
- this._entity = null;
3180
- }
3181
3280
  /**
3182
3281
  * Sets the id of the `pc-asset` to use for the model.
3183
3282
  * @param value - The asset ID.
@@ -3185,7 +3284,7 @@ class ModelElement extends AsyncElement {
3185
3284
  set asset(value) {
3186
3285
  this._asset = value;
3187
3286
  if (this.isConnected) {
3188
- this._loadModel();
3287
+ this._loadContent();
3189
3288
  }
3190
3289
  }
3191
3290
  /**
@@ -3196,13 +3295,38 @@ class ModelElement extends AsyncElement {
3196
3295
  return this._asset;
3197
3296
  }
3198
3297
  static get observedAttributes() {
3199
- return ['asset'];
3298
+ return ['asset', 'enabled', 'name', 'position', 'rotation', 'scale', 'tags', ...POINTER_ATTRIBUTES];
3200
3299
  }
3201
3300
  attributeChangedCallback(name, _oldValue, newValue) {
3202
3301
  switch (name) {
3203
3302
  case 'asset':
3204
3303
  this.asset = newValue ?? '';
3205
3304
  break;
3305
+ case 'enabled':
3306
+ this.enabled = parseBool(newValue, true);
3307
+ break;
3308
+ case 'name':
3309
+ this.name = newValue ?? 'Untitled';
3310
+ break;
3311
+ case 'position':
3312
+ this.position = parseVec3(newValue, Vec3.ZERO, name);
3313
+ break;
3314
+ case 'rotation':
3315
+ this.rotation = parseVec3(newValue, Vec3.ZERO, name);
3316
+ break;
3317
+ case 'scale':
3318
+ this.scale = parseVec3(newValue, Vec3.ONE, name);
3319
+ break;
3320
+ case 'tags':
3321
+ this.tags = parseTags(newValue);
3322
+ break;
3323
+ case 'onpointerenter':
3324
+ case 'onpointerleave':
3325
+ case 'onpointerdown':
3326
+ case 'onpointerup':
3327
+ case 'onpointermove':
3328
+ this._updateInlineHandler(name, newValue);
3329
+ break;
3206
3330
  }
3207
3331
  }
3208
3332
  }
@@ -3293,7 +3417,7 @@ class ComponentElement extends AsyncElement {
3293
3417
  // A component can only exist on an entity, so an element placed outside one is inert.
3294
3418
  // It still becomes ready (with a null `component`), so warn rather than fail silently
3295
3419
  const label = this.id ? ` '${this.id}'` : '';
3296
- console.warn(`${this.tagName.toLowerCase()}${label} must be a descendant of pc-entity - component not added`);
3420
+ console.warn(`${this.tagName.toLowerCase()}${label} must be a descendant of pc-entity, pc-model or pc-node - component not added`);
3297
3421
  return;
3298
3422
  }
3299
3423
  await entityElement.ready();
@@ -3381,8 +3505,9 @@ class ComponentElement extends AsyncElement {
3381
3505
  }
3382
3506
  /**
3383
3507
  * The PlayCanvas component instance. `null` until the element is ready, and also for an
3384
- * element that is not a descendant of a `<pc-entity>` — await {@link whenReady} or the
3385
- * element's `ready()` promise before accessing it.
3508
+ * element that is not a descendant of an entity-fronting element (`<pc-entity>`,
3509
+ * `<pc-model>` or `<pc-node>`) — await {@link whenReady} or the element's `ready()` promise
3510
+ * before accessing it.
3386
3511
  * @returns The component instance, or `null`.
3387
3512
  */
3388
3513
  get component() {
@@ -3435,6 +3560,8 @@ class ComponentElement extends AsyncElement {
3435
3560
  * the underlying {@link AnimComponent} (via {@link component}) for playback state beyond what
3436
3561
  * this element exposes.
3437
3562
  *
3563
+ * Engine component: {@link AnimComponent} (`anim`).
3564
+ *
3438
3565
  * @category Components
3439
3566
  */
3440
3567
  class AnimComponentElement extends ComponentElement {
@@ -3456,6 +3583,11 @@ class AnimComponentElement extends ComponentElement {
3456
3583
  * The name of the active clip.
3457
3584
  */
3458
3585
  _clip = '';
3586
+ /**
3587
+ * The binding root this element last assigned through {@link _applyRootBone}, distinguishing
3588
+ * its own writes from a `rootBone` assigned through the engine API — which is left alone.
3589
+ */
3590
+ _managedRootBone = null;
3459
3591
  /**
3460
3592
  * The element the model-readiness listener is attached to, held so disconnection can detach
3461
3593
  * it after `closestEntity` no longer resolves.
@@ -3481,27 +3613,71 @@ class AnimComponentElement extends ComponentElement {
3481
3613
  */
3482
3614
  _warnedClip = null;
3483
3615
  /**
3484
- * Rebinds when a model under the host announces readiness. The engine resolves each curve
3485
- * once, at the first tick after assignment, and never retries and its mesh-instance
3616
+ * Rebinds when a model that is not this component's host announces readiness a sibling or
3617
+ * deeper model whose content changed under the assigned clips. The engine resolves each
3618
+ * curve once, at the first tick after assignment, and never retries — and its mesh-instance
3486
3619
  * broadcast fires before an instantiated hierarchy is parented, so a model that loads after
3487
- * the clips were assigned would otherwise stay silently unbound. A re-instantiation of the
3488
- * implicit clip source (the parent `<pc-model>`) means a new container, so the clip set
3489
- * refreshes instead unless every clip declares its own asset, where a rebind suffices.
3620
+ * the clips were assigned would otherwise stay silently unbound. The host model's own cycles
3621
+ * are excluded: those re-enter through {@link initComponent}, which refreshes the whole clip
3622
+ * set against the new container.
3490
3623
  */
3491
3624
  _onModelReady = (event) => {
3492
3625
  if (!(event.target instanceof ModelElement) || !this.component) {
3493
3626
  return;
3494
3627
  }
3495
- if (event.target === this.parentElement) {
3496
- const implicit = this._autoAssigned ||
3497
- [...this._assignedClips.values()].some(clip => !clip.asset);
3498
- if (implicit) {
3499
- this._refreshClips();
3500
- return;
3501
- }
3628
+ // The host model's own readiness cycle already re-initialized this component through
3629
+ // ComponentElement's host-ready listener, which is attached first and so has run by now.
3630
+ // Acting here too would resolve every track twice and capture the playhead-restore
3631
+ // snapshot mid-rebuild.
3632
+ if (event.target === this._modelListenerTarget) {
3633
+ return;
3502
3634
  }
3635
+ // A model cycle can replace the skeleton source's host entity (a rebuild under a
3636
+ // retargeting pc-node), so the binding root is re-asserted before the rebind.
3637
+ this._applyRootBone();
3503
3638
  this.component.rebind();
3504
3639
  };
3640
+ /**
3641
+ * The model whose host entity scopes this component's curve binding: the parent `pc-model`,
3642
+ * or the sole `pc-model` among the parent's direct children (the arrangement where clips
3643
+ * live in a library asset beside the skeleton). `null` when there is no such model, or more
3644
+ * than one — an ambiguous skeleton is left to the engine's name-based resolution.
3645
+ */
3646
+ _skeletonSource() {
3647
+ const parent = this.parentElement;
3648
+ if (parent instanceof ModelElement) {
3649
+ return parent;
3650
+ }
3651
+ const models = parent ? parent.querySelectorAll(':scope > pc-model') : null;
3652
+ return models?.length === 1 && models[0] instanceof ModelElement ? models[0] : null;
3653
+ }
3654
+ /**
3655
+ * Keeps the component's binding root pointing at the skeleton source's host entity. The host
3656
+ * wraps the instantiated content, so left at its default — the component's own entity — the
3657
+ * engine binder mis-resolves curves that target the asset's root node: its fallback treats
3658
+ * the graph as the asset root once the root is no longer a direct child.
3659
+ *
3660
+ * Authoritative in both directions for values this element assigned: a source appearing pins
3661
+ * its host, and a source dissolving (the model gone, or a second model making the skeleton
3662
+ * ambiguous) clears the pin rather than leaving it on a stale host. A root assigned through
3663
+ * the engine API is never overwritten — the user's choice outranks the managed default.
3664
+ * Writes are skipped while unchanged, because the engine setter itself triggers a rebind.
3665
+ */
3666
+ _applyRootBone() {
3667
+ const component = this.component;
3668
+ // A non-null root this element did not assign came through the engine API. A fresh
3669
+ // component starts at null, which is always reclaimable.
3670
+ if (component.rootBone !== null && component.rootBone !== this._managedRootBone) {
3671
+ return;
3672
+ }
3673
+ const host = this._skeletonSource()?.entity ?? null;
3674
+ if (component.rootBone !== host) {
3675
+ // The engine setter accepts null - restoring the component's own entity as the
3676
+ // binding graph - but its declared type does not
3677
+ component.rootBone = host;
3678
+ }
3679
+ this._managedRootBone = host;
3680
+ }
3505
3681
  /** @ignore */
3506
3682
  constructor() {
3507
3683
  super('anim');
@@ -3526,7 +3702,16 @@ class AnimComponentElement extends ComponentElement {
3526
3702
  host.addEventListener('ready', this._onModelReady);
3527
3703
  this._modelListenerTarget = host;
3528
3704
  }
3529
- this._applyClips();
3705
+ this._applyRootBone();
3706
+ if (this.component.baseLayer) {
3707
+ // The component survived the host's readiness cycle (a pc-model reloading content on
3708
+ // its stable host entity). A loaded graph cannot be reassigned in place, so drop it
3709
+ // and reassign from the current source, restoring the active clip and playhead.
3710
+ this._refreshClips();
3711
+ }
3712
+ else {
3713
+ this._applyClips();
3714
+ }
3530
3715
  }
3531
3716
  disconnectedCallback() {
3532
3717
  this._modelListenerTarget?.removeEventListener('ready', this._onModelReady);
@@ -3536,6 +3721,7 @@ class AnimComponentElement extends ComponentElement {
3536
3721
  this._sourceGeneration++;
3537
3722
  this._assignedClips.clear();
3538
3723
  this._autoAssigned = false;
3724
+ this._managedRootBone = null;
3539
3725
  super.disconnectedCallback();
3540
3726
  }
3541
3727
  /**
@@ -3704,6 +3890,10 @@ class AnimComponentElement extends ComponentElement {
3704
3890
  if (!component) {
3705
3891
  return;
3706
3892
  }
3893
+ // A clip-set change is also a chance for the skeleton source to have changed shape (a
3894
+ // clip child appearing or leaving can accompany a model coming or going) - re-derive the
3895
+ // binding root before the reassignment binds against it.
3896
+ this._applyRootBone();
3707
3897
  const layer = component.baseLayer;
3708
3898
  const restore = layer ? {
3709
3899
  state: layer.activeState,
@@ -4311,14 +4501,16 @@ class AnimClipElement extends AsyncElement {
4311
4501
  customElements.define('pc-anim-clip', AnimClipElement);
4312
4502
 
4313
4503
  /**
4314
- * The ListenerComponentElement interface provides properties and methods for manipulating
4315
- * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-listener/ | `<pc-listener>`} elements.
4316
- * The ListenerComponentElement interface also inherits the properties and methods of the
4504
+ * The AudioListenerComponentElement interface provides properties and methods for manipulating
4505
+ * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-audio-listener/ | `<pc-audio-listener>`} elements.
4506
+ * The AudioListenerComponentElement interface also inherits the properties and methods of the
4317
4507
  * {@link HTMLElement} interface.
4318
4508
  *
4509
+ * Engine component: {@link AudioListenerComponent} (`audiolistener`).
4510
+ *
4319
4511
  * @category Components
4320
4512
  */
4321
- class ListenerComponentElement extends ComponentElement {
4513
+ class AudioListenerComponentElement extends ComponentElement {
4322
4514
  /** @ignore */
4323
4515
  constructor() {
4324
4516
  super('audiolistener');
@@ -4331,7 +4523,7 @@ class ListenerComponentElement extends ComponentElement {
4331
4523
  return super.component;
4332
4524
  }
4333
4525
  }
4334
- customElements.define('pc-listener', ListenerComponentElement);
4526
+ customElements.define('pc-audio-listener', AudioListenerComponentElement);
4335
4527
 
4336
4528
  const transitionModes = new Map([
4337
4529
  ['tint', BUTTON_TRANSITION_MODE_TINT],
@@ -4343,6 +4535,8 @@ const transitionModes = new Map([
4343
4535
  * The ButtonComponentElement interface also inherits the properties and methods of the
4344
4536
  * {@link HTMLElement} interface.
4345
4537
  *
4538
+ * Engine component: {@link ButtonComponent} (`button`).
4539
+ *
4346
4540
  * @category Components
4347
4541
  */
4348
4542
  class ButtonComponentElement extends ComponentElement {
@@ -4423,7 +4617,9 @@ class ButtonComponentElement extends ComponentElement {
4423
4617
  }
4424
4618
  /**
4425
4619
  * Sets the reference (CSS selector, element id or entity name) to the `<pc-entity>` whose image
4426
- * element is used for visual transitions. Defaults to the button's own entity.
4620
+ * element is used for visual transitions. Defaults to the button's own entity — inside a
4621
+ * `<pc-model>`, that is the model's host entity, so supply an explicit reference to target a
4622
+ * UI entity instead.
4427
4623
  * @param value - The image entity reference.
4428
4624
  */
4429
4625
  set image(value) {
@@ -4742,6 +4938,8 @@ const tonemaps = new Map([
4742
4938
  * The CameraComponentElement interface also inherits the properties and methods of the
4743
4939
  * {@link HTMLElement} interface.
4744
4940
  *
4941
+ * Engine component: {@link CameraComponent} (`camera`).
4942
+ *
4745
4943
  * @category Components
4746
4944
  */
4747
4945
  class CameraComponentElement extends ComponentElement {
@@ -5227,6 +5425,8 @@ customElements.define('pc-camera', CameraComponentElement);
5227
5425
  * retargets or rebinds picks up the new node's geometry. An entity with no asset-backed render
5228
5426
  * component warns, and the collider has no shape.
5229
5427
  *
5428
+ * Engine component: {@link CollisionComponent} (`collision`).
5429
+ *
5230
5430
  * @category Components
5231
5431
  */
5232
5432
  class CollisionComponentElement extends ComponentElement {
@@ -5263,7 +5463,9 @@ class CollisionComponentElement extends ComponentElement {
5263
5463
  * no attribute to supply it - so the host's visible geometry, the meaning a mesh collider
5264
5464
  * on a glTF node carries, fills the gap. Runs on every application (so a rebound `pc-node`
5265
5465
  * recomputes it) and on a runtime switch to `type="mesh"`; an explicitly assigned
5266
- * `renderAsset` is never overwritten.
5466
+ * `renderAsset` is never overwritten. A `pc-model` host entity never carries a render
5467
+ * component (the instantiated content beneath it does), so a mesh collider that should take
5468
+ * an asset's geometry belongs on a bound `pc-node`.
5267
5469
  */
5268
5470
  _applyMeshGeometryDefault() {
5269
5471
  const component = this.component;
@@ -5408,6 +5610,13 @@ customElements.define('pc-collision', CollisionComponentElement);
5408
5610
  * The ElementComponentElement interface also inherits the properties and methods of the
5409
5611
  * {@link HTMLElement} interface.
5410
5612
  *
5613
+ * Despite the name, this is not a base class or a generic wrapper: it is the engine's 2D UI
5614
+ * component, which gives its host entity a rectangle in a `<pc-screen>` hierarchy that draws
5615
+ * either an image, a line of text or nothing (`type="image"`, `"text"` or `"group"`). The tag
5616
+ * spells the engine component it adds, as every component element does.
5617
+ *
5618
+ * Engine component: {@link ElementComponent} (`element`).
5619
+ *
5411
5620
  * @category Components
5412
5621
  */
5413
5622
  class ElementComponentElement extends ComponentElement {
@@ -6096,6 +6305,8 @@ customElements.define('pc-element', ElementComponentElement);
6096
6305
  * constraint breaks. A broken joint no longer constrains its bodies; calling `refreshFrames()` on
6097
6306
  * the underlying component re-attaches it. Bubbles and is composed.
6098
6307
  *
6308
+ * Engine component: {@link JointComponent} (`joint`).
6309
+ *
6099
6310
  * @category Components
6100
6311
  */
6101
6312
  class JointComponentElement extends ComponentElement {
@@ -6954,10 +7165,12 @@ customElements.define('pc-joint', JointComponentElement);
6954
7165
 
6955
7166
  /**
6956
7167
  * The LayoutChildComponentElement interface provides properties and methods for manipulating
6957
- * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-layoutchild/ | `<pc-layoutchild>`} elements.
7168
+ * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-layout-child/ | `<pc-layout-child>`} elements.
6958
7169
  * The LayoutChildComponentElement interface also inherits the properties and methods of the
6959
7170
  * {@link HTMLElement} interface.
6960
7171
  *
7172
+ * Engine component: {@link LayoutChildComponent} (`layoutchild`).
7173
+ *
6961
7174
  * @category Components
6962
7175
  */
6963
7176
  class LayoutChildComponentElement extends ComponentElement {
@@ -7150,7 +7363,7 @@ class LayoutChildComponentElement extends ComponentElement {
7150
7363
  }
7151
7364
  }
7152
7365
  }
7153
- customElements.define('pc-layoutchild', LayoutChildComponentElement);
7366
+ customElements.define('pc-layout-child', LayoutChildComponentElement);
7154
7367
 
7155
7368
  const orientations$1 = new Map([
7156
7369
  ['horizontal', ORIENTATION_HORIZONTAL],
@@ -7164,10 +7377,12 @@ const fittings = new Map([
7164
7377
  ]);
7165
7378
  /**
7166
7379
  * The LayoutGroupComponentElement interface provides properties and methods for manipulating
7167
- * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-layoutgroup/ | `<pc-layoutgroup>`} elements.
7380
+ * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-layout-group/ | `<pc-layout-group>`} elements.
7168
7381
  * The LayoutGroupComponentElement interface also inherits the properties and methods of the
7169
7382
  * {@link HTMLElement} interface.
7170
7383
  *
7384
+ * Engine component: {@link LayoutGroupComponent} (`layoutgroup`).
7385
+ *
7171
7386
  * @category Components
7172
7387
  */
7173
7388
  class LayoutGroupComponentElement extends ComponentElement {
@@ -7407,7 +7622,7 @@ class LayoutGroupComponentElement extends ComponentElement {
7407
7622
  }
7408
7623
  }
7409
7624
  }
7410
- customElements.define('pc-layoutgroup', LayoutGroupComponentElement);
7625
+ customElements.define('pc-layout-group', LayoutGroupComponentElement);
7411
7626
 
7412
7627
  const shadowTypes = new Map([
7413
7628
  ['pcf1-16f', SHADOW_PCF1_16F],
@@ -7426,6 +7641,8 @@ const shadowTypes = new Map([
7426
7641
  * The LightComponentElement interface also inherits the properties and methods of the
7427
7642
  * {@link HTMLElement} interface.
7428
7643
  *
7644
+ * Engine component: {@link LightComponent} (`light`).
7645
+ *
7429
7646
  * @category Components
7430
7647
  */
7431
7648
  class LightComponentElement extends ComponentElement {
@@ -7907,10 +8124,12 @@ customElements.define('pc-light', LightComponentElement);
7907
8124
 
7908
8125
  /**
7909
8126
  * The ParticleSystemComponentElement interface provides properties and methods for manipulating
7910
- * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-particles/ | `<pc-particles>`} elements.
8127
+ * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-particle-system/ | `<pc-particle-system>`} elements.
7911
8128
  * The ParticleSystemComponentElement interface also inherits the properties and methods of the
7912
8129
  * {@link HTMLElement} interface.
7913
8130
  *
8131
+ * Engine component: {@link ParticleSystemComponent} (`particlesystem`).
8132
+ *
7914
8133
  * @category Components
7915
8134
  */
7916
8135
  class ParticleSystemComponentElement extends ComponentElement {
@@ -8029,7 +8248,7 @@ class ParticleSystemComponentElement extends ComponentElement {
8029
8248
  }
8030
8249
  }
8031
8250
  }
8032
- customElements.define('pc-particles', ParticleSystemComponentElement);
8251
+ customElements.define('pc-particle-system', ParticleSystemComponentElement);
8033
8252
 
8034
8253
  const blendTypes = new Map([
8035
8254
  ['none', BLEND_NONE],
@@ -10261,6 +10480,8 @@ customElements.define('pc-material', MaterialElement);
10261
10480
  * to `box`). It does not cover the engine's `asset` render type, since there is no way to supply
10262
10481
  * a render asset here — use `pc-model` for glTF content instead.
10263
10482
  *
10483
+ * Engine component: {@link RenderComponent} (`render`).
10484
+ *
10264
10485
  * @category Components
10265
10486
  */
10266
10487
  class RenderComponentElement extends ComponentElement {
@@ -10385,10 +10606,12 @@ customElements.define('pc-render', RenderComponentElement);
10385
10606
 
10386
10607
  /**
10387
10608
  * The RigidBodyComponentElement interface provides properties and methods for manipulating
10388
- * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-rigidbody/ | `<pc-rigidbody>`} elements.
10609
+ * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-rigid-body/ | `<pc-rigid-body>`} elements.
10389
10610
  * The RigidBodyComponentElement interface also inherits the properties and methods of the
10390
10611
  * {@link HTMLElement} interface.
10391
10612
  *
10613
+ * Engine component: {@link RigidBodyComponent} (`rigidbody`).
10614
+ *
10392
10615
  * @category Components
10393
10616
  */
10394
10617
  class RigidBodyComponentElement extends ComponentElement {
@@ -10580,7 +10803,7 @@ class RigidBodyComponentElement extends ComponentElement {
10580
10803
  }
10581
10804
  }
10582
10805
  }
10583
- customElements.define('pc-rigidbody', RigidBodyComponentElement);
10806
+ customElements.define('pc-rigid-body', RigidBodyComponentElement);
10584
10807
 
10585
10808
  // The engine's SCALEMODE_* constants are the strings 'none' and 'blend', so this map happens to be
10586
10809
  // an identity. It is still the right shape: it supplies parseEnum's valid-name list, it is what the
@@ -10596,6 +10819,8 @@ const scaleModes = new Map([
10596
10819
  * The ScreenComponentElement interface also inherits the properties and methods of the
10597
10820
  * {@link HTMLElement} interface.
10598
10821
  *
10822
+ * Engine component: {@link ScreenComponent} (`screen`).
10823
+ *
10599
10824
  * @category Components
10600
10825
  */
10601
10826
  class ScreenComponentElement extends ComponentElement {
@@ -10748,6 +10973,8 @@ const orientations = new Map([
10748
10973
  * The ScrollbarComponentElement interface also inherits the properties and methods of the
10749
10974
  * {@link HTMLElement} interface.
10750
10975
  *
10976
+ * Engine component: {@link ScrollbarComponent} (`scrollbar`).
10977
+ *
10751
10978
  * @category Components
10752
10979
  */
10753
10980
  class ScrollbarComponentElement extends ComponentElement {
@@ -10883,10 +11110,12 @@ const visibilities = new Map([
10883
11110
  ]);
10884
11111
  /**
10885
11112
  * The ScrollViewComponentElement interface provides properties and methods for manipulating
10886
- * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-scrollview/ | `<pc-scrollview>`} elements.
11113
+ * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-scroll-view/ | `<pc-scroll-view>`} elements.
10887
11114
  * The ScrollViewComponentElement interface also inherits the properties and methods of the
10888
11115
  * {@link HTMLElement} interface.
10889
11116
  *
11117
+ * Engine component: {@link ScrollViewComponent} (`scrollview`).
11118
+ *
10890
11119
  * @category Components
10891
11120
  */
10892
11121
  class ScrollViewComponentElement extends ComponentElement {
@@ -11245,11 +11474,11 @@ class ScrollViewComponentElement extends ComponentElement {
11245
11474
  }
11246
11475
  }
11247
11476
  }
11248
- customElements.define('pc-scrollview', ScrollViewComponentElement);
11477
+ customElements.define('pc-scroll-view', ScrollViewComponentElement);
11249
11478
 
11250
11479
  /**
11251
- * The ScriptElement interface provides properties and methods for manipulating
11252
- * `<pc-script>` elements. The ScriptElement interface also inherits the properties and
11480
+ * The ScriptInstanceElement interface provides properties and methods for manipulating
11481
+ * `<pc-script-instance>` elements. The ScriptInstanceElement interface also inherits the properties and
11253
11482
  * methods of the {@link AsyncElement} interface.
11254
11483
  *
11255
11484
  * Script attributes can be supplied through two channels:
@@ -11271,7 +11500,7 @@ customElements.define('pc-scrollview', ScrollViewComponentElement);
11271
11500
  * new-name one, re-applying both attribute channels to it.
11272
11501
  *
11273
11502
  * The element becomes ready once its script instance has been created by the parent
11274
- * `<pc-scripts>` element.
11503
+ * `<pc-script>` element.
11275
11504
  *
11276
11505
  * @fires {CustomEvent} scriptattributeschange - Fired when the script's attributes change. The
11277
11506
  * `detail` carries the new `attributes` object. Bubbles.
@@ -11280,11 +11509,11 @@ customElements.define('pc-scrollview', ScrollViewComponentElement);
11280
11509
  * @fires {CustomEvent} scriptnamechange - Fired when the script is renamed on a live element. The
11281
11510
  * `detail` carries `oldName` and `newName`. Bubbles.
11282
11511
  */
11283
- class ScriptElement extends AsyncElement {
11512
+ class ScriptInstanceElement extends AsyncElement {
11284
11513
  _attributes = {};
11285
11514
  _enabled = true;
11286
11515
  /**
11287
- * The Script instance created for this element by its parent `<pc-scripts>` element.
11516
+ * The Script instance created for this element by its parent `<pc-script>` element.
11288
11517
  * @internal
11289
11518
  */
11290
11519
  _script = null;
@@ -11330,11 +11559,11 @@ class ScriptElement extends AsyncElement {
11330
11559
  }
11331
11560
  /**
11332
11561
  * Sets the name of the script to create. The `name` attribute is the single source of truth
11333
- * (it is what the parent `<pc-scripts>` element reads when creating the instance), so the
11562
+ * (it is what the parent `<pc-script>` element reads when creating the instance), so the
11334
11563
  * property writes through to it — assigning before insertion works as expected:
11335
11564
  *
11336
11565
  * ```js
11337
- * const script = document.createElement('pc-script');
11566
+ * const script = document.createElement('pc-script-instance');
11338
11567
  * script.name = 'rotate';
11339
11568
  * scriptsElement.appendChild(script);
11340
11569
  * await script.ready();
@@ -11361,10 +11590,10 @@ class ScriptElement extends AsyncElement {
11361
11590
  return this._script;
11362
11591
  }
11363
11592
  connectedCallback() {
11364
- // Script instances are created by the parent pc-scripts element, so an element placed
11593
+ // Script instances are created by the parent pc-script element, so an element placed
11365
11594
  // anywhere else is inert and never becomes ready - warn rather than hang silently
11366
- if (this.parentElement?.tagName !== 'PC-SCRIPTS') {
11367
- console.warn(`pc-script '${this.getAttribute('name')}' must be a direct child of pc-scripts - script not created`);
11595
+ if (this.parentElement?.tagName !== 'PC-SCRIPT') {
11596
+ console.warn(`pc-script-instance '${this.getAttribute('name')}' must be a direct child of pc-script - script not created`);
11368
11597
  }
11369
11598
  }
11370
11599
  disconnectedCallback() {
@@ -11375,7 +11604,7 @@ class ScriptElement extends AsyncElement {
11375
11604
  this._resetReady();
11376
11605
  }
11377
11606
  /**
11378
- * Called by the parent `<pc-scripts>` element when the script instance has been created.
11607
+ * Called by the parent `<pc-script>` element when the script instance has been created.
11379
11608
  * Creation can happen more than once per connection (a runtime `name` change recreates the
11380
11609
  * instance), but `_onReady` signals readiness at most once per cycle.
11381
11610
  * @internal
@@ -11397,7 +11626,7 @@ class ScriptElement extends AsyncElement {
11397
11626
  this.scriptAttributes = JSON.parse(newValue);
11398
11627
  }
11399
11628
  catch (error) {
11400
- console.warn(`Invalid 'attributes' JSON on pc-script '${this.getAttribute('name')}': ${error.message}`);
11629
+ console.warn(`Invalid 'attributes' JSON on pc-script-instance '${this.getAttribute('name')}': ${error.message}`);
11401
11630
  }
11402
11631
  break;
11403
11632
  case 'enabled':
@@ -11417,14 +11646,14 @@ class ScriptElement extends AsyncElement {
11417
11646
  }
11418
11647
  }
11419
11648
  }
11420
- customElements.define('pc-script', ScriptElement);
11649
+ customElements.define('pc-script-instance', ScriptInstanceElement);
11421
11650
 
11422
11651
  /**
11423
- * Attributes on `pc-script` that never map to script attributes: the element's own API (derived
11652
+ * Attributes on `pc-script-instance` that never map to script attributes: the element's own API (derived
11424
11653
  * from its observed attributes) plus reserved and global HTML attribute names.
11425
11654
  */
11426
11655
  const RESERVED_ATTRIBUTES = new Set([
11427
- ...ScriptElement.observedAttributes,
11656
+ ...ScriptInstanceElement.observedAttributes,
11428
11657
  'accesskey',
11429
11658
  'autocapitalize',
11430
11659
  'autofocus',
@@ -11455,7 +11684,7 @@ const RESERVED_ATTRIBUTES = new Set([
11455
11684
  'translate'
11456
11685
  ]);
11457
11686
  /**
11458
- * Checks whether a `pc-script` attribute name is reserved (and so never maps to a script
11687
+ * Checks whether a `pc-script-instance` attribute name is reserved (and so never maps to a script
11459
11688
  * attribute). Reserved names are the element's own API, global HTML attribute names, `data-*`
11460
11689
  * and `aria-*` attributes, names starting with `_` (framework-stamped attributes), and real
11461
11690
  * inline event handler names (`onclick` etc. — detected via the platform, so script attributes
@@ -11611,10 +11840,12 @@ const findCaseMatch = (script, key) => {
11611
11840
  };
11612
11841
  /**
11613
11842
  * The ScriptComponentElement interface provides properties and methods for manipulating
11614
- * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-scripts/ | `<pc-scripts>`} elements.
11843
+ * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-script/ | `<pc-script>`} elements.
11615
11844
  * The ScriptComponentElement interface also inherits the properties and methods of the
11616
11845
  * {@link HTMLElement} interface.
11617
11846
  *
11847
+ * Engine component: {@link ScriptComponent} (`script`).
11848
+ *
11618
11849
  * @category Components
11619
11850
  */
11620
11851
  class ScriptComponentElement extends ComponentElement {
@@ -11631,14 +11862,23 @@ class ScriptComponentElement extends ComponentElement {
11631
11862
  }
11632
11863
  connectedCallback() {
11633
11864
  // (Re-)observe on every connection - disconnectedCallback disconnects the observer.
11634
- // Attribute changes on child pc-script elements are watched here too: per-property
11865
+ // Attribute changes on child pc-script-instance elements are watched here too: per-property
11635
11866
  // script attributes are not statically known, so they cannot use observedAttributes.
11636
11867
  this.observer.observe(this, { childList: true, subtree: true, attributes: true });
11637
11868
  return super.connectedCallback();
11638
11869
  }
11639
11870
  initComponent() {
11640
- // Handle initial script elements
11641
- this.querySelectorAll(':scope > pc-script').forEach((scriptElement) => {
11871
+ this.querySelectorAll(':scope > pc-script-instance').forEach((scriptElement) => {
11872
+ // A host readiness cycle re-runs this against a component that can have survived it
11873
+ // (a pc-model reloading content on its stable host entity). The engine rejects a
11874
+ // duplicate create - returning null, silently in production builds - which would
11875
+ // skip attribute application entirely. A surviving instance is re-asserted instead,
11876
+ // so both cycle outcomes leave the component reflecting the element's declared state.
11877
+ const script = this.scriptFor(scriptElement);
11878
+ if (script) {
11879
+ this.applyDeclaredState(script, scriptElement);
11880
+ return;
11881
+ }
11642
11882
  this.createScript(scriptElement);
11643
11883
  });
11644
11884
  }
@@ -11772,9 +12012,9 @@ class ScriptComponentElement extends ComponentElement {
11772
12012
  this.mergeDeep(script, converted);
11773
12013
  }
11774
12014
  /**
11775
- * Returns the camelCase keys of the per-property attributes present on a `pc-script`
12015
+ * Returns the camelCase keys of the per-property attributes present on a `pc-script-instance`
11776
12016
  * element.
11777
- * @param scriptElement - The `pc-script` element.
12017
+ * @param scriptElement - The `pc-script-instance` element.
11778
12018
  * @returns The camelCase keys.
11779
12019
  */
11780
12020
  inlineKeys(scriptElement) {
@@ -11787,10 +12027,10 @@ class ScriptComponentElement extends ComponentElement {
11787
12027
  return keys;
11788
12028
  }
11789
12029
  /**
11790
- * Resolves the script instance owned by a `pc-script` element. Returns `null` when the
12030
+ * Resolves the script instance owned by a `pc-script-instance` element. Returns `null` when the
11791
12031
  * element has no created script, or when its name resolves to a script created by a
11792
12032
  * different element (e.g. a duplicate-named sibling).
11793
- * @param scriptElement - The `pc-script` element.
12033
+ * @param scriptElement - The `pc-script-instance` element.
11794
12034
  * @returns The owned script, or `null`.
11795
12035
  */
11796
12036
  scriptFor(scriptElement) {
@@ -11819,7 +12059,7 @@ class ScriptComponentElement extends ComponentElement {
11819
12059
  }
11820
12060
  }
11821
12061
  /**
11822
- * Handles a runtime `name` change on a child `pc-script`, swapping the engine script instance
12062
+ * Handles a runtime `name` change on a child `pc-script-instance`, swapping the engine script instance
11823
12063
  * to match. Without this the element would keep pointing at the old-name instance: the old
11824
12064
  * script would go on running while every subsequent update (attribute changes, enable
11825
12065
  * changes, destruction on removal) resolved the new name and silently no-opped.
@@ -11830,8 +12070,8 @@ class ScriptComponentElement extends ComponentElement {
11830
12070
  */
11831
12071
  handleScriptNameChange(event) {
11832
12072
  const scriptElement = event.target;
11833
- // Only direct children are managed, matching initComponent's ':scope > pc-script'
11834
- // contract - the event bubbles, so a deeper pc-script must not be created here
12073
+ // Only direct children are managed, matching initComponent's ':scope > pc-script-instance'
12074
+ // contract - the event bubbles, so a deeper pc-script-instance must not be created here
11835
12075
  if (scriptElement.parentElement !== this)
11836
12076
  return;
11837
12077
  // Before the component exists there is nothing to swap: initComponent creates from
@@ -11848,11 +12088,11 @@ class ScriptComponentElement extends ComponentElement {
11848
12088
  this.createScript(scriptElement);
11849
12089
  }
11850
12090
  /**
11851
- * Creates the script instance for a `pc-script` element. The instance is created disabled,
12091
+ * Creates the script instance for a `pc-script-instance` element. The instance is created disabled,
11852
12092
  * the element's converted attributes are merged over the instance's defaults (which is what
11853
12093
  * allows plain numeric arrays to be typed against those defaults), and only then is the
11854
12094
  * declared enabled state applied — so `initialize()` runs with every attribute in place.
11855
- * @param scriptElement - The `pc-script` element to create the script instance for.
12095
+ * @param scriptElement - The `pc-script-instance` element to create the script instance for.
11856
12096
  * @returns The created script, or `null`.
11857
12097
  */
11858
12098
  createScript(scriptElement) {
@@ -11863,21 +12103,30 @@ class ScriptComponentElement extends ComponentElement {
11863
12103
  if (!script)
11864
12104
  return null;
11865
12105
  scriptElement._script = script;
11866
- // The JSON blob first with per-property-shadowed keys stripped, then the per-property
11867
- // attributes: each property is written exactly once and individual attributes win
12106
+ this.applyDeclaredState(script, scriptElement);
12107
+ scriptElement._onScriptCreated();
12108
+ return script;
12109
+ }
12110
+ /**
12111
+ * Applies a `pc-script-instance` element's declared state to a script instance: the `attributes` JSON
12112
+ * first with per-property-shadowed keys stripped, then the per-property attributes — each
12113
+ * property is written exactly once and individual attributes win — and finally the declared
12114
+ * enabled state, so `initialize()` runs with every attribute in place.
12115
+ * @param script - The script instance.
12116
+ * @param scriptElement - The `pc-script-instance` element holding the declared state.
12117
+ */
12118
+ applyDeclaredState(script, scriptElement) {
11868
12119
  this.applyAttributes(script, scriptElement.scriptAttributes, this.inlineKeys(scriptElement));
11869
12120
  this.applyInlineAttributes(script, scriptElement);
11870
12121
  script.enabled = scriptElement.enabled;
11871
- scriptElement._onScriptCreated();
11872
- return script;
11873
12122
  }
11874
12123
  /**
11875
- * Applies the per-property attributes present on a `pc-script` element — any attribute that
12124
+ * Applies the per-property attributes present on a `pc-script-instance` element — any attribute that
11876
12125
  * is not part of the element's own API or a reserved HTML attribute name. These are applied
11877
12126
  * after the `attributes` JSON, so an individual attribute always takes precedence over the
11878
12127
  * blob.
11879
12128
  * @param script - The script to apply the attributes to.
11880
- * @param scriptElement - The `pc-script` element holding the attributes.
12129
+ * @param scriptElement - The `pc-script-instance` element holding the attributes.
11881
12130
  */
11882
12131
  applyInlineAttributes(script, scriptElement) {
11883
12132
  const scriptName = scriptElement.getAttribute('name') ?? '';
@@ -11888,10 +12137,10 @@ class ScriptComponentElement extends ComponentElement {
11888
12137
  }
11889
12138
  }
11890
12139
  /**
11891
- * Applies a single per-property attribute change to the script of a `pc-script` element.
12140
+ * Applies a single per-property attribute change to the script of a `pc-script-instance` element.
11892
12141
  * When the attribute has been removed, the value from the `attributes` JSON (if any) takes
11893
12142
  * effect again.
11894
- * @param scriptElement - The `pc-script` element whose attribute changed.
12143
+ * @param scriptElement - The `pc-script-instance` element whose attribute changed.
11895
12144
  * @param attributeName - The name of the changed attribute.
11896
12145
  */
11897
12146
  applyScriptProperty(scriptElement, attributeName) {
@@ -11927,7 +12176,7 @@ class ScriptComponentElement extends ComponentElement {
11927
12176
  try {
11928
12177
  const current = script[key];
11929
12178
  if (typeof current === 'function' || SCRIPT_API_MEMBERS.has(key)) {
11930
- console.warn(`Ignoring attribute '${attributeName}' on pc-script '${scriptName}' - '${key}' is part of the Script API.`);
12179
+ console.warn(`Ignoring attribute '${attributeName}' on pc-script-instance '${scriptName}' - '${key}' is part of the Script API.`);
11931
12180
  return;
11932
12181
  }
11933
12182
  if (typeof current === 'string') {
@@ -11983,10 +12232,10 @@ class ScriptComponentElement extends ComponentElement {
11983
12232
  }
11984
12233
  handleMutations(mutations) {
11985
12234
  for (const mutation of mutations) {
11986
- // Handle per-property attribute changes on child pc-script elements
12235
+ // Handle per-property attribute changes on child pc-script-instance elements
11987
12236
  if (mutation.type === 'attributes') {
11988
12237
  const target = mutation.target;
11989
- if (target instanceof ScriptElement &&
12238
+ if (target instanceof ScriptInstanceElement &&
11990
12239
  target.parentElement === this &&
11991
12240
  mutation.attributeName &&
11992
12241
  !isReservedAttribute(mutation.attributeName)) {
@@ -11996,16 +12245,16 @@ class ScriptComponentElement extends ComponentElement {
11996
12245
  }
11997
12246
  // Only direct children are managed - the observer watches the subtree for attribute
11998
12247
  // changes, but deeper childList records must not create or destroy scripts
11999
- // (matching initComponent's ':scope > pc-script' contract)
12248
+ // (matching initComponent's ':scope > pc-script-instance' contract)
12000
12249
  if (mutation.target !== this) {
12001
12250
  continue;
12002
12251
  }
12003
- // Handle removed nodes first, so that replacing a pc-script with a same-named one
12252
+ // Handle removed nodes first, so that replacing a pc-script-instance with a same-named one
12004
12253
  // destroys the old script before the replacement is created. Only destroy a script
12005
12254
  // this element actually owns - a duplicate-named element whose own create() failed
12006
12255
  // must not take down the live script on removal.
12007
12256
  mutation.removedNodes.forEach((node) => {
12008
- if (node instanceof ScriptElement) {
12257
+ if (node instanceof ScriptInstanceElement) {
12009
12258
  const scriptName = node.getAttribute('name');
12010
12259
  if (scriptName &&
12011
12260
  node._script &&
@@ -12018,7 +12267,7 @@ class ScriptComponentElement extends ComponentElement {
12018
12267
  });
12019
12268
  // Handle added nodes
12020
12269
  mutation.addedNodes.forEach((node) => {
12021
- if (node instanceof ScriptElement) {
12270
+ if (node instanceof ScriptInstanceElement) {
12022
12271
  this.createScript(node);
12023
12272
  }
12024
12273
  });
@@ -12036,14 +12285,16 @@ class ScriptComponentElement extends ComponentElement {
12036
12285
  return super.component;
12037
12286
  }
12038
12287
  }
12039
- customElements.define('pc-scripts', ScriptComponentElement);
12288
+ customElements.define('pc-script', ScriptComponentElement);
12040
12289
 
12041
12290
  /**
12042
12291
  * The SoundComponentElement interface provides properties and methods for manipulating
12043
- * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-sounds/ | `<pc-sounds>`} elements.
12292
+ * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-sound/ | `<pc-sound>`} elements.
12044
12293
  * The SoundComponentElement interface also inherits the properties and methods of the
12045
12294
  * {@link HTMLElement} interface.
12046
12295
  *
12296
+ * Engine component: {@link SoundComponent} (`sound`).
12297
+ *
12047
12298
  * @category Components
12048
12299
  */
12049
12300
  class SoundComponentElement extends ComponentElement {
@@ -12234,11 +12485,11 @@ class SoundComponentElement extends ComponentElement {
12234
12485
  }
12235
12486
  }
12236
12487
  }
12237
- customElements.define('pc-sounds', SoundComponentElement);
12488
+ customElements.define('pc-sound', SoundComponentElement);
12238
12489
 
12239
12490
  /**
12240
12491
  * The SoundSlotElement interface provides properties and methods for manipulating
12241
- * `<pc-sound>` elements. The SoundSlotElement interface also inherits the properties and
12492
+ * `<pc-sound-slot>` elements. The SoundSlotElement interface also inherits the properties and
12242
12493
  * methods of the {@link AsyncElement} interface.
12243
12494
  */
12244
12495
  class SoundSlotElement extends AsyncElement {
@@ -12252,7 +12503,7 @@ class SoundSlotElement extends AsyncElement {
12252
12503
  _startTime = 0;
12253
12504
  _volume = 1;
12254
12505
  /**
12255
- * The `<pc-sounds>` this slot was added to, captured at connect time.
12506
+ * The `<pc-sound>` this slot was added to, captured at connect time.
12256
12507
  *
12257
12508
  * `disconnectedCallback` cannot rediscover it: by the time the element is disconnected its
12258
12509
  * `parentElement` is already `null`, so a lookup would both fail to find the component and
@@ -12315,7 +12566,7 @@ class SoundSlotElement extends AsyncElement {
12315
12566
  get soundElement() {
12316
12567
  const soundElement = this.parentElement;
12317
12568
  if (!(soundElement instanceof SoundComponentElement)) {
12318
- console.warn('pc-sound must be a direct child of a pc-sounds element');
12569
+ console.warn('pc-sound-slot must be a direct child of a pc-sound element');
12319
12570
  return null;
12320
12571
  }
12321
12572
  return soundElement;
@@ -12511,7 +12762,7 @@ class SoundSlotElement extends AsyncElement {
12511
12762
  }
12512
12763
  }
12513
12764
  }
12514
- customElements.define('pc-sound', SoundSlotElement);
12765
+ customElements.define('pc-sound-slot', SoundSlotElement);
12515
12766
 
12516
12767
  /**
12517
12768
  * The GSplatComponentElement interface provides properties and methods for manipulating
@@ -12519,6 +12770,8 @@ customElements.define('pc-sound', SoundSlotElement);
12519
12770
  * The GSplatComponentElement interface also inherits the properties and methods of the
12520
12771
  * {@link HTMLElement} interface.
12521
12772
  *
12773
+ * Engine component: {@link GSplatComponent} (`gsplat`).
12774
+ *
12522
12775
  * @category Components
12523
12776
  */
12524
12777
  class GSplatComponentElement extends ComponentElement {
@@ -12940,7 +13193,10 @@ class NodeElement extends EntityBaseElement {
12940
13193
  * is retained untouched — a redundant edit must not flicker overrides through a revert.
12941
13194
  */
12942
13195
  _rebind() {
12943
- const hostEntity = this._host?.entity ?? null;
13196
+ // A model fronts a host entity of its own; the names this element resolves are the
13197
+ // asset's, so the search starts at the instantiated content root, not the wrapper.
13198
+ const host = this._host;
13199
+ const hostEntity = (host instanceof ModelElement ? host.contentEntity : host?.entity) ?? null;
12944
13200
  if (!hostEntity || !this._name) {
12945
13201
  // Host not instantiated (or nothing to look up yet): return to pending. An assigned
12946
13202
  // name arriving later, or the host's next cycle, resolves it.
@@ -13037,8 +13293,9 @@ class NodeElement extends EntityBaseElement {
13037
13293
  }
13038
13294
  this._revertOverrides();
13039
13295
  // Attachment points anchor to the bound node, so they cannot outlive the binding. Each
13040
- // destroyed entity resets its element, which the next _buildChildren re-creates.
13041
- this.querySelectorAll('pc-entity').forEach((child) => {
13296
+ // destroyed entity resets its element, which the next _buildChildren re-creates - a
13297
+ // model host among them re-instantiates its content when it rebuilds.
13298
+ this.querySelectorAll('pc-entity, pc-model').forEach((child) => {
13042
13299
  if (child.closestEntity === this) {
13043
13300
  child.entity?.destroy();
13044
13301
  }
@@ -14064,5 +14321,5 @@ class SkyElement extends AsyncElement {
14064
14321
  }
14065
14322
  customElements.define('pc-sky', SkyElement);
14066
14323
 
14067
- export { AnimClipElement, AnimComponentElement, AppElement, AssetElement, AsyncElement, ButtonComponentElement, CameraComponentElement, CollisionComponentElement, ComponentElement, ElementComponentElement, EntityBaseElement, EntityElement, GSplatComponentElement, JointComponentElement, LayoutChildComponentElement, LayoutGroupComponentElement, LightComponentElement, ListenerComponentElement, MaterialElement, ModelElement, ModuleElement, NodeElement, ParticleSystemComponentElement, RenderComponentElement, RigidBodyComponentElement, SceneElement, ScreenComponentElement, ScriptComponentElement, ScriptElement, ScrollViewComponentElement, ScrollbarComponentElement, SkyElement, SoundComponentElement, SoundSlotElement, whenReady };
14324
+ export { AnimClipElement, AnimComponentElement, AppElement, AssetElement, AsyncElement, AudioListenerComponentElement, ButtonComponentElement, CameraComponentElement, CollisionComponentElement, ComponentElement, ElementComponentElement, EntityBaseElement, EntityElement, EntityOwnerElement, GSplatComponentElement, JointComponentElement, LayoutChildComponentElement, LayoutGroupComponentElement, LightComponentElement, MaterialElement, ModelElement, NodeElement, ParticleSystemComponentElement, RenderComponentElement, RigidBodyComponentElement, SceneElement, ScreenComponentElement, ScriptComponentElement, ScriptInstanceElement, ScrollViewComponentElement, ScrollbarComponentElement, SkyElement, SoundComponentElement, SoundSlotElement, WasmElement, whenReady };
14068
14325
  //# sourceMappingURL=pwc.mjs.map