@playcanvas/web-components 0.11.1 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/pwc.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { basisInitialize, WasmModule, Vec3, Color, Vec4, Quat, Vec2, createGraphicsDevice, AppOptions, Keyboard, Mouse, ElementInput, AnimComponentSystem, AnimationComponentSystem, AudioListenerComponentSystem, ButtonComponentSystem, CameraComponentSystem, CollisionComponentSystem, ElementComponentSystem, GSplatComponentSystem, JointComponentSystem, LayoutChildComponentSystem, LayoutGroupComponentSystem, LightComponentSystem, ModelComponentSystem, ParticleSystemComponentSystem, RenderComponentSystem, RigidBodyComponentSystem, ScreenComponentSystem, ScriptComponentSystem, ScrollbarComponentSystem, ScrollViewComponentSystem, SoundComponentSystem, SpriteComponentSystem, ZoneComponentSystem, AnimClipHandler, AnimationHandler, AnimStateGraphHandler, AudioHandler, BinaryHandler, CssHandler, ContainerHandler, CubemapHandler, FolderHandler, FontHandler, GSplatHandler, HierarchyHandler, HtmlHandler, JsonHandler, MaterialHandler, ModelHandler, RenderHandler, ScriptHandler, SceneHandler, ShaderHandler, SpriteHandler, TemplateHandler, TextHandler, TextureAtlasHandler, TextureHandler, SoundManager, Lightmapper, BatchManager, XrManager, AppBase, FILLMODE_FILL_WINDOW, RESOLUTION_AUTO, Picker, MeshInstance, Entity, Asset, SPRITE_RENDERMODE_SIMPLE, SPRITE_RENDERMODE_SLICED, SPRITE_RENDERMODE_TILED, BUTTON_TRANSITION_MODE_TINT, BUTTON_TRANSITION_MODE_SPRITE_CHANGE, TONEMAP_NONE, PROJECTION_PERSPECTIVE, GAMMA_SRGB, GAMMA_NONE, XRTYPE_VR, TONEMAP_LINEAR, TONEMAP_FILMIC, TONEMAP_HEJL, TONEMAP_ACES, TONEMAP_ACES2, TONEMAP_NEUTRAL, PROJECTION_ORTHOGRAPHIC, ORIENTATION_HORIZONTAL, FITTING_NONE, FITTING_STRETCH, FITTING_SHRINK, FITTING_BOTH, ORIENTATION_VERTICAL, SHADOW_PCF3_32F, SHADOW_PCF1_16F, SHADOW_PCF1_32F, SHADOW_PCF3_16F, SHADOW_PCF5_16F, SHADOW_PCF5_32F, SHADOW_VSM_16F, SHADOW_VSM_32F, SHADOW_PCSS_32F, StandardMaterial, BLEND_NONE, CULLFACE_BACK, FRESNEL_SCHLICK, SPECOCC_AO, BLEND_NORMAL, BLEND_ADDITIVE, BLEND_ADDITIVEALPHA, BLEND_PREMULTIPLIED, BLEND_MULTIPLICATIVE, BLEND_MULTIPLICATIVE2X, BLEND_SCREEN, BLEND_MIN, BLEND_MAX, BLEND_SUBTRACTIVE, CULLFACE_NONE, CULLFACE_FRONT, CULLFACE_FRONTANDBACK, FRESNEL_NONE, SPECOCC_NONE, SPECOCC_GLOSSDEPENDENT, SCALEMODE_NONE, SCALEMODE_BLEND, SCROLL_MODE_BOUNCE, SCROLLBAR_VISIBILITY_SHOW_WHEN_REQUIRED, SCROLLBAR_VISIBILITY_SHOW_ALWAYS, SCROLL_MODE_CLAMP, SCROLL_MODE_INFINITE, EnvLighting, LAYERID_SKYBOX } from 'playcanvas';
1
+ import { basisInitialize, WasmModule, Vec3, Color, Vec4, Quat, Vec2, createGraphicsDevice, AppOptions, Keyboard, Mouse, ElementInput, AnimComponentSystem, AnimationComponentSystem, AudioListenerComponentSystem, ButtonComponentSystem, CameraComponentSystem, CollisionComponentSystem, ElementComponentSystem, GSplatComponentSystem, JointComponentSystem, LayoutChildComponentSystem, LayoutGroupComponentSystem, LightComponentSystem, ModelComponentSystem, ParticleSystemComponentSystem, RenderComponentSystem, RigidBodyComponentSystem, ScreenComponentSystem, ScriptComponentSystem, ScrollbarComponentSystem, ScrollViewComponentSystem, SoundComponentSystem, SpriteComponentSystem, ZoneComponentSystem, AnimClipHandler, AnimationHandler, AnimStateGraphHandler, AudioHandler, BinaryHandler, CssHandler, ContainerHandler, CubemapHandler, FolderHandler, FontHandler, GSplatHandler, HierarchyHandler, HtmlHandler, JsonHandler, MaterialHandler, ModelHandler, RenderHandler, ScriptHandler, SceneHandler, ShaderHandler, SpriteHandler, TemplateHandler, TextHandler, TextureAtlasHandler, TextureHandler, SoundManager, Lightmapper, BatchManager, XrManager, AppBase, FILLMODE_FILL_WINDOW, RESOLUTION_AUTO, Picker, MeshInstance, Entity, Asset, SPRITE_RENDERMODE_SIMPLE, FILTER_LINEAR_MIPMAP_LINEAR, FILTER_LINEAR, ADDRESS_REPEAT, SPRITE_RENDERMODE_SLICED, SPRITE_RENDERMODE_TILED, ADDRESS_CLAMP_TO_EDGE, ADDRESS_MIRRORED_REPEAT, FILTER_NEAREST, FILTER_NEAREST_MIPMAP_NEAREST, FILTER_LINEAR_MIPMAP_NEAREST, FILTER_NEAREST_MIPMAP_LINEAR, BUTTON_TRANSITION_MODE_TINT, BUTTON_TRANSITION_MODE_SPRITE_CHANGE, TONEMAP_NONE, PROJECTION_PERSPECTIVE, GAMMA_SRGB, GAMMA_NONE, XRTYPE_VR, TONEMAP_LINEAR, TONEMAP_FILMIC, TONEMAP_HEJL, TONEMAP_ACES, TONEMAP_ACES2, TONEMAP_NEUTRAL, PROJECTION_ORTHOGRAPHIC, ORIENTATION_HORIZONTAL, FITTING_NONE, FITTING_STRETCH, FITTING_SHRINK, FITTING_BOTH, ORIENTATION_VERTICAL, SHADOW_PCF3_32F, SHADOW_PCF1_16F, SHADOW_PCF1_32F, SHADOW_PCF3_16F, SHADOW_PCF5_16F, SHADOW_PCF5_32F, SHADOW_VSM_16F, SHADOW_VSM_32F, SHADOW_PCSS_32F, StandardMaterial, BLEND_NONE, CULLFACE_BACK, FRESNEL_SCHLICK, SPECOCC_AO, BLEND_NORMAL, BLEND_ADDITIVE, BLEND_ADDITIVEALPHA, BLEND_PREMULTIPLIED, BLEND_MULTIPLICATIVE, BLEND_MULTIPLICATIVE2X, BLEND_SCREEN, BLEND_MIN, BLEND_MAX, BLEND_SUBTRACTIVE, CULLFACE_NONE, CULLFACE_FRONT, CULLFACE_FRONTANDBACK, FRESNEL_NONE, SPECOCC_NONE, SPECOCC_GLOSSDEPENDENT, SCALEMODE_NONE, SCALEMODE_BLEND, SCROLL_MODE_BOUNCE, SCROLLBAR_VISIBILITY_SHOW_WHEN_REQUIRED, SCROLLBAR_VISIBILITY_SHOW_ALWAYS, SCROLL_MODE_CLAMP, SCROLL_MODE_INFINITE, EnvLighting, LAYERID_SKYBOX } from 'playcanvas';
2
2
 
3
3
  /**
4
4
  * Base class for all PlayCanvas Web Components that initialize asynchronously.
@@ -27,12 +27,13 @@ class AsyncElement extends HTMLElement {
27
27
  return this.parentElement?.closest('pc-app') ?? null;
28
28
  }
29
29
  /**
30
- * The nearest ancestor `<pc-entity>` element, or `null` if this element has no `<pc-entity>`
31
- * ancestor. The search starts at the parent, so an element never resolves to itself.
32
- * @returns The closest entity element, or `null`.
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.
33
+ * @returns The closest entity-fronting element, or `null`.
33
34
  */
34
35
  get closestEntity() {
35
- return this.parentElement?.closest('pc-entity') ?? null;
36
+ return this.parentElement?.closest('pc-entity, pc-node') ?? null;
36
37
  }
37
38
  /**
38
39
  * Called when the element is fully initialized and ready. Subclasses should call this when
@@ -742,8 +743,9 @@ class AppElement extends AsyncElement {
742
743
  _bootGeneration = 0;
743
744
  /**
744
745
  * The elements backing this application's entities, keyed by the entity itself. Registered
745
- * by EntityElement at creation and removed when an entity is destroyed, this joins engine
746
- * scene nodes back to their owning elements by identity - never by name.
746
+ * by EntityElement at creation (and NodeElement at binding) and removed when an entity is
747
+ * destroyed or unbound, this joins engine scene nodes back to their owning elements by
748
+ * identity - never by name.
747
749
  */
748
750
  _entityElements = new Map();
749
751
  _picker = null;
@@ -1048,7 +1050,7 @@ class AppElement extends AsyncElement {
1048
1050
  // created from onpointer* attributes when their elements were first upgraded, or
1049
1051
  // listeners carried over from before a re-boot)
1050
1052
  pointerEventTypes.forEach((type) => {
1051
- const anyListeners = Array.from(this.querySelectorAll('pc-entity')).some((entity) => entity._hasListeners(type));
1053
+ const anyListeners = Array.from(this.querySelectorAll('pc-entity, pc-node')).some((entity) => entity._hasListeners(type));
1052
1054
  if (anyListeners) {
1053
1055
  this._onPointerListenerAdded(type);
1054
1056
  }
@@ -1078,11 +1080,11 @@ class AppElement extends AsyncElement {
1078
1080
  };
1079
1081
  }
1080
1082
  /**
1081
- * Registers the element that created an entity. Called by EntityElement when it creates its
1082
- * entity.
1083
+ * Registers the element that fronts an entity. Called by EntityElement when it creates its
1084
+ * entity, and by NodeElement when it binds one.
1083
1085
  *
1084
1086
  * @param entity - The entity.
1085
- * @param element - The element that created it.
1087
+ * @param element - The element that fronts it.
1086
1088
  * @internal
1087
1089
  */
1088
1090
  _registerEntityElement(entity, element) {
@@ -1098,21 +1100,22 @@ class AppElement extends AsyncElement {
1098
1100
  this._entityElements.delete(entity);
1099
1101
  }
1100
1102
  /**
1101
- * Returns the `<pc-entity>` element whose backing entity is `entity`, or `null` if the
1102
- * entity was not created by an element of this application - for example, a node inside a
1103
- * model's instantiated hierarchy, or an entity created through the engine API.
1103
+ * Returns the `<pc-entity>` or `<pc-node>` element whose backing entity is `entity`, or
1104
+ * `null` if the entity is not fronted by an element of this application - for example, an
1105
+ * unbound node inside a model's instantiated hierarchy, or an entity created through the
1106
+ * engine API.
1104
1107
  *
1105
1108
  * @param entity - The entity to look up.
1106
- * @returns The element backing the entity, or `null`.
1109
+ * @returns The element fronting the entity, or `null`.
1107
1110
  */
1108
1111
  elementFromEntity(entity) {
1109
1112
  return this._entityElements.get(entity) ?? null;
1110
1113
  }
1111
1114
  /**
1112
1115
  * Resolves the element that owns a picked node: the nearest node up the parent chain -
1113
- * starting with the node itself - that was created by a `<pc-entity>` of this application.
1114
- * A hit inside a model's instantiated hierarchy therefore resolves to the element hosting
1115
- * the model.
1116
+ * starting with the node itself - that is fronted by a `<pc-entity>` or `<pc-node>` of this
1117
+ * application. A hit inside a model's instantiated hierarchy therefore resolves to the
1118
+ * nearest bound `<pc-node>`, or failing that the element hosting the model.
1116
1119
  *
1117
1120
  * @param node - The picked node, or `null`.
1118
1121
  * @returns The owning element, or `null`.
@@ -1244,7 +1247,7 @@ class AppElement extends AsyncElement {
1244
1247
  }
1245
1248
  }
1246
1249
  _onPointerListenerRemoved(type) {
1247
- const hasListeners = Array.from(this.querySelectorAll('pc-entity')).some((entity) => entity._hasListeners(type));
1250
+ const hasListeners = Array.from(this.querySelectorAll('pc-entity, pc-node')).some((entity) => entity._hasListeners(type));
1248
1251
  if (!hasListeners && this._canvas) {
1249
1252
  this._hasPointerListeners[type] = false;
1250
1253
  const handler = type === 'pointerenter' || type === 'pointerleave'
@@ -1422,6 +1425,125 @@ class AppElement extends AsyncElement {
1422
1425
  }
1423
1426
  customElements.define('pc-app', AppElement);
1424
1427
 
1428
+ /**
1429
+ * The attribute names of the inline `onpointer*` event handlers, shared by every element that
1430
+ * fronts an engine entity. Spread into `observedAttributes` by subclasses.
1431
+ * @ignore
1432
+ */
1433
+ const POINTER_ATTRIBUTES = [
1434
+ 'onpointerenter',
1435
+ 'onpointerleave',
1436
+ 'onpointerdown',
1437
+ 'onpointerup',
1438
+ 'onpointermove'
1439
+ ];
1440
+ /**
1441
+ * The base class for elements that front an engine {@link Entity}: `<pc-entity>`, which creates
1442
+ * one, and `<pc-node>`, which binds to one inside a model's instantiated hierarchy. It carries
1443
+ * what both need — the `entity` contract, registration with the owning application (which joins
1444
+ * picked scene nodes back to elements by identity, never by name), and the pointer listener
1445
+ * bookkeeping that lets the application lazily attach its canvas handlers.
1446
+ */
1447
+ class EntityBaseElement extends AsyncElement {
1448
+ _entity = null;
1449
+ /**
1450
+ * The application element this entity is registered with, cached at registration time so the
1451
+ * entity can be unregistered even once this element has left the DOM.
1452
+ */
1453
+ _appElement = null;
1454
+ /**
1455
+ * The pointer event listeners for the entity.
1456
+ */
1457
+ _listeners = {};
1458
+ /**
1459
+ * The event types for which an inline `onpointer*` attribute is currently present.
1460
+ */
1461
+ _inlineHandlerTypes = new Set();
1462
+ /**
1463
+ * The PlayCanvas entity instance. `null` until the element is ready, and again once the
1464
+ * entity is gone — await {@link whenReady} or the element's `ready()` promise before
1465
+ * accessing it.
1466
+ * @returns The entity instance, or `null`.
1467
+ */
1468
+ get entity() {
1469
+ return this._entity;
1470
+ }
1471
+ /**
1472
+ * Registers `entity` as this element's backing entity with the owning application, which
1473
+ * joins engine nodes back to elements by identity (never by name).
1474
+ *
1475
+ * @param entity - The entity to register.
1476
+ */
1477
+ _registerEntity(entity) {
1478
+ this._appElement = this.closestApp;
1479
+ this._appElement?._registerEntityElement(entity, this);
1480
+ }
1481
+ /**
1482
+ * Removes the registration for `entity`.
1483
+ *
1484
+ * @param entity - The entity to unregister.
1485
+ */
1486
+ _unregisterEntity(entity) {
1487
+ this._appElement?._unregisterEntityElement(entity);
1488
+ this._appElement = null;
1489
+ }
1490
+ /**
1491
+ * Tracks whether an inline `onpointer*` attribute is present. The browser itself compiles and
1492
+ * runs these attributes — they are standard `GlobalEventHandlers`, so setting one replaces
1493
+ * the previous handler and removing it removes the handler, exactly like `onclick` on any
1494
+ * HTML element. But because they bypass {@link addEventListener}, the connect/disconnect
1495
+ * bookkeeping that lets the application lazily attach its canvas pointer handlers must be
1496
+ * kept in sync here.
1497
+ *
1498
+ * @param name - The attribute name (e.g. 'onpointerdown').
1499
+ * @param value - The attribute value, or `null` when the attribute has been removed.
1500
+ */
1501
+ _updateInlineHandler(name, value) {
1502
+ const type = name.substring(2);
1503
+ const had = this._inlineHandlerTypes.has(type);
1504
+ const has = value !== null;
1505
+ if (has && !had) {
1506
+ this._inlineHandlerTypes.add(type);
1507
+ this.dispatchEvent(new CustomEvent(`${type}:connect`, { bubbles: true }));
1508
+ }
1509
+ else if (!has && had) {
1510
+ this._inlineHandlerTypes.delete(type);
1511
+ this.dispatchEvent(new CustomEvent(`${type}:disconnect`, { bubbles: true }));
1512
+ }
1513
+ }
1514
+ addEventListener(type, listener, options) {
1515
+ if (!this._listeners[type]) {
1516
+ this._listeners[type] = [];
1517
+ }
1518
+ this._listeners[type].push(listener);
1519
+ super.addEventListener(type, listener, options);
1520
+ if (type.startsWith('pointer')) {
1521
+ this.dispatchEvent(new CustomEvent(`${type}:connect`, { bubbles: true }));
1522
+ }
1523
+ }
1524
+ removeEventListener(type, listener, options) {
1525
+ if (this._listeners[type]) {
1526
+ this._listeners[type] = this._listeners[type].filter((l) => l !== listener);
1527
+ }
1528
+ super.removeEventListener(type, listener, options);
1529
+ if (type.startsWith('pointer')) {
1530
+ this.dispatchEvent(new CustomEvent(`${type}:disconnect`, { bubbles: true }));
1531
+ }
1532
+ }
1533
+ /**
1534
+ * Whether the element has a listener for an event type, registered either with
1535
+ * {@link addEventListener} or with the matching inline `onpointer*` attribute. Read by the
1536
+ * containing `<pc-app>` element to gate pointer event synthesis.
1537
+ *
1538
+ * @param type - The event type.
1539
+ * @returns Whether a listener is registered.
1540
+ * @internal
1541
+ */
1542
+ _hasListeners(type) {
1543
+ return Boolean(this._listeners[type]?.length) || this._inlineHandlerTypes.has(type);
1544
+ }
1545
+ }
1546
+
1425
1547
  /**
1426
1548
  * The EntityElement interface provides properties and methods for manipulating
1427
1549
  * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-entity/ | `<pc-entity>`} elements.
@@ -1446,7 +1568,7 @@ customElements.define('pc-app', AppElement);
1446
1568
  * @fires {PointerEvent} pointerdown - Fired when a pointer button is pressed over the entity.
1447
1569
  * @fires {PointerEvent} pointerup - Fired when a pointer button is released over the entity.
1448
1570
  */
1449
- class EntityElement extends AsyncElement {
1571
+ class EntityElement extends EntityBaseElement {
1450
1572
  /**
1451
1573
  * Whether the entity is enabled.
1452
1574
  */
@@ -1471,33 +1593,10 @@ class EntityElement extends AsyncElement {
1471
1593
  * The tags of the entity.
1472
1594
  */
1473
1595
  _tags = [];
1474
- /**
1475
- * The pointer event listeners for the entity.
1476
- */
1477
- _listeners = {};
1478
- /**
1479
- * The event types for which an inline `onpointer*` attribute is currently present.
1480
- */
1481
- _inlineHandlerTypes = new Set();
1482
1596
  /**
1483
1597
  * Whether the hierarchy has been built for this entity.
1484
1598
  */
1485
1599
  _built = false;
1486
- _entity = null;
1487
- /**
1488
- * The application element this entity is registered with, cached at creation time so the
1489
- * entity can be unregistered even once this element has left the DOM.
1490
- */
1491
- _appElement = null;
1492
- /**
1493
- * The PlayCanvas entity instance. `null` until the element is ready, and again once it has
1494
- * been removed from the document — await {@link whenReady} or the element's `ready()`
1495
- * promise before accessing it.
1496
- * @returns The entity instance, or `null`.
1497
- */
1498
- get entity() {
1499
- return this._entity;
1500
- }
1501
1600
  /**
1502
1601
  * Creates the backing entity. Called by the containing `<pc-app>` element during its boot
1503
1602
  * sweep, and on connection for elements inserted while the application is already running.
@@ -1525,13 +1624,11 @@ class EntityElement extends AsyncElement {
1525
1624
  if (this._tags.length > 0) {
1526
1625
  entity.tags.add(this._tags);
1527
1626
  }
1528
- // Register with the owning application, which joins engine nodes back to elements by
1529
- // identity (never by name), and hook the entity's destruction. The engine fires 'destroy'
1530
- // for every entity in a destroyed subtree, so the element learns of its entity's death no
1531
- // matter who causes it: this element, an ancestor, the whole application, or a user
1532
- // script calling entity.destroy().
1533
- this._appElement = this.closestApp;
1534
- this._appElement?._registerEntityElement(entity, this);
1627
+ // Register with the owning application and hook the entity's destruction. The engine
1628
+ // fires 'destroy' for every entity in a destroyed subtree, so the element learns of its
1629
+ // entity's death no matter who causes it: this element, an ancestor, the whole
1630
+ // application, or a user script calling entity.destroy().
1631
+ this._registerEntity(entity);
1535
1632
  entity.once('destroy', this._onEntityDestroy, this);
1536
1633
  }
1537
1634
  /**
@@ -1544,17 +1641,16 @@ class EntityElement extends AsyncElement {
1544
1641
  * @param entity - The entity that was destroyed.
1545
1642
  */
1546
1643
  _onEntityDestroy(entity) {
1547
- this._appElement?._unregisterEntityElement(entity);
1548
- this._appElement = null;
1644
+ this._unregisterEntity(entity);
1549
1645
  this._entity = null;
1550
1646
  this._built = false;
1551
1647
  this._resetReady();
1552
1648
  }
1553
1649
  /**
1554
- * Parents the backing entity: under the entity of the nearest ancestor `<pc-entity>` when
1555
- * there is one, and under the application root otherwise. Called by the containing `<pc-app>`
1556
- * element once a sweep has created every entity, so a parent's existence never depends on
1557
- * document order.
1650
+ * Parents the backing entity: under the entity of the nearest ancestor `<pc-entity>` or
1651
+ * `<pc-node>` when there is one, and under the application root otherwise. Called by the
1652
+ * containing `<pc-app>` element once a sweep has created every entity, so a parent's
1653
+ * existence never depends on document order.
1558
1654
  *
1559
1655
  * @param app - The application whose root adopts parentless entities.
1560
1656
  * @internal
@@ -1562,8 +1658,14 @@ class EntityElement extends AsyncElement {
1562
1658
  _buildHierarchy(app) {
1563
1659
  if (!this.entity || this._built)
1564
1660
  return;
1565
- this._built = true;
1566
1661
  const closestEntity = this.closestEntity;
1662
+ // A host element without an entity is an unresolved `<pc-node>`: building now would
1663
+ // mis-anchor this entity to the application root while the host is still resolving.
1664
+ // Stay unbuilt - the host drives this subtree itself once it binds.
1665
+ if (closestEntity && !closestEntity.entity) {
1666
+ return;
1667
+ }
1668
+ this._built = true;
1567
1669
  if (closestEntity?.entity) {
1568
1670
  closestEntity.entity.addChild(this.entity);
1569
1671
  }
@@ -1709,44 +1811,8 @@ class EntityElement extends AsyncElement {
1709
1811
  get tags() {
1710
1812
  return this._tags;
1711
1813
  }
1712
- /**
1713
- * Tracks whether an inline `onpointer*` attribute is present. The browser itself compiles and
1714
- * runs these attributes — they are standard `GlobalEventHandlers`, so setting one replaces
1715
- * the previous handler and removing it removes the handler, exactly like `onclick` on any
1716
- * HTML element. But because they bypass {@link addEventListener}, the connect/disconnect
1717
- * bookkeeping that lets the application lazily attach its canvas pointer handlers must be
1718
- * kept in sync here.
1719
- *
1720
- * @param name - The attribute name (e.g. 'onpointerdown').
1721
- * @param value - The attribute value, or `null` when the attribute has been removed.
1722
- */
1723
- _updateInlineHandler(name, value) {
1724
- const type = name.substring(2);
1725
- const had = this._inlineHandlerTypes.has(type);
1726
- const has = value !== null;
1727
- if (has && !had) {
1728
- this._inlineHandlerTypes.add(type);
1729
- this.dispatchEvent(new CustomEvent(`${type}:connect`, { bubbles: true }));
1730
- }
1731
- else if (!has && had) {
1732
- this._inlineHandlerTypes.delete(type);
1733
- this.dispatchEvent(new CustomEvent(`${type}:disconnect`, { bubbles: true }));
1734
- }
1735
- }
1736
1814
  static get observedAttributes() {
1737
- return [
1738
- 'enabled',
1739
- 'name',
1740
- 'position',
1741
- 'rotation',
1742
- 'scale',
1743
- 'tags',
1744
- 'onpointerenter',
1745
- 'onpointerleave',
1746
- 'onpointerdown',
1747
- 'onpointerup',
1748
- 'onpointermove'
1749
- ];
1815
+ return ['enabled', 'name', 'position', 'rotation', 'scale', 'tags', ...POINTER_ATTRIBUTES];
1750
1816
  }
1751
1817
  attributeChangedCallback(name, _oldValue, newValue) {
1752
1818
  switch (name) {
@@ -1777,37 +1843,6 @@ class EntityElement extends AsyncElement {
1777
1843
  break;
1778
1844
  }
1779
1845
  }
1780
- addEventListener(type, listener, options) {
1781
- if (!this._listeners[type]) {
1782
- this._listeners[type] = [];
1783
- }
1784
- this._listeners[type].push(listener);
1785
- super.addEventListener(type, listener, options);
1786
- if (type.startsWith('pointer')) {
1787
- this.dispatchEvent(new CustomEvent(`${type}:connect`, { bubbles: true }));
1788
- }
1789
- }
1790
- removeEventListener(type, listener, options) {
1791
- if (this._listeners[type]) {
1792
- this._listeners[type] = this._listeners[type].filter((l) => l !== listener);
1793
- }
1794
- super.removeEventListener(type, listener, options);
1795
- if (type.startsWith('pointer')) {
1796
- this.dispatchEvent(new CustomEvent(`${type}:disconnect`, { bubbles: true }));
1797
- }
1798
- }
1799
- /**
1800
- * Whether the element has a listener for an event type, registered either with
1801
- * {@link addEventListener} or with the matching inline `onpointer*` attribute. Read by the
1802
- * containing `<pc-app>` element to gate pointer event synthesis.
1803
- *
1804
- * @param type - The event type.
1805
- * @returns Whether a listener is registered.
1806
- * @internal
1807
- */
1808
- _hasListeners(type) {
1809
- return Boolean(this._listeners[type]?.length) || this._inlineHandlerTypes.has(type);
1810
- }
1811
1846
  }
1812
1847
  customElements.define('pc-entity', EntityElement);
1813
1848
 
@@ -1992,6 +2027,48 @@ const renderModes = new Map([
1992
2027
  ['sliced', SPRITE_RENDERMODE_SLICED],
1993
2028
  ['tiled', SPRITE_RENDERMODE_TILED]
1994
2029
  ]);
2030
+ const addressModes = new Map([
2031
+ ['repeat', ADDRESS_REPEAT],
2032
+ ['clamp', ADDRESS_CLAMP_TO_EDGE],
2033
+ ['mirror', ADDRESS_MIRRORED_REPEAT]
2034
+ ]);
2035
+ const minFilterModes = new Map([
2036
+ ['nearest', FILTER_NEAREST],
2037
+ ['linear', FILTER_LINEAR],
2038
+ ['nearest-mip-nearest', FILTER_NEAREST_MIPMAP_NEAREST],
2039
+ ['linear-mip-nearest', FILTER_LINEAR_MIPMAP_NEAREST],
2040
+ ['nearest-mip-linear', FILTER_NEAREST_MIPMAP_LINEAR],
2041
+ ['linear-mip-linear', FILTER_LINEAR_MIPMAP_LINEAR]
2042
+ ]);
2043
+ const magFilterModes = new Map([
2044
+ ['nearest', FILTER_NEAREST],
2045
+ ['linear', FILTER_LINEAR]
2046
+ ]);
2047
+ // The engine's texture JSON spells the filter names with underscores ('linear_mip_linear'); the
2048
+ // attribute values are kebab-case like every other enum attribute in this library. The address
2049
+ // mode names contain no dashes, so for them the rename is the identity.
2050
+ const toTextureJson = (name) => name.replace(/-/g, '_');
2051
+ // Engine Texture constructor defaults, restored on a loaded texture when a texture option
2052
+ // attribute is removed.
2053
+ const textureOptionDefaults = {
2054
+ addressU: ADDRESS_REPEAT,
2055
+ addressV: ADDRESS_REPEAT,
2056
+ anisotropy: 1,
2057
+ flipY: false,
2058
+ magFilter: FILTER_LINEAR,
2059
+ minFilter: FILTER_LINEAR_MIPMAP_LINEAR,
2060
+ mipmaps: true,
2061
+ srgb: false
2062
+ };
2063
+ // Attributes that only apply to certain asset types, used to warn when one is set on an asset of
2064
+ // any other type (where it would otherwise be silently ignored).
2065
+ const typeScopedAttributes = [
2066
+ [
2067
+ ['address-u', 'address-v', 'anisotropy', 'flip-y', 'mag-filter', 'min-filter', 'mipmaps', 'srgb'],
2068
+ ['texture', 'textureatlas']
2069
+ ],
2070
+ [['atlas', 'frame-keys', 'pixels-per-unit', 'render-mode'], ['sprite']]
2071
+ ];
1995
2072
  const extToType = new Map([
1996
2073
  ['bin', 'binary'],
1997
2074
  ['css', 'css'],
@@ -2048,8 +2125,16 @@ const processBufferView = (gltfBuffer, buffers, continuation) => {
2048
2125
  * immediately unless `lazy`. A `pc-asset` must be a direct child of `pc-app` — elements placed
2049
2126
  * elsewhere, or with an unsupported asset type, never become ready.
2050
2127
  *
2051
- * Apart from `lazy`, these attributes are read once when the asset is created, so changing them
2052
- * later has no effect.
2128
+ * For `texture` and `textureatlas` assets, the texture options (`address-u`, `address-v`,
2129
+ * `min-filter`, `mag-filter`, `anisotropy`, `mipmaps`, `srgb`, `flip-y`) apply when the texture is
2130
+ * created and — like `lazy` — are observed: changing one updates a texture that has already
2131
+ * loaded, and removing one restores the engine default. Changing `srgb` or `mipmaps` on a loaded
2132
+ * texture recreates the underlying GPU resource, so prefer declaring those up front. Each option
2133
+ * overrides the matching key in the `data` JSON; options left unset write nothing, leaving the
2134
+ * engine's per-format defaults in force.
2135
+ *
2136
+ * Apart from `lazy` and the texture options, these attributes are read once when the asset is
2137
+ * created, so changing them later has no effect.
2053
2138
  *
2054
2139
  * @attribute {string} id - The identifier used to reference the asset from other elements.
2055
2140
  * @attribute {string} src - The URL of the asset to load.
@@ -2071,7 +2156,15 @@ const processBufferView = (gltfBuffer, buffers, continuation) => {
2071
2156
  * not that it succeeded.
2072
2157
  */
2073
2158
  class AssetElement extends AsyncElement {
2159
+ _addressU = null;
2160
+ _addressV = null;
2161
+ _anisotropy = null;
2162
+ _flipY = null;
2074
2163
  _lazy = false;
2164
+ _magFilter = null;
2165
+ _minFilter = null;
2166
+ _mipmaps = null;
2167
+ _srgb = null;
2075
2168
  /**
2076
2169
  * The asset that is loaded. Available once the element is ready — await
2077
2170
  * {@link whenReady} or the element's `ready()` promise before accessing it.
@@ -2142,6 +2235,15 @@ class AssetElement extends AsyncElement {
2142
2235
  console.warn(`Unsupported asset type: ${src}`);
2143
2236
  return;
2144
2237
  }
2238
+ // Attributes scoped to other asset types have no effect here - say so rather than
2239
+ // failing silently.
2240
+ const inapplicable = typeScopedAttributes
2241
+ .filter(([, types]) => !types.includes(type))
2242
+ .flatMap(([attributes]) => attributes)
2243
+ .filter((attribute) => this.hasAttribute(attribute));
2244
+ if (inapplicable.length > 0) {
2245
+ console.warn(`pc-asset '${id || src}' has attributes that do not apply to asset type '${type}' and are ignored: ${inapplicable.join(', ')}`);
2246
+ }
2145
2247
  // Optional inline asset data, used by data-driven assets such as texture atlases (frame
2146
2248
  // definitions) and sprites (atlas reference, frame keys, etc.).
2147
2249
  const data = this._buildData(type);
@@ -2169,9 +2271,11 @@ class AssetElement extends AsyncElement {
2169
2271
  this.asset.on('error', this._onAssetError, this);
2170
2272
  }
2171
2273
  /**
2172
- * Builds the `data` object for the asset from an optional inline `data` attribute (JSON) and,
2173
- * for sprites, from the convenience attributes (`atlas`, `frame-keys`, `pixels-per-unit`,
2174
- * `render-mode`). Returns `undefined` when there is no data to apply.
2274
+ * Builds the `data` object for the asset from an optional inline `data` attribute (JSON), the
2275
+ * texture option attributes (for `texture` and `textureatlas` assets), and the sprite
2276
+ * convenience attributes (`atlas`, `frame-keys`, `pixels-per-unit`, `render-mode`). An
2277
+ * attribute overrides the matching `data` JSON key. Returns `undefined` when there is no data
2278
+ * to apply.
2175
2279
  * @param type - The resolved asset type.
2176
2280
  * @returns The asset data, or `undefined`.
2177
2281
  */
@@ -2186,6 +2290,37 @@ class AssetElement extends AsyncElement {
2186
2290
  console.warn(`Invalid 'data' JSON on pc-asset: ${dataAttr}`);
2187
2291
  }
2188
2292
  }
2293
+ if (type === 'texture' || type === 'textureatlas') {
2294
+ data = data ?? {};
2295
+ // Only options the user actually set are written: the engine reads these keys with
2296
+ // hasOwnProperty semantics, and an absent key leaves its per-format default (an HDR's
2297
+ // 'rgbe' type, a KTX2's transcoded format) in force.
2298
+ if (this._addressU !== null) {
2299
+ data.addressu = this._addressU;
2300
+ }
2301
+ if (this._addressV !== null) {
2302
+ data.addressv = this._addressV;
2303
+ }
2304
+ if (this._anisotropy !== null) {
2305
+ data.anisotropy = this._anisotropy;
2306
+ }
2307
+ if (this._flipY !== null) {
2308
+ // 'flipY' is the one camelCase key in the engine's texture JSON
2309
+ data.flipY = this._flipY;
2310
+ }
2311
+ if (this._magFilter !== null) {
2312
+ data.magfilter = toTextureJson(this._magFilter);
2313
+ }
2314
+ if (this._minFilter !== null) {
2315
+ data.minfilter = toTextureJson(this._minFilter);
2316
+ }
2317
+ if (this._mipmaps !== null) {
2318
+ data.mipmaps = this._mipmaps;
2319
+ }
2320
+ if (this._srgb !== null) {
2321
+ data.srgb = this._srgb;
2322
+ }
2323
+ }
2189
2324
  if (type === 'sprite') {
2190
2325
  data = data ?? {};
2191
2326
  // Resolve the referenced texture atlas to its (numeric) asset id. The atlas must be
@@ -2219,6 +2354,56 @@ class AssetElement extends AsyncElement {
2219
2354
  }
2220
2355
  return data;
2221
2356
  }
2357
+ /**
2358
+ * Returns the engine texture behind this asset, when there is one: the resource itself for a
2359
+ * `texture` asset, the atlas's texture for a `textureatlas` asset, `null` otherwise
2360
+ * (including before the asset has loaded).
2361
+ * @returns The texture, or `null`.
2362
+ */
2363
+ _texture() {
2364
+ const asset = this.asset;
2365
+ if (!asset?.resource)
2366
+ return null;
2367
+ if (asset.type === 'texture')
2368
+ return asset.resource;
2369
+ if (asset.type === 'textureatlas')
2370
+ return asset.resource.texture ?? null;
2371
+ return null;
2372
+ }
2373
+ /**
2374
+ * Writes one texture option through to the created asset, if any. The engine-JSON key is
2375
+ * written into `asset.data`, mutated in place - replacing the whole object would make the
2376
+ * registry re-patch every key, and a re-patched `srgb` or `mipmaps` recreates the texture
2377
+ * even when unchanged. The in-place key is what a not-yet-started load reads at texture
2378
+ * construction, and what any later reload reads. When the texture already exists, the
2379
+ * corresponding property is assigned directly; `null` (attribute removed) deletes the key
2380
+ * and restores the engine default. Assets of any other type are left untouched.
2381
+ *
2382
+ * @param key - The engine texture JSON key in `asset.data`.
2383
+ * @param property - The Texture property to assign.
2384
+ * @param dataValue - The engine-JSON value for `asset.data`, or `null` to delete the key.
2385
+ * @param textureValue - The value for the Texture property, or `null` for the engine default.
2386
+ */
2387
+ _applyTextureOption(key, property, dataValue, textureValue) {
2388
+ const asset = this.asset;
2389
+ if (!asset || (asset.type !== 'texture' && asset.type !== 'textureatlas'))
2390
+ return;
2391
+ const data = asset.data;
2392
+ if (dataValue === null) {
2393
+ delete data[key];
2394
+ }
2395
+ else {
2396
+ data[key] = dataValue;
2397
+ }
2398
+ const texture = this._texture();
2399
+ if (texture) {
2400
+ // Every option here is a number- or boolean-valued Texture property; the
2401
+ // value/property pairing is fixed by the callers, which TypeScript cannot see
2402
+ // through the union.
2403
+ texture[property] =
2404
+ textureValue ?? textureOptionDefaults[property];
2405
+ }
2406
+ }
2222
2407
  _destroyAsset() {
2223
2408
  if (this.asset) {
2224
2409
  // A caller that keeps the Asset alive must not dispatch on a removed element
@@ -2230,6 +2415,75 @@ class AssetElement extends AsyncElement {
2230
2415
  this.asset = null;
2231
2416
  }
2232
2417
  }
2418
+ /**
2419
+ * Sets the texture's horizontal (U) address mode: how texture coordinates outside the 0 to 1
2420
+ * range sample the texture. Applies to `texture` and `textureatlas` assets, both when the
2421
+ * texture is created and after it has loaded.
2422
+ * @param value - The address mode, or `null` to use the engine default of 'repeat'.
2423
+ */
2424
+ set addressU(value) {
2425
+ this._addressU = value;
2426
+ const constant = value === null ? null : (addressModes.get(value) ?? ADDRESS_REPEAT);
2427
+ this._applyTextureOption('addressu', 'addressU', value, constant);
2428
+ }
2429
+ /**
2430
+ * Gets the texture's horizontal (U) address mode.
2431
+ * @returns The address mode, or `null` when unset.
2432
+ */
2433
+ get addressU() {
2434
+ return this._addressU;
2435
+ }
2436
+ /**
2437
+ * Sets the texture's vertical (V) address mode: how texture coordinates outside the 0 to 1
2438
+ * range sample the texture. Applies to `texture` and `textureatlas` assets, both when the
2439
+ * texture is created and after it has loaded.
2440
+ * @param value - The address mode, or `null` to use the engine default of 'repeat'.
2441
+ */
2442
+ set addressV(value) {
2443
+ this._addressV = value;
2444
+ const constant = value === null ? null : (addressModes.get(value) ?? ADDRESS_REPEAT);
2445
+ this._applyTextureOption('addressv', 'addressV', value, constant);
2446
+ }
2447
+ /**
2448
+ * Gets the texture's vertical (V) address mode.
2449
+ * @returns The address mode, or `null` when unset.
2450
+ */
2451
+ get addressV() {
2452
+ return this._addressV;
2453
+ }
2454
+ /**
2455
+ * Sets the texture's maximum anisotropic filtering level, which improves quality at oblique
2456
+ * viewing angles. Applies to `texture` and `textureatlas` assets, both when the texture is
2457
+ * created and after it has loaded.
2458
+ * @param value - The anisotropy level, or `null` to use the engine default of 1.
2459
+ */
2460
+ set anisotropy(value) {
2461
+ this._anisotropy = value;
2462
+ this._applyTextureOption('anisotropy', 'anisotropy', value, value);
2463
+ }
2464
+ /**
2465
+ * Gets the texture's maximum anisotropic filtering level.
2466
+ * @returns The anisotropy level, or `null` when unset.
2467
+ */
2468
+ get anisotropy() {
2469
+ return this._anisotropy;
2470
+ }
2471
+ /**
2472
+ * Sets whether the texture's image data is flipped vertically at upload. Applies to `texture`
2473
+ * and `textureatlas` assets, both when the texture is created and after it has loaded.
2474
+ * @param value - The flip flag, or `null` to use the engine default of `false`.
2475
+ */
2476
+ set flipY(value) {
2477
+ this._flipY = value;
2478
+ this._applyTextureOption('flipY', 'flipY', value, value);
2479
+ }
2480
+ /**
2481
+ * Gets whether the texture's image data is flipped vertically at upload.
2482
+ * @returns The flip flag, or `null` when unset.
2483
+ */
2484
+ get flipY() {
2485
+ return this._flipY;
2486
+ }
2233
2487
  /**
2234
2488
  * Sets whether the asset should be loaded lazily.
2235
2489
  * @param value - The lazy loading flag.
@@ -2248,37 +2502,200 @@ class AssetElement extends AsyncElement {
2248
2502
  return this._lazy;
2249
2503
  }
2250
2504
  /**
2251
- * Returns the {@link Asset} created by the `<pc-asset>` element with the given `id`, or
2252
- * `undefined` if there is no such element or its asset has not been created yet.
2253
- *
2254
- * @param id - The `id` of the `<pc-asset>` element.
2255
- * @returns The asset, or `undefined`.
2505
+ * Sets the texture's magnification filter, used when the texture is displayed larger than its
2506
+ * source size. Applies to `texture` and `textureatlas` assets, both when the texture is
2507
+ * created and after it has loaded.
2508
+ * @param value - The filter, or `null` to use the engine default of 'linear'.
2256
2509
  */
2257
- static get(id) {
2258
- const assetElement = document.querySelector(`pc-asset[id="${id}"]`);
2259
- return assetElement?.asset;
2510
+ set magFilter(value) {
2511
+ this._magFilter = value;
2512
+ const json = value === null ? null : toTextureJson(value);
2513
+ const constant = value === null ? null : (magFilterModes.get(value) ?? FILTER_LINEAR);
2514
+ this._applyTextureOption('magfilter', 'magFilter', json, constant);
2260
2515
  }
2261
- static get observedAttributes() {
2262
- return ['lazy'];
2516
+ /**
2517
+ * Gets the texture's magnification filter.
2518
+ * @returns The filter, or `null` when unset.
2519
+ */
2520
+ get magFilter() {
2521
+ return this._magFilter;
2263
2522
  }
2264
- attributeChangedCallback(name, _oldValue, newValue) {
2265
- if (name === 'lazy') {
2266
- this.lazy = parseBool(newValue, false);
2267
- }
2523
+ /**
2524
+ * Sets the texture's minification filter, used when the texture is displayed smaller than its
2525
+ * source size. The mip variants blend within (and, for the second `linear`, between) mipmap
2526
+ * levels. Applies to `texture` and `textureatlas` assets, both when the texture is created
2527
+ * and after it has loaded.
2528
+ * @param value - The filter, or `null` to use the engine default of 'linear-mip-linear'.
2529
+ */
2530
+ set minFilter(value) {
2531
+ this._minFilter = value;
2532
+ const json = value === null ? null : toTextureJson(value);
2533
+ const constant = value === null ? null : (minFilterModes.get(value) ?? FILTER_LINEAR_MIPMAP_LINEAR);
2534
+ this._applyTextureOption('minfilter', 'minFilter', json, constant);
2268
2535
  }
2269
- }
2270
- customElements.define('pc-asset', AssetElement);
2271
-
2272
- /**
2273
- * Represents a component in the PlayCanvas engine.
2274
- *
2275
- * @category Components
2276
- */
2536
+ /**
2537
+ * Gets the texture's minification filter.
2538
+ * @returns The filter, or `null` when unset.
2539
+ */
2540
+ get minFilter() {
2541
+ return this._minFilter;
2542
+ }
2543
+ /**
2544
+ * Sets whether the texture generates and uses mipmaps. Changing this on a loaded texture
2545
+ * recreates the underlying GPU resource, so prefer declaring it up front. Applies to
2546
+ * `texture` and `textureatlas` assets.
2547
+ * @param value - The mipmaps flag, or `null` to use the engine default of `true`.
2548
+ */
2549
+ set mipmaps(value) {
2550
+ this._mipmaps = value;
2551
+ this._applyTextureOption('mipmaps', 'mipmaps', value, value);
2552
+ }
2553
+ /**
2554
+ * Gets whether the texture generates and uses mipmaps.
2555
+ * @returns The mipmaps flag, or `null` when unset.
2556
+ */
2557
+ get mipmaps() {
2558
+ return this._mipmaps;
2559
+ }
2560
+ /**
2561
+ * Sets whether the texture holds sRGB (gamma-encoded) color data, enabling hardware gamma
2562
+ * decode. Free when set before the texture loads; changing it on a loaded texture recreates
2563
+ * the underlying GPU resource. Applies to `texture` and `textureatlas` assets.
2564
+ * @param value - The sRGB flag, or `null` to use the engine default of `false`.
2565
+ */
2566
+ set srgb(value) {
2567
+ this._srgb = value;
2568
+ this._applyTextureOption('srgb', 'srgb', value, value);
2569
+ }
2570
+ /**
2571
+ * Gets whether the texture holds sRGB (gamma-encoded) color data.
2572
+ * @returns The sRGB flag, or `null` when unset.
2573
+ */
2574
+ get srgb() {
2575
+ return this._srgb;
2576
+ }
2577
+ /**
2578
+ * Returns the {@link Asset} created by the `<pc-asset>` element with the given `id`, or
2579
+ * `undefined` if there is no such element or its asset has not been created yet.
2580
+ *
2581
+ * @param id - The `id` of the `<pc-asset>` element.
2582
+ * @returns The asset, or `undefined`.
2583
+ */
2584
+ static get(id) {
2585
+ const assetElement = document.querySelector(`pc-asset[id="${id}"]`);
2586
+ return assetElement?.asset;
2587
+ }
2588
+ static get observedAttributes() {
2589
+ return [
2590
+ 'address-u',
2591
+ 'address-v',
2592
+ 'anisotropy',
2593
+ 'flip-y',
2594
+ 'lazy',
2595
+ 'mag-filter',
2596
+ 'min-filter',
2597
+ 'mipmaps',
2598
+ 'srgb'
2599
+ ];
2600
+ }
2601
+ attributeChangedCallback(name, _oldValue, newValue) {
2602
+ // Each texture option keeps its parse* call as the branch's first assignment (the CEM
2603
+ // manifest derives the attribute's type and default from it - a ternary would degrade
2604
+ // both to plain string) and treats a removed attribute (null) as a reset to unset,
2605
+ // which restores the engine default on a loaded texture.
2606
+ switch (name) {
2607
+ case 'address-u':
2608
+ if (newValue !== null) {
2609
+ this.addressU = parseEnum(newValue, addressModes, 'repeat', name);
2610
+ }
2611
+ else {
2612
+ this.addressU = null;
2613
+ }
2614
+ break;
2615
+ case 'address-v':
2616
+ if (newValue !== null) {
2617
+ this.addressV = parseEnum(newValue, addressModes, 'repeat', name);
2618
+ }
2619
+ else {
2620
+ this.addressV = null;
2621
+ }
2622
+ break;
2623
+ case 'anisotropy':
2624
+ if (newValue !== null) {
2625
+ this.anisotropy = parseNumber(newValue, 1, name);
2626
+ }
2627
+ else {
2628
+ this.anisotropy = null;
2629
+ }
2630
+ break;
2631
+ case 'flip-y':
2632
+ if (newValue !== null) {
2633
+ this.flipY = parseBool(newValue, false);
2634
+ }
2635
+ else {
2636
+ this.flipY = null;
2637
+ }
2638
+ break;
2639
+ case 'lazy':
2640
+ this.lazy = parseBool(newValue, false);
2641
+ break;
2642
+ case 'mag-filter':
2643
+ if (newValue !== null) {
2644
+ this.magFilter = parseEnum(newValue, magFilterModes, 'linear', name);
2645
+ }
2646
+ else {
2647
+ this.magFilter = null;
2648
+ }
2649
+ break;
2650
+ case 'min-filter':
2651
+ if (newValue !== null) {
2652
+ this.minFilter = parseEnum(newValue, minFilterModes, 'linear-mip-linear', name);
2653
+ }
2654
+ else {
2655
+ this.minFilter = null;
2656
+ }
2657
+ break;
2658
+ case 'mipmaps':
2659
+ if (newValue !== null) {
2660
+ this.mipmaps = parseBool(newValue, true);
2661
+ }
2662
+ else {
2663
+ this.mipmaps = null;
2664
+ }
2665
+ break;
2666
+ case 'srgb':
2667
+ if (newValue !== null) {
2668
+ this.srgb = parseBool(newValue, false);
2669
+ }
2670
+ else {
2671
+ this.srgb = null;
2672
+ }
2673
+ break;
2674
+ }
2675
+ }
2676
+ }
2677
+ customElements.define('pc-asset', AssetElement);
2678
+
2679
+ /**
2680
+ * Represents a component in the PlayCanvas engine.
2681
+ *
2682
+ * @category Components
2683
+ */
2277
2684
  class ComponentElement extends AsyncElement {
2278
2685
  _componentName;
2279
2686
  _enabled = true;
2280
2687
  _component = null;
2281
2688
  _appElement = null;
2689
+ /**
2690
+ * The element hosting this component, held so the host's readiness cycles can be observed
2691
+ * even after `closestEntity` would no longer resolve (during teardown).
2692
+ */
2693
+ _hostElement = null;
2694
+ /**
2695
+ * The listener re-applying this component when the host's readiness cycles. Held for
2696
+ * removal on disconnect.
2697
+ */
2698
+ _hostReadyListener = null;
2282
2699
  /**
2283
2700
  * Incremented on every connect and disconnect. connectedCallback captures the value on entry
2284
2701
  * and abandons itself wherever it resumes from an await if the value has moved on — so a
@@ -2306,6 +2723,37 @@ class ComponentElement extends AsyncElement {
2306
2723
  getInitialComponentData() {
2307
2724
  return {};
2308
2725
  }
2726
+ /**
2727
+ * Creates the component on the host's current entity, removing it first from a previous
2728
+ * entity that is still alive (a retargeted `<pc-node>` moves its decorations with it). When
2729
+ * the entity already has a component of this type — a glTF node arriving with its authored
2730
+ * `render` component, say — warns and leaves `component` null. The element-level warning is
2731
+ * load-bearing: the engine's own duplicate-addComponent warning is Debug-stripped from
2732
+ * production builds, which would otherwise leave a silent null.
2733
+ */
2734
+ _applyComponent() {
2735
+ const entity = this._hostElement?.entity ?? null;
2736
+ if (this._component && this._component.entity === entity) {
2737
+ return;
2738
+ }
2739
+ // A retarget leaves the previous component on a still-live entity - remove it so the
2740
+ // decoration follows the element, or vanishes with a dissolved binding. A destroyed
2741
+ // entity took its components with it.
2742
+ const previous = this._component;
2743
+ if (previous?.entity && previous.entity.c[this._componentName] === previous) {
2744
+ previous.entity.removeComponent(this._componentName);
2745
+ }
2746
+ this._component = null;
2747
+ if (!entity) {
2748
+ return;
2749
+ }
2750
+ if (entity.c[this._componentName]) {
2751
+ const label = this.id ? ` '${this.id}'` : '';
2752
+ console.warn(`${this.tagName.toLowerCase()}${label} - '${entity.name}' already has a '${this._componentName}' component - component not added`);
2753
+ return;
2754
+ }
2755
+ this._component = entity.addComponent(this._componentName, this.getInitialComponentData());
2756
+ }
2309
2757
  async _addComponent() {
2310
2758
  const generation = this._connectionGeneration;
2311
2759
  const entityElement = this.closestEntity;
@@ -2322,9 +2770,40 @@ class ComponentElement extends AsyncElement {
2322
2770
  if (generation !== this._connectionGeneration) {
2323
2771
  return;
2324
2772
  }
2325
- // Add the component to the entity
2326
- const data = this.getInitialComponentData();
2327
- this._component = entityElement.entity.addComponent(this._componentName, data);
2773
+ this._hostElement = entityElement;
2774
+ this._applyComponent();
2775
+ // Re-apply when the host's readiness cycles without this element disconnecting: a
2776
+ // `<pc-node>` rebinding after its model reloads or retargets, or a re-created entity.
2777
+ // The 'ready' event bubbles, so events from descendants pass through this host - only
2778
+ // the host's own cycles count. Readiness is cycled here too, so decorations one level
2779
+ // down re-apply the same way.
2780
+ this._hostReadyListener = (event) => {
2781
+ if (event.target !== this._hostElement) {
2782
+ return;
2783
+ }
2784
+ if (generation !== this._connectionGeneration) {
2785
+ return;
2786
+ }
2787
+ this._hostCycled();
2788
+ };
2789
+ entityElement.addEventListener('ready', this._hostReadyListener);
2790
+ }
2791
+ /**
2792
+ * Re-evaluates this component against the host's current entity: applied to a new entity,
2793
+ * moved from a still-live old one, or removed when the host no longer fronts an entity at
2794
+ * all. Readiness follows - it cycles with a re-application and stays unresolved while the
2795
+ * host is unbound. Called by the host-ready listener, and directly by a `<pc-node>`
2796
+ * dissolving its binding: the one transition that fires no ready event to ride.
2797
+ *
2798
+ * @internal
2799
+ */
2800
+ _hostCycled() {
2801
+ this._resetReady();
2802
+ this._applyComponent();
2803
+ if (this._hostElement?.entity) {
2804
+ this.initComponent();
2805
+ this._onReady();
2806
+ }
2328
2807
  }
2329
2808
  /**
2330
2809
  * Configures the newly added component. Overridden by subclasses whose setup goes beyond
@@ -2353,6 +2832,11 @@ class ComponentElement extends AsyncElement {
2353
2832
  disconnectedCallback() {
2354
2833
  // Invalidate any connectedCallback still suspended on an await
2355
2834
  this._connectionGeneration++;
2835
+ if (this._hostElement && this._hostReadyListener) {
2836
+ this._hostElement.removeEventListener('ready', this._hostReadyListener);
2837
+ }
2838
+ this._hostElement = null;
2839
+ this._hostReadyListener = null;
2356
2840
  // Remove the component when the element is disconnected. Skip this when the owning
2357
2841
  // application has already been destroyed — removing a <pc-app> disconnects it before
2358
2842
  // its children, taking the component systems with it.
@@ -3312,6 +3796,12 @@ customElements.define('pc-camera', CameraComponentElement);
3312
3796
  * The CollisionComponentElement interface also inherits the properties and methods of the
3313
3797
  * {@link HTMLElement} interface.
3314
3798
  *
3799
+ * For `type="mesh"`, the collision geometry defaults to the host entity's own render component
3800
+ * (its render asset) — a collider matching the visible mesh, which is what a mesh collider on a
3801
+ * glTF node means. The default resolves each time the component applies, so a `pc-node` that
3802
+ * retargets or rebinds picks up the new node's geometry. An entity with no asset-backed render
3803
+ * component warns, and the collider has no shape.
3804
+ *
3315
3805
  * @category Components
3316
3806
  */
3317
3807
  class CollisionComponentElement extends ComponentElement {
@@ -3339,6 +3829,29 @@ class CollisionComponentElement extends ComponentElement {
3339
3829
  type: this._type
3340
3830
  };
3341
3831
  }
3832
+ initComponent() {
3833
+ this._applyMeshGeometryDefault();
3834
+ }
3835
+ /**
3836
+ * Defaults a mesh collider's geometry to the host entity's own render component. The
3837
+ * engine's mesh collider only works with explicitly supplied geometry, and the element has
3838
+ * no attribute to supply it - so the host's visible geometry, the meaning a mesh collider
3839
+ * on a glTF node carries, fills the gap. Runs on every application (so a rebound `pc-node`
3840
+ * recomputes it) and on a runtime switch to `type="mesh"`; an explicitly assigned
3841
+ * `renderAsset` is never overwritten.
3842
+ */
3843
+ _applyMeshGeometryDefault() {
3844
+ const component = this.component;
3845
+ if (!component || this._type !== 'mesh' || component.renderAsset !== null) {
3846
+ return;
3847
+ }
3848
+ const asset = component.entity.render?.asset ?? null;
3849
+ if (asset === null) {
3850
+ console.warn(`pc-collision type="mesh" on '${component.entity.name}' found no asset-backed render component to take geometry from - collider has no shape`);
3851
+ return;
3852
+ }
3853
+ component.renderAsset = asset;
3854
+ }
3342
3855
  /**
3343
3856
  * Gets the underlying PlayCanvas collision component.
3344
3857
  * @returns The collision component.
@@ -3413,6 +3926,7 @@ class CollisionComponentElement extends ComponentElement {
3413
3926
  this._type = value;
3414
3927
  if (this.component) {
3415
3928
  this.component.type = value;
3929
+ this._applyMeshGeometryDefault();
3416
3930
  }
3417
3931
  }
3418
3932
  get type() {
@@ -5583,13 +6097,13 @@ class MaterialElement extends HTMLElement {
5583
6097
  }
5584
6098
  /**
5585
6099
  * @param slot - The material property to write.
5586
- * @param texture - The loaded texture.
6100
+ * @param texture - The loaded texture, applied with its sampler state untouched - anisotropy
6101
+ * and friends belong to the `pc-asset`'s texture options.
5587
6102
  */
5588
6103
  _applyMap(slot, texture) {
5589
6104
  if (!this.material)
5590
6105
  return;
5591
6106
  this.material[slot] = texture;
5592
- texture.anisotropy = 4;
5593
6107
  this._scheduleUpdate();
5594
6108
  }
5595
6109
  /**
@@ -9869,6 +10383,21 @@ customElements.define('pc-gsplat', GSplatComponentElement);
9869
10383
  * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-model/ | `<pc-model>`} elements.
9870
10384
  * The ModelElement interface also inherits the properties and methods of the
9871
10385
  * {@link HTMLElement} interface.
10386
+ *
10387
+ * The element becomes ready once its container asset has loaded and the instantiated hierarchy has
10388
+ * been added to the scene — `entity` is non-null by then. A failed load also settles readiness,
10389
+ * with `entity` remaining `null`: readiness means the load settled, not that it succeeded — listen
10390
+ * for `error`, or check `entity`, to tell the outcomes apart. Changing `asset` re-arms readiness
10391
+ * and instantiates anew, so a `ready()` obtained after the change resolves against the new
10392
+ * hierarchy. A `pc-model` outside a `pc-app`, or referencing an unknown asset id, warns and never
10393
+ * becomes ready.
10394
+ *
10395
+ * @fires {Event} load - Fired each time a container asset finishes instantiating, including
10396
+ * re-instantiation after `asset` changes. Does not bubble — listen on this element, or use a
10397
+ * capture-phase listener on an ancestor.
10398
+ * @fires {ErrorEvent} error - Fired when the container asset fails to load, with the engine's
10399
+ * error in `message`. Does not bubble. The element still becomes ready — readiness means the load
10400
+ * settled, not that it succeeded.
9872
10401
  */
9873
10402
  class ModelElement extends AsyncElement {
9874
10403
  _asset = '';
@@ -9881,11 +10410,12 @@ class ModelElement extends AsyncElement {
9881
10410
  */
9882
10411
  _loadGeneration = 0;
9883
10412
  /**
9884
- * The pending asset-load subscription of the current load, if it is waiting for its asset.
9885
- * Held so that whatever supersedes the load can detach the handler from the asset, rather
9886
- * than leave it registered until the asset loads (or forever, if it never does).
10413
+ * The pending asset subscriptions of the current load, if it is waiting for its asset. Held
10414
+ * so that whatever supersedes the load can detach the handlers from the asset, rather than
10415
+ * leave them registered until the asset settles (or forever, if it never does).
9887
10416
  */
9888
10417
  _loadHandle = null;
10418
+ _errorHandle = null;
9889
10419
  /**
9890
10420
  * The root entity of the instantiated model. `null` until the container asset has loaded
9891
10421
  * and been instantiated, and again once the element has been removed from the document.
@@ -9895,18 +10425,36 @@ class ModelElement extends AsyncElement {
9895
10425
  return this._entity;
9896
10426
  }
9897
10427
  connectedCallback() {
10428
+ // A model outside an application is inert and never becomes ready, so awaiting it hangs.
10429
+ // Warn rather than fail silently, naming the parent it requires, as every other misplaced
10430
+ // element does.
10431
+ if (!this.closestApp) {
10432
+ const label = this._asset ? ` '${this._asset}'` : '';
10433
+ console.warn(`pc-model${label} must be a descendant of pc-app - model not created`);
10434
+ return;
10435
+ }
9898
10436
  this._loadModel();
9899
- this._onReady();
9900
10437
  }
9901
10438
  disconnectedCallback() {
9902
10439
  this._loadGeneration++;
9903
- this._detachLoadHandler();
10440
+ this._detachLoadHandlers();
9904
10441
  this._unloadModel();
9905
10442
  this._resetReady();
9906
10443
  }
9907
- _detachLoadHandler() {
10444
+ _detachLoadHandlers() {
9908
10445
  this._loadHandle?.off();
9909
10446
  this._loadHandle = null;
10447
+ this._errorHandle?.off();
10448
+ this._errorHandle = null;
10449
+ }
10450
+ /**
10451
+ * Resolves readiness and dispatches the `load` event. Called once the instantiated hierarchy
10452
+ * has been parented — readiness means "in the scene graph", matching `pc-entity`, so a ready
10453
+ * model's entity always has world transforms.
10454
+ */
10455
+ _announceLoad() {
10456
+ this._onReady();
10457
+ this.dispatchEvent(new Event('load'));
9910
10458
  }
9911
10459
  _instantiate(container) {
9912
10460
  const generation = this._loadGeneration;
@@ -9928,6 +10476,7 @@ class ModelElement extends AsyncElement {
9928
10476
  return;
9929
10477
  }
9930
10478
  parentEntityElement.entity.addChild(entity);
10479
+ this._announceLoad();
9931
10480
  });
9932
10481
  }
9933
10482
  else {
@@ -9938,6 +10487,7 @@ class ModelElement extends AsyncElement {
9938
10487
  return;
9939
10488
  }
9940
10489
  appElement.app.root.addChild(entity);
10490
+ this._announceLoad();
9941
10491
  });
9942
10492
  }
9943
10493
  }
@@ -9946,15 +10496,29 @@ class ModelElement extends AsyncElement {
9946
10496
  this._unloadModel();
9947
10497
  // Supersede any load already in flight - only the newest load may instantiate
9948
10498
  const generation = ++this._loadGeneration;
9949
- this._detachLoadHandler();
9950
- const appElement = await this.closestApp?.ready();
10499
+ this._detachLoadHandlers();
10500
+ // Re-arm readiness so a waiter obtained after an asset change resolves against the new
10501
+ // hierarchy. A no-op on first connection, where readiness is still pending.
10502
+ this._resetReady();
10503
+ const appElement = this.closestApp;
10504
+ if (!appElement) {
10505
+ // Outside pc-app; connectedCallback already warned. Reached through the asset setter.
10506
+ return;
10507
+ }
10508
+ await appElement.ready();
9951
10509
  // The element may have been removed, or another load started, while we waited
9952
10510
  if (generation !== this._loadGeneration) {
9953
10511
  return;
9954
10512
  }
9955
- const app = appElement?.app;
10513
+ const app = appElement.app;
9956
10514
  const asset = AssetElement.get(this._asset);
9957
10515
  if (!asset) {
10516
+ // An empty id is a legitimate transient (the asset may be assigned later); a
10517
+ // non-empty one that resolves to nothing is a dead end - say so rather than staying
10518
+ // silently pending.
10519
+ if (this._asset) {
10520
+ console.warn(`pc-model could not find asset '${this._asset}' - model not created`);
10521
+ }
9958
10522
  return;
9959
10523
  }
9960
10524
  if (asset.loaded) {
@@ -9963,14 +10527,26 @@ class ModelElement extends AsyncElement {
9963
10527
  else {
9964
10528
  // The generation is re-checked even though a superseded handler is detached: the
9965
10529
  // detach relies on how the engine's event emitter treats removal, while the check
9966
- // holds on its own.
10530
+ // holds on its own. Whichever of load/error fires first detaches the other.
9967
10531
  this._loadHandle = asset.once('load', () => {
9968
- this._loadHandle = null;
10532
+ this._detachLoadHandlers();
9969
10533
  if (generation !== this._loadGeneration) {
9970
10534
  return;
9971
10535
  }
9972
10536
  this._instantiate(asset.resource);
9973
10537
  });
10538
+ this._errorHandle = asset.once('error', (err) => {
10539
+ this._detachLoadHandlers();
10540
+ if (generation !== this._loadGeneration) {
10541
+ return;
10542
+ }
10543
+ // A failed load settles readiness with a null entity, mirroring pc-asset:
10544
+ // readiness means the load settled, not that it succeeded.
10545
+ this.dispatchEvent(new ErrorEvent('error', {
10546
+ message: err instanceof Error ? err.message : String(err)
10547
+ }));
10548
+ this._onReady();
10549
+ });
9974
10550
  app.assets.load(asset);
9975
10551
  }
9976
10552
  }
@@ -10008,6 +10584,625 @@ class ModelElement extends AsyncElement {
10008
10584
  }
10009
10585
  customElements.define('pc-model', ModelElement);
10010
10586
 
10587
+ /**
10588
+ * Computes the Levenshtein distance between two strings, for near-miss suggestions in the
10589
+ * resolution warnings.
10590
+ *
10591
+ * @param a - The first string.
10592
+ * @param b - The second string.
10593
+ * @returns The edit distance.
10594
+ */
10595
+ const levenshtein = (a, b) => {
10596
+ const row = Array.from({ length: b.length + 1 }, (_, i) => i);
10597
+ for (let i = 1; i <= a.length; i++) {
10598
+ let previous = row[0];
10599
+ row[0] = i;
10600
+ for (let j = 1; j <= b.length; j++) {
10601
+ const current = row[j];
10602
+ row[j] = Math.min(row[j] + 1, row[j - 1] + 1, previous + (a[i - 1] === b[j - 1] ? 0 : 1));
10603
+ previous = current;
10604
+ }
10605
+ }
10606
+ return row[b.length];
10607
+ };
10608
+ /**
10609
+ * The NodeElement interface provides properties and methods for manipulating
10610
+ * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-node/ | `<pc-node>`}
10611
+ * elements. The NodeElement interface also inherits the properties and methods of the
10612
+ * {@link HTMLElement} interface.
10613
+ *
10614
+ * A `pc-node` is an override element: where `pc-entity` creates an entity, `pc-node` binds to a
10615
+ * node a `pc-model` loaded and declares overrides against the authored asset — components to
10616
+ * add, properties to change, content to attach. Attributes present apply as overrides; attributes
10617
+ * absent leave authored values untouched, and removing an attribute (or assigning `null` to the
10618
+ * matching property) restores the authored value.
10619
+ *
10620
+ * `name` selects among the host model's nodes (first match in depth-first order), nesting a
10621
+ * `pc-node` inside another scopes the search to that subtree, and `index` picks among identically
10622
+ * named matches. When `name` matches more than one node and no `index` is given, the element
10623
+ * warns and binds nothing.
10624
+ *
10625
+ * The element becomes ready once bound, and never while unresolved — a missing or ambiguous
10626
+ * name warns and records the failure in `state`, readiness stays unresolved, and descendants
10627
+ * wait with it.
10628
+ *
10629
+ * The pointer events below are dispatched by the containing `<pc-app>` element when the pointer
10630
+ * intersects the bound node's geometry, exactly as for `<pc-entity>`.
10631
+ *
10632
+ * @attribute {string} name - The name of the node to bind, resolved within the nearest ancestor
10633
+ * `pc-model` (or `pc-node`) once it has instantiated.
10634
+ * @attribute {number} index - Which match to bind when `name` matches more than one node,
10635
+ * 0-based in depth-first order. Optional for a unique match; required for an ambiguous one.
10636
+ * @attribute {boolean} enabled - Overrides the node's enabled state.
10637
+ * @attribute {string} position - Overrides the node's local position, as an "x y z" triple.
10638
+ * @attribute {string} rotation - Overrides the node's local rotation (Euler angles), as an
10639
+ * "x y z" triple.
10640
+ * @attribute {string} scale - Overrides the node's local scale, as an "x y z" triple.
10641
+ * @attribute {string} tags - Overrides the node's tags, separated by spaces or commas.
10642
+ * @attribute {string} onpointerenter - Script to run when the pointer moves onto the node.
10643
+ * @attribute {string} onpointerleave - Script to run when the pointer moves off the node.
10644
+ * @attribute {string} onpointermove - Script to run when the pointer moves over the node.
10645
+ * @attribute {string} onpointerdown - Script to run when a pointer button is pressed over the
10646
+ * node.
10647
+ * @attribute {string} onpointerup - Script to run when a pointer button is released over the
10648
+ * node.
10649
+ * @fires {PointerEvent} pointerenter - Fired when the pointer moves onto the node.
10650
+ * @fires {PointerEvent} pointerleave - Fired when the pointer moves off the node.
10651
+ * @fires {PointerEvent} pointermove - Fired when the pointer moves over the node.
10652
+ * @fires {PointerEvent} pointerdown - Fired when a pointer button is pressed over the node.
10653
+ * @fires {PointerEvent} pointerup - Fired when a pointer button is released over the node.
10654
+ */
10655
+ class NodeElement extends EntityBaseElement {
10656
+ _name = '';
10657
+ _index = null;
10658
+ _state = 'pending';
10659
+ _path = null;
10660
+ /**
10661
+ * The element whose entity roots this element's search: the nearest ancestor `pc-node`, or
10662
+ * failing that the nearest ancestor `pc-model`. Resolved on connection.
10663
+ */
10664
+ _host = null;
10665
+ /**
10666
+ * The listener following the host's binding cycles. Both host kinds announce each cycle
10667
+ * with a `ready` event — `pc-model` on every instantiation, `pc-node` on every bind.
10668
+ */
10669
+ _hostListener = null;
10670
+ /**
10671
+ * The subscription to the bound entity's destruction, detached on unbind so a retargeted
10672
+ * element cannot be reset by the eventual death of a node it no longer fronts.
10673
+ */
10674
+ _destroyHandle = null;
10675
+ /** The authored values displaced by this element's overrides, captured per property. */
10676
+ _authored = {};
10677
+ // Override values. `null` means "no override": the authored value stays in force.
10678
+ _enabled = null;
10679
+ _position = null;
10680
+ _rotation = null;
10681
+ _scale = null;
10682
+ _tags = null;
10683
+ /**
10684
+ * The binding state: `pending` until the host instantiates and `name` resolves, `bound`
10685
+ * once decorated, `missing`/`ambiguous`/`duplicate` when resolution failed (each also
10686
+ * warns). Useful for asserting a document's bindings programmatically.
10687
+ * @returns The binding state.
10688
+ */
10689
+ get state() {
10690
+ return this._state;
10691
+ }
10692
+ /**
10693
+ * The path of the bound node below the search root, `/`-separated, or `null` while not
10694
+ * bound.
10695
+ * @returns The bound node's path, or `null`.
10696
+ */
10697
+ get path() {
10698
+ return this._path;
10699
+ }
10700
+ connectedCallback() {
10701
+ const host = (this.parentElement?.closest('pc-model, pc-node') ?? null);
10702
+ if (!host) {
10703
+ const label = this._name ? ` '${this._name}'` : '';
10704
+ console.warn(`pc-node${label} must be a descendant of pc-model - node not bound`);
10705
+ return;
10706
+ }
10707
+ this._host = host;
10708
+ // Follow the host's binding cycles. `ready` bubbles, so cycles of elements nested under
10709
+ // the host pass through it - only the host's own count.
10710
+ this._hostListener = (event) => {
10711
+ if (event.target !== this._host) {
10712
+ return;
10713
+ }
10714
+ this._rebind();
10715
+ };
10716
+ host.addEventListener('ready', this._hostListener);
10717
+ // The host may already be instantiated (an element inserted after load binds immediately)
10718
+ this._rebind();
10719
+ }
10720
+ disconnectedCallback() {
10721
+ if (this._host && this._hostListener) {
10722
+ this._host.removeEventListener('ready', this._hostListener);
10723
+ }
10724
+ this._host = null;
10725
+ this._hostListener = null;
10726
+ // Removal reverts: the model owns the node, so the entity is left as authored. Children
10727
+ // clean up through their own disconnect behavior.
10728
+ this._unbind();
10729
+ this._state = 'pending';
10730
+ }
10731
+ /**
10732
+ * Re-resolves the binding against the host's current hierarchy: on connection, on a `name`
10733
+ * or `index` change, and on every host cycle (a model [re]instantiating, an enclosing
10734
+ * `pc-node` [re]binding). When re-resolution yields the entity already bound, the binding
10735
+ * is retained untouched — a redundant edit must not flicker overrides through a revert.
10736
+ */
10737
+ _rebind() {
10738
+ const hostEntity = this._host?.entity ?? null;
10739
+ if (!hostEntity || !this._name) {
10740
+ // Host not instantiated (or nothing to look up yet): return to pending. An assigned
10741
+ // name arriving later, or the host's next cycle, resolves it.
10742
+ this._unbind();
10743
+ this._state = 'pending';
10744
+ return;
10745
+ }
10746
+ const target = this._resolve(hostEntity);
10747
+ if (target && target === this._entity) {
10748
+ this._path = this._pathOf(target, hostEntity);
10749
+ return;
10750
+ }
10751
+ this._unbind();
10752
+ if (!target) {
10753
+ // _resolve warned and set the failure state
10754
+ return;
10755
+ }
10756
+ this._bind(target, hostEntity);
10757
+ }
10758
+ /**
10759
+ * Resolves `name` (and `index`) to an entity under `hostEntity`, warning and recording the
10760
+ * failure state when it cannot.
10761
+ *
10762
+ * @param hostEntity - The root of the search.
10763
+ * @returns The resolved entity, or `null`.
10764
+ */
10765
+ _resolve(hostEntity) {
10766
+ const matches = hostEntity.find((node) => node.name === this._name);
10767
+ if (matches.length === 0) {
10768
+ const closest = this._closestName(hostEntity);
10769
+ const hint = closest ? ` - closest match: '${closest}'` : '';
10770
+ console.warn(`pc-node '${this._name}' not found in ${this._describeHost()}${hint}`);
10771
+ this._state = 'missing';
10772
+ return null;
10773
+ }
10774
+ let target;
10775
+ if (this._index !== null) {
10776
+ if (this._index >= matches.length) {
10777
+ console.warn(`pc-node '${this._name}' index ${this._index} is out of range - ${matches.length} match(es) in ${this._describeHost()}`);
10778
+ this._state = 'missing';
10779
+ return null;
10780
+ }
10781
+ target = matches[this._index];
10782
+ }
10783
+ else if (matches.length > 1) {
10784
+ // Ambiguity binds nothing: a fallback guess performs side effects on the wrong
10785
+ // scene node, and would go wrong silently when a re-export introduces a duplicate
10786
+ // name. The candidates tell the author exactly what to write.
10787
+ const candidates = matches.map((m, i) => `[${i}] ${this._pathOf(m, hostEntity)}`).join(', ');
10788
+ console.warn(`pc-node '${this._name}' is ambiguous in ${this._describeHost()} - specify index: ${candidates}`);
10789
+ this._state = 'ambiguous';
10790
+ return null;
10791
+ }
10792
+ else {
10793
+ target = matches[0];
10794
+ }
10795
+ const owner = this.closestApp?.elementFromEntity(target);
10796
+ if (owner && owner !== this) {
10797
+ console.warn(`pc-node '${this._name}' resolves to a node already bound by another element - element ignored`);
10798
+ this._state = 'duplicate';
10799
+ return null;
10800
+ }
10801
+ return target;
10802
+ }
10803
+ /**
10804
+ * Binds `target`: registers it (making it a pick target), hooks its destruction, applies
10805
+ * this element's overrides, announces readiness and builds the deferred child subtree.
10806
+ *
10807
+ * @param target - The entity to bind.
10808
+ * @param hostEntity - The search root, for the path.
10809
+ */
10810
+ _bind(target, hostEntity) {
10811
+ this._entity = target;
10812
+ this._registerEntity(target);
10813
+ this._destroyHandle = target.once('destroy', this._onEntityDestroy, this);
10814
+ this._state = 'bound';
10815
+ this._path = this._pathOf(target, hostEntity);
10816
+ this._applyOverrides();
10817
+ this._onReady();
10818
+ this._buildChildren();
10819
+ }
10820
+ /**
10821
+ * Dissolves the current binding, restoring every authored value this element's overrides
10822
+ * displaced and removing the decorations this binding hosts: attachment entities are
10823
+ * destroyed (re-created against the next binding) and component decorations are removed
10824
+ * from the abandoned node. Both sweeps are scoped by `closestEntity`, so a still-bound
10825
+ * nested `pc-node` keeps its own decorations. Safe to call in any state.
10826
+ */
10827
+ _unbind() {
10828
+ const entity = this._entity;
10829
+ if (!entity) {
10830
+ return;
10831
+ }
10832
+ this._revertOverrides();
10833
+ // Attachment points anchor to the bound node, so they cannot outlive the binding. Each
10834
+ // destroyed entity resets its element, which the next _buildChildren re-creates.
10835
+ this.querySelectorAll('pc-entity').forEach((child) => {
10836
+ if (child.closestEntity === this) {
10837
+ child.entity?.destroy();
10838
+ }
10839
+ });
10840
+ this._destroyHandle?.off();
10841
+ this._destroyHandle = null;
10842
+ this._unregisterEntity(entity);
10843
+ this._entity = null;
10844
+ this._path = null;
10845
+ this._authored = {};
10846
+ // Component decorations come off through the same hook the host-ready cycle uses. A
10847
+ // dissolve that never rebinds fires no ready event, so the sweep is explicit - after
10848
+ // `_entity` is cleared, so the hook sees a host without an entity.
10849
+ this.querySelectorAll('*').forEach((child) => {
10850
+ if (child instanceof ComponentElement && child.closestEntity === this) {
10851
+ child._hostCycled();
10852
+ }
10853
+ });
10854
+ this._resetReady();
10855
+ }
10856
+ /**
10857
+ * Handles the destruction of the bound entity - its model unloading, reloading, or a script
10858
+ * destroying it. There is nothing to revert on a destroyed entity; the element returns to
10859
+ * pending and the host's next cycle re-resolves it.
10860
+ */
10861
+ _onEntityDestroy(entity) {
10862
+ this._destroyHandle = null;
10863
+ this._unregisterEntity(entity);
10864
+ this._entity = null;
10865
+ this._path = null;
10866
+ this._authored = {};
10867
+ this._state = 'pending';
10868
+ this._resetReady();
10869
+ }
10870
+ /**
10871
+ * Creates and parents the entities of child `pc-entity` elements - the attachment points.
10872
+ * Mirrors the runtime-insertion path in EntityElement.connectedCallback: children were
10873
+ * deferred while this host was unresolved (or reset when a previous binding dissolved), and
10874
+ * build here once it binds.
10875
+ */
10876
+ _buildChildren() {
10877
+ const app = this.closestApp?.app;
10878
+ if (!app) {
10879
+ return;
10880
+ }
10881
+ const childEntities = this.querySelectorAll('pc-entity');
10882
+ childEntities.forEach((child) => {
10883
+ child._createEntity(app);
10884
+ });
10885
+ childEntities.forEach((child) => {
10886
+ child._buildHierarchy(app);
10887
+ });
10888
+ }
10889
+ /**
10890
+ * Applies every override that is explicitly set, capturing the authored value it displaces.
10891
+ */
10892
+ _applyOverrides() {
10893
+ if (this._enabled !== null) {
10894
+ this.enabled = this._enabled;
10895
+ }
10896
+ if (this._position !== null) {
10897
+ this.position = this._position;
10898
+ }
10899
+ if (this._rotation !== null) {
10900
+ this.rotation = this._rotation;
10901
+ }
10902
+ if (this._scale !== null) {
10903
+ this.scale = this._scale;
10904
+ }
10905
+ if (this._tags !== null) {
10906
+ this.tags = this._tags;
10907
+ }
10908
+ }
10909
+ /**
10910
+ * Restores every authored value this element's overrides displaced. The override values
10911
+ * themselves are kept - they re-apply on the next binding.
10912
+ */
10913
+ _revertOverrides() {
10914
+ const entity = this._entity;
10915
+ const authored = this._authored;
10916
+ if (authored.enabled !== undefined) {
10917
+ entity.enabled = authored.enabled;
10918
+ }
10919
+ if (authored.position) {
10920
+ entity.setLocalPosition(authored.position);
10921
+ }
10922
+ if (authored.rotation) {
10923
+ entity.setLocalRotation(authored.rotation);
10924
+ }
10925
+ if (authored.scale) {
10926
+ entity.setLocalScale(authored.scale);
10927
+ }
10928
+ if (authored.tags) {
10929
+ entity.tags.clear();
10930
+ entity.tags.add(authored.tags);
10931
+ }
10932
+ this._authored = {};
10933
+ }
10934
+ /**
10935
+ * Renders the path of `node` below `root`, for the `path` property and the resolution
10936
+ * warnings.
10937
+ *
10938
+ * @param node - The node to describe.
10939
+ * @param root - The search root.
10940
+ * @returns The `/`-separated path.
10941
+ */
10942
+ _pathOf(node, root) {
10943
+ const parts = [];
10944
+ for (let current = node; current && current !== root; current = current.parent) {
10945
+ parts.unshift(current.name);
10946
+ }
10947
+ return parts.join('/') || node.name;
10948
+ }
10949
+ /**
10950
+ * Describes the search root for warnings: the model's asset id, or the enclosing node's
10951
+ * name.
10952
+ * @returns The description.
10953
+ */
10954
+ _describeHost() {
10955
+ if (this._host instanceof ModelElement) {
10956
+ return `model '${this._host.asset}'`;
10957
+ }
10958
+ return `pc-node '${this._host?.name ?? ''}' subtree`;
10959
+ }
10960
+ /**
10961
+ * Finds the node name nearest to the missing `name`, for the miss warning. The names are
10962
+ * already in hand from resolution, so the suggestion is nearly free.
10963
+ *
10964
+ * @param hostEntity - The root of the search.
10965
+ * @returns The closest name within an edit distance of 2, or `null`.
10966
+ */
10967
+ _closestName(hostEntity) {
10968
+ let best = null;
10969
+ let bestDistance = 3;
10970
+ hostEntity.find((node) => {
10971
+ const distance = levenshtein(this._name, node.name);
10972
+ if (distance < bestDistance) {
10973
+ bestDistance = distance;
10974
+ best = node.name;
10975
+ }
10976
+ return false;
10977
+ });
10978
+ return best;
10979
+ }
10980
+ /**
10981
+ * Sets the name of the node to bind. A change retargets: the current binding's overrides
10982
+ * revert and the new name resolves afresh. `name` on a `pc-node` is never a rename of the
10983
+ * authored node - it is only ever a reference.
10984
+ * @param value - The node name.
10985
+ */
10986
+ set name(value) {
10987
+ this._name = value;
10988
+ if (this.isConnected && this._host) {
10989
+ this._rebind();
10990
+ }
10991
+ }
10992
+ /**
10993
+ * Gets the name of the node to bind.
10994
+ * @returns The node name.
10995
+ */
10996
+ get name() {
10997
+ return this._name;
10998
+ }
10999
+ /**
11000
+ * Sets which match to bind when `name` matches more than one node, 0-based in depth-first
11001
+ * order. A change retargets, like `name`. `null` means unset - required when the name is
11002
+ * ambiguous, optional otherwise.
11003
+ * @param value - The match index, or `null`.
11004
+ */
11005
+ set index(value) {
11006
+ this._index = value;
11007
+ if (this.isConnected && this._host) {
11008
+ this._rebind();
11009
+ }
11010
+ }
11011
+ /**
11012
+ * Gets which match to bind.
11013
+ * @returns The match index, or `null` when unset.
11014
+ */
11015
+ get index() {
11016
+ return this._index;
11017
+ }
11018
+ /**
11019
+ * Sets the enabled override. `null` clears it, restoring the authored state.
11020
+ * @param value - The enabled state, or `null`.
11021
+ */
11022
+ set enabled(value) {
11023
+ this._enabled = value;
11024
+ const entity = this._state === 'bound' ? this._entity : null;
11025
+ if (!entity) {
11026
+ return;
11027
+ }
11028
+ if (value !== null) {
11029
+ this._authored.enabled ??= entity.enabled;
11030
+ entity.enabled = value;
11031
+ }
11032
+ else if (this._authored.enabled !== undefined) {
11033
+ entity.enabled = this._authored.enabled;
11034
+ delete this._authored.enabled;
11035
+ }
11036
+ }
11037
+ /**
11038
+ * Gets the enabled override.
11039
+ * @returns The enabled state, or `null` while no override is set.
11040
+ */
11041
+ get enabled() {
11042
+ return this._enabled;
11043
+ }
11044
+ /**
11045
+ * Sets the local position override. `null` clears it, restoring the authored position.
11046
+ * @param value - The position, or `null`.
11047
+ */
11048
+ set position(value) {
11049
+ this._position = value;
11050
+ const entity = this._state === 'bound' ? this._entity : null;
11051
+ if (!entity) {
11052
+ return;
11053
+ }
11054
+ if (value !== null) {
11055
+ this._authored.position ??= entity.getLocalPosition().clone();
11056
+ entity.setLocalPosition(value);
11057
+ }
11058
+ else if (this._authored.position) {
11059
+ entity.setLocalPosition(this._authored.position);
11060
+ delete this._authored.position;
11061
+ }
11062
+ }
11063
+ /**
11064
+ * Gets the local position override.
11065
+ * @returns The position, or `null` while no override is set.
11066
+ */
11067
+ get position() {
11068
+ return this._position;
11069
+ }
11070
+ /**
11071
+ * Sets the local rotation override, as Euler angles in degrees. `null` clears it, restoring
11072
+ * the authored rotation.
11073
+ * @param value - The rotation, or `null`.
11074
+ */
11075
+ set rotation(value) {
11076
+ this._rotation = value;
11077
+ const entity = this._state === 'bound' ? this._entity : null;
11078
+ if (!entity) {
11079
+ return;
11080
+ }
11081
+ if (value !== null) {
11082
+ // The authored rotation is cached as a quaternion: it restores exactly, where a
11083
+ // round trip through Euler angles need not.
11084
+ this._authored.rotation ??= entity.getLocalRotation().clone();
11085
+ entity.setLocalEulerAngles(value);
11086
+ }
11087
+ else if (this._authored.rotation) {
11088
+ entity.setLocalRotation(this._authored.rotation);
11089
+ delete this._authored.rotation;
11090
+ }
11091
+ }
11092
+ /**
11093
+ * Gets the local rotation override.
11094
+ * @returns The rotation, or `null` while no override is set.
11095
+ */
11096
+ get rotation() {
11097
+ return this._rotation;
11098
+ }
11099
+ /**
11100
+ * Sets the local scale override. `null` clears it, restoring the authored scale.
11101
+ * @param value - The scale, or `null`.
11102
+ */
11103
+ set scale(value) {
11104
+ this._scale = value;
11105
+ const entity = this._state === 'bound' ? this._entity : null;
11106
+ if (!entity) {
11107
+ return;
11108
+ }
11109
+ if (value !== null) {
11110
+ this._authored.scale ??= entity.getLocalScale().clone();
11111
+ entity.setLocalScale(value);
11112
+ }
11113
+ else if (this._authored.scale) {
11114
+ entity.setLocalScale(this._authored.scale);
11115
+ delete this._authored.scale;
11116
+ }
11117
+ }
11118
+ /**
11119
+ * Gets the local scale override.
11120
+ * @returns The scale, or `null` while no override is set.
11121
+ */
11122
+ get scale() {
11123
+ return this._scale;
11124
+ }
11125
+ /**
11126
+ * Sets the tags override. `null` clears it, restoring the authored tags.
11127
+ * @param value - The tags, or `null`.
11128
+ */
11129
+ set tags(value) {
11130
+ this._tags = value;
11131
+ const entity = this._state === 'bound' ? this._entity : null;
11132
+ if (!entity) {
11133
+ return;
11134
+ }
11135
+ if (value !== null) {
11136
+ this._authored.tags ??= entity.tags.list().slice();
11137
+ entity.tags.clear();
11138
+ entity.tags.add(value);
11139
+ }
11140
+ else if (this._authored.tags) {
11141
+ entity.tags.clear();
11142
+ entity.tags.add(this._authored.tags);
11143
+ delete this._authored.tags;
11144
+ }
11145
+ }
11146
+ /**
11147
+ * Gets the tags override.
11148
+ * @returns The tags, or `null` while no override is set.
11149
+ */
11150
+ get tags() {
11151
+ return this._tags;
11152
+ }
11153
+ static get observedAttributes() {
11154
+ return ['enabled', 'index', 'name', 'position', 'rotation', 'scale', 'tags', ...POINTER_ATTRIBUTES];
11155
+ }
11156
+ attributeChangedCallback(name, _oldValue, newValue) {
11157
+ switch (name) {
11158
+ case 'enabled':
11159
+ this.enabled = newValue === null ? null : parseBool(newValue, true);
11160
+ break;
11161
+ case 'index':
11162
+ if (newValue === null) {
11163
+ this.index = null;
11164
+ }
11165
+ else {
11166
+ // Number('') is 0, which would make index="" silently mean the first match
11167
+ const index = newValue.trim() === '' ? NaN : Number(newValue);
11168
+ if (!Number.isInteger(index) || index < 0) {
11169
+ // Invalid values are treated as absent: under ambiguity that means
11170
+ // unbound, the fail-safe direction.
11171
+ console.warn(`pc-node index '${newValue}' is not a non-negative integer - treated as absent`);
11172
+ this.index = null;
11173
+ }
11174
+ else {
11175
+ this.index = index;
11176
+ }
11177
+ }
11178
+ break;
11179
+ case 'name':
11180
+ this.name = newValue ?? '';
11181
+ break;
11182
+ case 'position':
11183
+ this.position = newValue === null ? null : parseVec3(newValue, Vec3.ZERO, name);
11184
+ break;
11185
+ case 'rotation':
11186
+ this.rotation = newValue === null ? null : parseVec3(newValue, Vec3.ZERO, name);
11187
+ break;
11188
+ case 'scale':
11189
+ this.scale = newValue === null ? null : parseVec3(newValue, Vec3.ONE, name);
11190
+ break;
11191
+ case 'tags':
11192
+ this.tags = newValue === null ? null : parseTags(newValue);
11193
+ break;
11194
+ case 'onpointerenter':
11195
+ case 'onpointerleave':
11196
+ case 'onpointerdown':
11197
+ case 'onpointerup':
11198
+ case 'onpointermove':
11199
+ this._updateInlineHandler(name, newValue);
11200
+ break;
11201
+ }
11202
+ }
11203
+ }
11204
+ customElements.define('pc-node', NodeElement);
11205
+
10011
11206
  /**
10012
11207
  * The SceneElement interface provides properties and methods for manipulating
10013
11208
  * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-scene/ | `<pc-scene>`} elements.
@@ -10280,7 +11475,6 @@ class SkyElement extends AsyncElement {
10280
11475
  return;
10281
11476
  const source = asset.resource;
10282
11477
  const skybox = EnvLighting.generateSkyboxCubemap(source);
10283
- skybox.anisotropy = 4;
10284
11478
  // This element owns what it generated (see _unloadSkybox) - replacing a skybox from an
10285
11479
  // earlier load must release it, not orphan it on the GPU
10286
11480
  this._scene.skybox?.destroy();
@@ -10529,5 +11723,5 @@ class SkyElement extends AsyncElement {
10529
11723
  }
10530
11724
  customElements.define('pc-sky', SkyElement);
10531
11725
 
10532
- export { AppElement, AssetElement, AsyncElement, ButtonComponentElement, CameraComponentElement, CollisionComponentElement, ComponentElement, ElementComponentElement, EntityElement, GSplatComponentElement, LayoutChildComponentElement, LayoutGroupComponentElement, LightComponentElement, ListenerComponentElement, MaterialElement, ModelElement, ModuleElement, ParticleSystemComponentElement, RenderComponentElement, RigidBodyComponentElement, SceneElement, ScreenComponentElement, ScriptComponentElement, ScriptElement, ScrollViewComponentElement, ScrollbarComponentElement, SkyElement, SoundComponentElement, SoundSlotElement, whenReady };
11726
+ export { AppElement, AssetElement, AsyncElement, ButtonComponentElement, CameraComponentElement, CollisionComponentElement, ComponentElement, ElementComponentElement, EntityBaseElement, EntityElement, GSplatComponentElement, LayoutChildComponentElement, LayoutGroupComponentElement, LightComponentElement, ListenerComponentElement, MaterialElement, ModelElement, ModuleElement, NodeElement, ParticleSystemComponentElement, RenderComponentElement, RigidBodyComponentElement, SceneElement, ScreenComponentElement, ScriptComponentElement, ScriptElement, ScrollViewComponentElement, ScrollbarComponentElement, SkyElement, SoundComponentElement, SoundSlotElement, whenReady };
10533
11727
  //# sourceMappingURL=pwc.mjs.map