@playcanvas/web-components 0.19.0 → 0.20.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 (38) hide show
  1. package/dist/app.d.cts +14 -0
  2. package/dist/app.d.ts +14 -0
  3. package/dist/components/button-component.d.cts +9 -5
  4. package/dist/components/button-component.d.ts +9 -5
  5. package/dist/components/joint-component.d.cts +24 -10
  6. package/dist/components/joint-component.d.ts +24 -10
  7. package/dist/components/script-component.d.cts +4 -2
  8. package/dist/components/script-component.d.ts +4 -2
  9. package/dist/components/script-instance.d.cts +14 -6
  10. package/dist/components/script-instance.d.ts +14 -6
  11. package/dist/components/scroll-view-component.d.cts +24 -12
  12. package/dist/components/scroll-view-component.d.ts +24 -12
  13. package/dist/components/scrollbar-component.d.cts +6 -3
  14. package/dist/components/scrollbar-component.d.ts +6 -3
  15. package/dist/custom-elements.json +21 -21
  16. package/dist/parse.d.cts +7 -2
  17. package/dist/parse.d.ts +7 -2
  18. package/dist/pwc.cjs +406 -128
  19. package/dist/pwc.cjs.map +1 -1
  20. package/dist/pwc.js +406 -128
  21. package/dist/pwc.js.map +1 -1
  22. package/dist/pwc.min.js +1 -1
  23. package/dist/pwc.min.js.map +1 -1
  24. package/dist/pwc.min.mjs +1 -1
  25. package/dist/pwc.min.mjs.map +1 -1
  26. package/dist/pwc.mjs +406 -128
  27. package/dist/pwc.mjs.map +1 -1
  28. package/dist/vscode.html-custom-data.json +10 -10
  29. package/dist/web-types.json +20 -20
  30. package/package.json +3 -3
  31. package/src/app.ts +76 -41
  32. package/src/components/button-component.ts +18 -10
  33. package/src/components/joint-component.ts +29 -15
  34. package/src/components/script-component.ts +25 -12
  35. package/src/components/script-instance.ts +14 -6
  36. package/src/components/scroll-view-component.ts +49 -29
  37. package/src/components/scrollbar-component.ts +13 -8
  38. package/src/parse.ts +213 -16
package/dist/pwc.cjs CHANGED
@@ -587,8 +587,13 @@ const CSS_COLORS = {
587
587
  * - `parseBool` and `parseTags` take no attribute name, because every value is valid for them and
588
588
  * so they never warn.
589
589
  *
590
- * `getEntity` is the exception: it resolves a reference to a live entity rather than parsing a
591
- * literal, and returns `null` instead of falling back to a default.
590
+ * `findEntityElement` and `getEntity` are the exceptions: they resolve a reference rather than
591
+ * parsing a literal, and return `null` instead of falling back to a default. A reference
592
+ * beginning with `#` is a document-wide selector (an element id, or any selector rooted in one);
593
+ * anything else is an entity name, resolved lexically through the entity hierarchy first and
594
+ * against the document after — never as a selector or an id. They also do not warn - what an
595
+ * unresolved reference means depends on the element holding it - so elements report through
596
+ * `resolveEntity`, which takes that meaning as parameters.
592
597
  */
593
598
  /**
594
599
  * Splits an attribute value into exactly `count` numeric components. Returns `null` when the
@@ -835,31 +840,212 @@ const parseVec4 = (value, defaultValue, attribute) => {
835
840
  return new playcanvas.Vec4(components);
836
841
  };
837
842
  /**
838
- * Resolves a reference string to the {@link Entity} backing a `<pc-entity>` element. The reference
839
- * can be a CSS selector (e.g. `#my-id`, `pc-entity[name="Foo"]`), a bare element id, or a bare
840
- * entity name. Returns `null` if no matching element (or backing entity) is found.
843
+ * Runs querySelector, absorbing the SyntaxError an unparseable selector throws - references are
844
+ * arbitrary author text, so a lookup must fail to `null`, never throw.
841
845
  *
842
- * @param ref - The reference string to resolve.
843
- * @returns The resolved entity, or `null`.
844
- * @internal
846
+ * @param selector - The selector to query.
847
+ * @returns The matched element, or `null`.
845
848
  */
846
- const getEntity = (ref) => {
847
- if (!ref) {
849
+ const query = (selector) => {
850
+ try {
851
+ return document.querySelector(selector);
852
+ }
853
+ catch {
848
854
  return null;
849
855
  }
850
- let element = null;
851
- // Try the reference as a CSS selector. An invalid selector (e.g. a bare name containing
852
- // spaces) throws, in which case we fall back to id/name lookups below.
856
+ };
857
+ /**
858
+ * Runs a lookup against one scope, checking the scope element itself before its subtree a
859
+ * reference deep in a cloned prefab must be able to name the prefab's root. Absorbs the
860
+ * SyntaxError of an invalid selector like {@link query}: escaping quotes and backslashes does not
861
+ * make arbitrary text a valid CSS string (a reference containing a newline still throws), so a
862
+ * lookup must fail to `null`, never throw.
863
+ *
864
+ * @param scope - The element whose inclusive subtree to search.
865
+ * @param selector - The selector to query.
866
+ * @returns The matched element, or `null`.
867
+ */
868
+ const queryScope = (scope, selector) => {
853
869
  try {
854
- element = document.querySelector(ref);
870
+ return scope.matches(selector) ? scope : scope.querySelector(selector);
855
871
  }
856
872
  catch {
857
- element = null;
873
+ return null;
858
874
  }
875
+ };
876
+ /**
877
+ * Reads the entity a resolved element is backing, through the `entity` accessor every
878
+ * entity-fronting element exposes. `null` for no element, and for an element backing nothing.
879
+ *
880
+ * @param element - The element to read, or `null`.
881
+ * @returns The backing entity, or `null`.
882
+ */
883
+ const entityOf = (element) => {
884
+ return element?.entity ?? null;
885
+ };
886
+ /**
887
+ * The elements that front an entity: what a bare name can resolve to, and the scopes of the
888
+ * lexical name lookup.
889
+ */
890
+ const ENTITY_KINDS = ['pc-entity', 'pc-model', 'pc-node'];
891
+ /**
892
+ * The entity-fronting elements as one selector, for the scope walk.
893
+ */
894
+ const ENTITY_SCOPES = ENTITY_KINDS.join(', ');
895
+ /**
896
+ * Resolves a reference string to the element it names. The grammar is closed — every reference
897
+ * has exactly one interpretation:
898
+ *
899
+ * - A reference beginning with `#` is a document-wide CSS selector — an element id (`#body`), or
900
+ * any selector rooted in one (`#hud pc-entity`). It is authoritative: the name lookup never
901
+ * runs for it, so an unusually named entity cannot shadow it.
902
+ * - Any other reference is the name of an entity-fronting element (`<pc-entity>`, `<pc-model>` or
903
+ * `<pc-node>` — for a node, the glTF node name it binds), and nothing else. A bare reference is
904
+ * never interpreted as a selector or an element id, so adding or renaming elements can never
905
+ * change which form it takes.
906
+ *
907
+ * When `from` is supplied, a name resolves lexically first: the closest entity-fronting
908
+ * ancestor's inclusive subtree, then each outer entity-fronting ancestor, then the containing
909
+ * `<pc-app>`, then the document. This is what lets a `<template>` prefab reference its own
910
+ * entities by name — every clone resolves within itself before a document-wide lookup could reach
911
+ * an earlier clone — provided the prefab has a single entity-fronting root to be the enclosing
912
+ * scope.
913
+ *
914
+ * Separate from {@link getEntity} so a caller reporting a failure can tell the causes apart
915
+ * ({@link unresolvedCause} words them): nothing in the document matches the reference, or
916
+ * something matches but is not backing an entity (yet, or ever).
917
+ *
918
+ * @param ref - The reference string to resolve.
919
+ * @param from - The element resolving the reference, whose entity-fronting ancestors scope the
920
+ * name lookup. Omitted, the name lookup is document-wide only.
921
+ * @returns The matched element, or `null`.
922
+ * @internal
923
+ */
924
+ const findEntityElement = (ref, from) => {
925
+ if (!ref) {
926
+ return null;
927
+ }
928
+ // A '#' reference is document-wide and bypasses the name lookup entirely - an entity named
929
+ // '#body' must never shadow the element whose id is 'body'.
930
+ if (ref.startsWith('#')) {
931
+ return query(ref);
932
+ }
933
+ // The name lands inside a quoted CSS string, so its quotes and backslashes are escaped -
934
+ // a name like `say "hi"` must resolve, not turn the lookup into a SyntaxError.
935
+ const escaped = ref.replace(/["\\]/g, '\\$&');
936
+ const nameSelector = ENTITY_KINDS.map(kind => `${kind}[name="${escaped}"]`).join(', ');
937
+ if (from) {
938
+ let scope = from.parentElement?.closest(ENTITY_SCOPES);
939
+ while (scope) {
940
+ const element = queryScope(scope, nameSelector);
941
+ if (element) {
942
+ return element;
943
+ }
944
+ scope = scope.parentElement?.closest(ENTITY_SCOPES);
945
+ }
946
+ const app = from.parentElement?.closest('pc-app');
947
+ if (app) {
948
+ const element = queryScope(app, nameSelector);
949
+ if (element) {
950
+ return element;
951
+ }
952
+ }
953
+ }
954
+ return query(nameSelector);
955
+ };
956
+ /**
957
+ * Resolves a reference string to the {@link Entity} backing an entity-fronting element
958
+ * (`<pc-entity>`, `<pc-model>` or `<pc-node>`). The reference is a name — resolved lexically
959
+ * through the entity hierarchy first when `from` is supplied — or a document-wide `#` selector
960
+ * ({@link findEntityElement} details the grammar and order). Returns `null` if no matching
961
+ * element (or backing entity) is found.
962
+ *
963
+ * @param ref - The reference string to resolve.
964
+ * @param from - The element resolving the reference, whose entity-fronting ancestors scope the
965
+ * name lookup. Omitted, the name lookup is document-wide only.
966
+ * @returns The resolved entity, or `null`.
967
+ * @internal
968
+ */
969
+ const getEntity = (ref, from) => {
970
+ return entityOf(findEntityElement(ref, from));
971
+ };
972
+ /**
973
+ * Describes why a non-empty reference did not resolve, for a warning. Three causes, because they
974
+ * have three different fixes: nothing matches (usually a typo), the matched element is not backing
975
+ * an entity yet (usually timing - a `pc-node` whose asset has not loaded - so resolving again
976
+ * later can work), or the matched element can never back one (the reference points at the wrong
977
+ * element, so only correcting it can). Capability is the `entity` accessor every entity-backing
978
+ * element inherits from EntityBaseElement.
979
+ *
980
+ * @param element - The element the reference matched, or `null` when nothing did.
981
+ * @returns The cause, phrased to follow `could not resolve ... -`.
982
+ * @internal
983
+ */
984
+ const unresolvedCause = (element) => {
859
985
  if (!element) {
860
- element = document.getElementById(ref) ?? document.querySelector(`pc-entity[name="${ref}"]`);
986
+ return 'nothing in the document matches it';
861
987
  }
862
- return element?.entity ?? null;
988
+ const tag = `<${element.tagName.toLowerCase()}>`;
989
+ return 'entity' in element
990
+ ? `${tag} matches it but is not backing an entity yet`
991
+ : `${tag} matches it but cannot back an entity`;
992
+ };
993
+ /**
994
+ * Builds the migration pointer for a bare reference that names nothing but matches the id of an
995
+ * entity-fronting element - it was almost certainly meant as an id, so point at the form that
996
+ * expresses it, escaped so the suggestion actually parses as a selector (an id like `a:b` must
997
+ * be written `#a\:b`). Empty when the reference is already a `#` form, matches no id, or the id
998
+ * belongs to an element that could never back an entity - suggesting it would only trade this
999
+ * warning for the wrong-target one.
1000
+ *
1001
+ * @param ref - The unresolved reference.
1002
+ * @param prefix - Text the suggested form must carry in the caller's syntax (e.g. `entity:`).
1003
+ * @returns The advice sentence, or an empty string.
1004
+ * @internal
1005
+ */
1006
+ const idHint = (ref, prefix = '') => {
1007
+ const match = !ref.startsWith('#') && document.getElementById(ref);
1008
+ return match && 'entity' in match
1009
+ ? `A bare reference is a name - write '${prefix}#${CSS.escape(ref)}' to reference the element with that id.`
1010
+ : '';
1011
+ };
1012
+ /**
1013
+ * Resolves a reference string to the {@link Entity} backing an entity-fronting element, scoped to
1014
+ * the resolving element ({@link findEntityElement} details the order) and warning when a
1015
+ * non-empty reference does not resolve - otherwise the reference fails silently, invisible
1016
+ * except through the behavior it should have driven. The message names which of the three causes
1017
+ * ({@link unresolvedCause}) it hit, and advises reassigning later only when that can work.
1018
+ *
1019
+ * An empty reference stays silent: it is the unset state of an optional attribute, and on some
1020
+ * elements (`pc-joint` `entity-b`, `pc-button` `image`) a documented value of its own.
1021
+ *
1022
+ * @param ref - The reference string to resolve.
1023
+ * @param from - The element resolving the reference; scopes the lookup and names the message.
1024
+ * @param attribute - The attribute being resolved, for the message.
1025
+ * @param consequence - What the unresolved reference means for the element, for the message.
1026
+ * @returns The resolved entity, or `null`.
1027
+ * @internal
1028
+ */
1029
+ const resolveEntity = (ref, from, attribute, consequence) => {
1030
+ if (!ref) {
1031
+ return null;
1032
+ }
1033
+ const element = findEntityElement(ref, from);
1034
+ const entity = entityOf(element);
1035
+ if (!entity) {
1036
+ let advice = `Assign ${attribute} again once the entity exists.`;
1037
+ if (element && !('entity' in element)) {
1038
+ advice = `Point ${attribute} at a pc-entity, pc-model or pc-node instead.`;
1039
+ }
1040
+ else if (!element) {
1041
+ const hint = idHint(ref);
1042
+ if (hint) {
1043
+ advice = hint;
1044
+ }
1045
+ }
1046
+ console.warn(`${from.tagName.toLowerCase()} could not resolve ${attribute} '${ref}' - ${unresolvedCause(element)} - ${consequence}. ${advice}`);
1047
+ }
1048
+ return entity;
863
1049
  };
864
1050
 
865
1051
  /**
@@ -1015,6 +1201,12 @@ class AppElement extends AsyncElement {
1015
1201
  * click count that `detail` carries. `null` until a click has fired.
1016
1202
  */
1017
1203
  _lastClick = null;
1204
+ /**
1205
+ * Serializes dispatch of the discrete synthesized events (pointerdown, pointerup, click),
1206
+ * whose picks resolve in GPU order, not canvas-event order. Replaced on teardown, so a pick
1207
+ * that never resolves cannot stall the dispatches of a later boot.
1208
+ */
1209
+ _dispatchChain = Promise.resolve();
1018
1210
  _app = null;
1019
1211
  _loadProgress = 0;
1020
1212
  /**
@@ -1361,9 +1553,8 @@ class AppElement extends AsyncElement {
1361
1553
  _pickerCreate() {
1362
1554
  const { width, height } = this.app.graphicsDevice;
1363
1555
  this._picker = new playcanvas.Picker(this.app, width, height);
1364
- // Create bound handlers but don't attach them yet. The handlers pick asynchronously, so
1365
- // each is wrapped to discard the promise - a listener must not return one, and nothing
1366
- // awaits the result.
1556
+ // Create bound handlers but don't attach them yet. The move handler is async, so it is
1557
+ // wrapped to discard the promise - a listener must not return one.
1367
1558
  const listener = (handler) => {
1368
1559
  return (event) => {
1369
1560
  handler.call(this, event);
@@ -1399,6 +1590,8 @@ class AppElement extends AsyncElement {
1399
1590
  this._downPicks.clear();
1400
1591
  this._clickListened = false;
1401
1592
  this._lastClick = null;
1593
+ // Replace the chain: a pick that never resolves must not stall a later boot's dispatches
1594
+ this._dispatchChain = Promise.resolve();
1402
1595
  }
1403
1596
  /**
1404
1597
  * Registers the element that fronts an entity. Called by EntityElement when it creates its
@@ -1601,9 +1794,22 @@ class AppElement extends AsyncElement {
1601
1794
  newHoverEntity.dispatchEvent(new PointerEvent('pointermove', event));
1602
1795
  }
1603
1796
  }
1604
- async _onPointerDown(event) {
1797
+ /**
1798
+ * Appends a dispatch step to {@link _dispatchChain}. Must be called synchronously from the
1799
+ * canvas event handler - the order of appends is what carries canvas-event order. A step
1800
+ * that rejects is reported and released, so the steps queued behind it still dispatch.
1801
+ *
1802
+ * @param step - The dispatch work to run once every earlier step has finished.
1803
+ */
1804
+ _chainDispatch(step) {
1805
+ this._dispatchChain = this._dispatchChain.then(step).catch((error) => {
1806
+ console.error(error);
1807
+ });
1808
+ }
1809
+ _onPointerDown(event) {
1605
1810
  if (!this._picker || !this.app)
1606
1811
  return;
1812
+ // Picks stay concurrent - only the dispatch of the results is serialized
1607
1813
  const pick = this._pickNode(event);
1608
1814
  // A click concludes on the matching pointerup, which needs to know what the press
1609
1815
  // picked. Primary button only - the only button a click can conclude from - and only
@@ -1612,51 +1818,61 @@ class AppElement extends AsyncElement {
1612
1818
  if (this._clickListened && event.button === 0) {
1613
1819
  this._downPicks.set(event.pointerId, pick);
1614
1820
  }
1615
- const node = await pick;
1616
- if (!this._picker)
1617
- return; // the element disconnected while the pick was in flight
1618
- const entityElement = this._elementWithListener(node, 'pointerdown');
1619
- if (entityElement) {
1620
- entityElement.dispatchEvent(new PointerEvent('pointerdown', event));
1621
- }
1821
+ this._chainDispatch(async () => {
1822
+ const node = await pick;
1823
+ if (!this._picker)
1824
+ return; // the element disconnected while the pick was in flight
1825
+ const entityElement = this._elementWithListener(node, 'pointerdown');
1826
+ if (entityElement) {
1827
+ entityElement.dispatchEvent(new PointerEvent('pointerdown', event));
1828
+ }
1829
+ });
1622
1830
  }
1623
- async _onPointerUp(event) {
1831
+ _onPointerUp(event) {
1624
1832
  if (!this._picker || !this.app)
1625
1833
  return;
1626
1834
  // The press pick this release may conclude as a click. Claimed synchronously, so the
1627
1835
  // entry is gone before any other event for this pointer can be handled.
1628
1836
  const downPick = this._downPicks.get(event.pointerId);
1629
1837
  this._downPicks.delete(event.pointerId);
1630
- const node = await this._pickNode(event);
1631
- if (!this._picker)
1632
- return; // the element disconnected while the pick was in flight
1633
- const entityElement = this._elementWithListener(node, 'pointerup');
1634
- if (entityElement) {
1635
- entityElement.dispatchEvent(new PointerEvent('pointerup', event));
1636
- }
1838
+ const pick = this._pickNode(event);
1839
+ this._chainDispatch(async () => {
1840
+ const node = await pick;
1841
+ if (!this._picker)
1842
+ return; // the element disconnected while the pick was in flight
1843
+ const entityElement = this._elementWithListener(node, 'pointerup');
1844
+ if (entityElement) {
1845
+ entityElement.dispatchEvent(new PointerEvent('pointerup', event));
1846
+ }
1847
+ });
1637
1848
  // A click fires where the DOM fires it: at the nearest common inclusive ancestor of
1638
- // what the press and the release picked, for the primary button only. The press pick
1639
- // may still be in flight - a quick tap resolves in pick order, not event order.
1849
+ // what the press and the release picked, for the primary button only. Appended after
1850
+ // the release's own step, so it dispatches after the pointerup that concludes it.
1640
1851
  if (!downPick || event.button !== 0)
1641
1852
  return;
1642
- const downNode = await downPick;
1643
- if (!this._picker)
1644
- return;
1645
- const clickElement = this._elementWithListener(commonAncestor(downNode, node), 'click');
1646
- if (clickElement) {
1647
- const click = new PointerEvent('click', event);
1648
- // The init above copied pointerup's `detail`, which the Pointer Events spec fixes
1649
- // at 0 - but click is exempt: its detail is the click count, chained here as the
1650
- // platform chains it (same target, within the double-click window). Overridden
1651
- // with defineProperty because an event instance used as an init dict cannot have
1652
- // single fields replaced.
1653
- const time = performance.now();
1654
- const last = this._lastClick;
1655
- const count = last && last.element === clickElement && time - last.time <= CLICK_CHAIN_MS ? last.count + 1 : 1;
1656
- this._lastClick = { element: clickElement, time, count };
1657
- Object.defineProperty(click, 'detail', { value: count });
1658
- clickElement.dispatchEvent(click);
1659
- }
1853
+ this._chainDispatch(async () => {
1854
+ // A rejected pick was already reported by the press or release step that awaited it;
1855
+ // here it just means no click can conclude.
1856
+ const picked = await Promise.all([downPick, pick]).catch(() => null);
1857
+ if (!picked || !this._picker)
1858
+ return;
1859
+ const [downNode, upNode] = picked;
1860
+ const clickElement = this._elementWithListener(commonAncestor(downNode, upNode), 'click');
1861
+ if (clickElement) {
1862
+ const click = new PointerEvent('click', event);
1863
+ // The init above copied pointerup's `detail`, which the Pointer Events spec fixes
1864
+ // at 0 - but click is exempt: its detail is the click count, chained here as the
1865
+ // platform chains it (same target, within the double-click window). Overridden
1866
+ // with defineProperty because an event instance used as an init dict cannot have
1867
+ // single fields replaced.
1868
+ const time = performance.now();
1869
+ const last = this._lastClick;
1870
+ const count = last && last.element === clickElement && time - last.time <= CLICK_CHAIN_MS ? last.count + 1 : 1;
1871
+ this._lastClick = { element: clickElement, time, count };
1872
+ Object.defineProperty(click, 'detail', { value: count });
1873
+ clickElement.dispatchEvent(click);
1874
+ }
1875
+ });
1660
1876
  }
1661
1877
  /**
1662
1878
  * Attaches exactly the canvas listeners the tree's current element listeners need, and
@@ -4720,7 +4936,9 @@ class ButtonComponentElement extends ComponentElement {
4720
4936
  };
4721
4937
  // The image entity defaults to the button's own entity (which carries the image element)
4722
4938
  // when no explicit reference is provided.
4723
- const imageEntity = this._image ? getEntity(this._image) : this.closestEntity?.entity;
4939
+ const imageEntity = this._image
4940
+ ? resolveEntity(this._image, this, 'image', 'reference ignored')
4941
+ : this.closestEntity?.entity;
4724
4942
  if (imageEntity) {
4725
4943
  data.imageEntity = imageEntity;
4726
4944
  }
@@ -4763,21 +4981,27 @@ class ButtonComponentElement extends ComponentElement {
4763
4981
  return this._active;
4764
4982
  }
4765
4983
  /**
4766
- * Sets the reference (CSS selector, element id or entity name) to the `<pc-entity>` whose image
4767
- * element is used for visual transitions. Defaults to the button's own entity — inside a
4768
- * `<pc-model>`, that is the model's host entity, so supply an explicit reference to target a
4769
- * UI entity instead.
4984
+ * Sets the reference (a `pc-entity`, `pc-model` or `pc-node` name, or a document-wide `#`
4985
+ * selector) to the entity whose image element is used for visual transitions. An exact name
4986
+ * resolves against the nearest enclosing entity first, then outward, then the document.
4987
+ * Defaults to the button's own entity — inside a `<pc-model>`, that is the model's host
4988
+ * entity, so supply an explicit reference to target a UI entity instead. A non-empty
4989
+ * reference that does not resolve warns and is ignored.
4770
4990
  * @param value - The image entity reference.
4771
4991
  */
4772
4992
  set image(value) {
4773
4993
  this._image = value;
4774
- const entity = getEntity(value);
4775
- if (this.component && entity) {
4776
- this.component.imageEntity = entity;
4994
+ if (this.component) {
4995
+ const entity = resolveEntity(value, this, 'image', 'reference ignored');
4996
+ if (entity) {
4997
+ this.component.imageEntity = entity;
4998
+ }
4777
4999
  }
4778
5000
  }
4779
5001
  /**
4780
- * Gets the reference to the `<pc-entity>` whose image element is used for visual transitions.
5002
+ * Gets the reference (a `pc-entity`, `pc-model` or `pc-node` name, or a document-wide `#`
5003
+ * selector) to the entity whose image element is used for visual transitions, or empty for
5004
+ * the button's own entity.
4781
5005
  * @returns The image entity reference.
4782
5006
  */
4783
5007
  get image() {
@@ -6505,7 +6729,12 @@ customElements.define('pc-element', ElementComponentElement);
6505
6729
  * primary axis: a hinge rotates about it, a slider translates along it and a ball joint twists
6506
6730
  * about it. The constrained bodies are referenced by `entity-a` and `entity-b`, both of which need
6507
6731
  * a rigid body component; leaving `entity-b` empty constrains `entity-a` to a fixed point in world
6508
- * space. The underlying engine component is in alpha, so its API may change.
6732
+ * space. A reference can name any entity-fronting element `<pc-entity>`, `<pc-model>` or
6733
+ * `<pc-node>`, so a ragdoll can join a model's own skeleton nodes by name — and a name resolves
6734
+ * against the nearest enclosing entity first, then outward through the entity hierarchy, then the
6735
+ * document, while a `#` selector resolves document-wide. A `<template>` prefab with one
6736
+ * entity-fronting root can therefore wire its joints by name and stay self-contained when cloned.
6737
+ * The underlying engine component is in alpha, so its API may change.
6509
6738
  *
6510
6739
  * @elementSummary The `<pc-joint>` element constrains two rigid bodies to each other — a hinged
6511
6740
  * door, a swinging chain, a sliding drawer. Its entity's transform is the joint frame, and
@@ -6659,8 +6888,8 @@ class JointComponentElement extends ComponentElement {
6659
6888
  breakImpulse: this._breakImpulse,
6660
6889
  enableCollision: this._enableCollision,
6661
6890
  enableLimits: this._enableLimits,
6662
- entityA: getEntity(this._entityA),
6663
- entityB: getEntity(this._entityB),
6891
+ entityA: resolveEntity(this._entityA, this, 'entity-a', 'constraint not created'),
6892
+ entityB: resolveEntity(this._entityB, this, 'entity-b', 'constraint not created'),
6664
6893
  limits: this._limits,
6665
6894
  linearDamping: this._linearDamping,
6666
6895
  linearEquilibrium: this._linearEquilibrium,
@@ -6915,39 +7144,48 @@ class JointComponentElement extends ComponentElement {
6915
7144
  return this._enableLimits;
6916
7145
  }
6917
7146
  /**
6918
- * Sets the reference (CSS selector, element id or entity name) to the `<pc-entity>` providing
6919
- * the first constrained body. The reference resolves when it is set, so an entity created
6920
- * later is picked up by setting the attribute again.
7147
+ * Sets the reference (a `pc-entity`, `pc-model` or `pc-node` name, or a document-wide `#`
7148
+ * selector) to the element providing the first constrained body. An exact name resolves
7149
+ * against the nearest enclosing entity first, then outward, then the document. The reference
7150
+ * resolves when it is set, so an entity created later is picked up by setting the attribute
7151
+ * again. A non-empty reference that does not resolve warns, naming which of the two causes it
7152
+ * hit.
6921
7153
  * @param value - The first body's entity reference.
6922
7154
  */
6923
7155
  set entityA(value) {
6924
7156
  this._entityA = value;
6925
7157
  if (this.component) {
6926
- this.component.entityA = getEntity(value);
7158
+ this.component.entityA = resolveEntity(value, this, 'entity-a', 'constraint not created');
6927
7159
  }
6928
7160
  }
6929
7161
  /**
6930
- * Gets the reference to the `<pc-entity>` providing the first constrained body.
7162
+ * Gets the reference (a `pc-entity`, `pc-model` or `pc-node` name, or a document-wide `#`
7163
+ * selector) to the element providing the first constrained body.
6931
7164
  * @returns The first body's entity reference.
6932
7165
  */
6933
7166
  get entityA() {
6934
7167
  return this._entityA;
6935
7168
  }
6936
7169
  /**
6937
- * Sets the reference (CSS selector, element id or entity name) to the `<pc-entity>` providing
6938
- * the second constrained body, or empty to constrain the first body to a fixed point in world
6939
- * space. The reference resolves when it is set, so an entity created later is picked up by
6940
- * setting the attribute again.
7170
+ * Sets the reference (a `pc-entity`, `pc-model` or `pc-node` name, or a document-wide `#`
7171
+ * selector) to the element providing the second constrained body, or empty to constrain the
7172
+ * first body to a fixed point in world space. An exact name resolves against the nearest
7173
+ * enclosing entity first, then outward, then the document. The reference resolves when it is
7174
+ * set, so an entity created later is picked up by setting the attribute again. A non-empty
7175
+ * reference that does not resolve warns; an empty one is the documented world-space case and
7176
+ * stays silent.
6941
7177
  * @param value - The second body's entity reference.
6942
7178
  */
6943
7179
  set entityB(value) {
6944
7180
  this._entityB = value;
6945
7181
  if (this.component) {
6946
- this.component.entityB = getEntity(value);
7182
+ this.component.entityB = resolveEntity(value, this, 'entity-b', 'constraint not created');
6947
7183
  }
6948
7184
  }
6949
7185
  /**
6950
- * Gets the reference to the `<pc-entity>` providing the second constrained body.
7186
+ * Gets the reference (a `pc-entity`, `pc-model` or `pc-node` name, or a document-wide `#`
7187
+ * selector) to the element providing the second constrained body, or empty for the
7188
+ * world-space case.
6951
7189
  * @returns The second body's entity reference.
6952
7190
  */
6953
7191
  get entityB() {
@@ -11321,7 +11559,7 @@ class ScrollbarComponentElement extends ComponentElement {
11321
11559
  value: this._value,
11322
11560
  handleSize: this._handleSize
11323
11561
  };
11324
- const handle = getEntity(this._handle);
11562
+ const handle = resolveEntity(this._handle, this, 'handle', 'reference ignored');
11325
11563
  if (handle) {
11326
11564
  data.handleEntity = handle;
11327
11565
  }
@@ -11387,19 +11625,24 @@ class ScrollbarComponentElement extends ComponentElement {
11387
11625
  return this._handleSize;
11388
11626
  }
11389
11627
  /**
11390
- * Sets the reference (CSS selector, element id or entity name) to the `<pc-entity>` used as the
11391
- * scrollbar handle.
11628
+ * Sets the reference (a `pc-entity`, `pc-model` or `pc-node` name, or a document-wide `#`
11629
+ * selector) to the entity used as the scrollbar handle. An exact name resolves against the
11630
+ * nearest enclosing entity first, then outward, then the document. A non-empty reference that
11631
+ * does not resolve warns and is ignored.
11392
11632
  * @param value - The handle entity reference.
11393
11633
  */
11394
11634
  set handle(value) {
11395
11635
  this._handle = value;
11396
- const entity = getEntity(value);
11397
- if (this.component && entity) {
11398
- this.component.handleEntity = entity;
11636
+ if (this.component) {
11637
+ const entity = resolveEntity(value, this, 'handle', 'reference ignored');
11638
+ if (entity) {
11639
+ this.component.handleEntity = entity;
11640
+ }
11399
11641
  }
11400
11642
  }
11401
11643
  /**
11402
- * Gets the reference to the `<pc-entity>` used as the scrollbar handle.
11644
+ * Gets the reference (a `pc-entity`, `pc-model` or `pc-node` name, or a document-wide `#`
11645
+ * selector) to the entity used as the scrollbar handle.
11403
11646
  * @returns The handle entity reference.
11404
11647
  */
11405
11648
  get handle() {
@@ -11481,19 +11724,19 @@ class ScrollViewComponentElement extends ComponentElement {
11481
11724
  horizontalScrollbarVisibility: visibilities.get(this._horizontalScrollbarVisibility),
11482
11725
  verticalScrollbarVisibility: visibilities.get(this._verticalScrollbarVisibility)
11483
11726
  };
11484
- const viewport = getEntity(this._viewport);
11727
+ const viewport = resolveEntity(this._viewport, this, 'viewport', 'reference ignored');
11485
11728
  if (viewport) {
11486
11729
  data.viewportEntity = viewport;
11487
11730
  }
11488
- const content = getEntity(this._content);
11731
+ const content = resolveEntity(this._content, this, 'content', 'reference ignored');
11489
11732
  if (content) {
11490
11733
  data.contentEntity = content;
11491
11734
  }
11492
- const horizontalScrollbar = getEntity(this._horizontalScrollbar);
11735
+ const horizontalScrollbar = resolveEntity(this._horizontalScrollbar, this, 'horizontal-scrollbar', 'reference ignored');
11493
11736
  if (horizontalScrollbar) {
11494
11737
  data.horizontalScrollbarEntity = horizontalScrollbar;
11495
11738
  }
11496
- const verticalScrollbar = getEntity(this._verticalScrollbar);
11739
+ const verticalScrollbar = resolveEntity(this._verticalScrollbar, this, 'vertical-scrollbar', 'reference ignored');
11497
11740
  if (verticalScrollbar) {
11498
11741
  data.verticalScrollbarEntity = verticalScrollbar;
11499
11742
  }
@@ -11669,76 +11912,96 @@ class ScrollViewComponentElement extends ComponentElement {
11669
11912
  return this._verticalScrollbarVisibility;
11670
11913
  }
11671
11914
  /**
11672
- * Sets the reference (CSS selector, element id or entity name) to the `<pc-entity>` used as the
11673
- * viewport, which clips the content to the scroll view's bounds.
11915
+ * Sets the reference (a `pc-entity`, `pc-model` or `pc-node` name, or a document-wide `#`
11916
+ * selector) to the entity used as the viewport, which clips the content to the scroll view's
11917
+ * bounds. An exact name resolves against the nearest enclosing entity first, then outward,
11918
+ * then the document. A non-empty reference that does not resolve warns and is ignored.
11674
11919
  * @param value - The viewport entity reference.
11675
11920
  */
11676
11921
  set viewport(value) {
11677
11922
  this._viewport = value;
11678
- const entity = getEntity(value);
11679
- if (this.component && entity) {
11680
- this.component.viewportEntity = entity;
11923
+ if (this.component) {
11924
+ const entity = resolveEntity(value, this, 'viewport', 'reference ignored');
11925
+ if (entity) {
11926
+ this.component.viewportEntity = entity;
11927
+ }
11681
11928
  }
11682
11929
  }
11683
11930
  /**
11684
- * Gets the reference to the `<pc-entity>` used as the viewport.
11931
+ * Gets the reference (a `pc-entity`, `pc-model` or `pc-node` name, or a document-wide `#`
11932
+ * selector) to the entity used as the viewport.
11685
11933
  * @returns The viewport entity reference.
11686
11934
  */
11687
11935
  get viewport() {
11688
11936
  return this._viewport;
11689
11937
  }
11690
11938
  /**
11691
- * Sets the reference (CSS selector, element id or entity name) to the `<pc-entity>` used as the
11692
- * content, which is moved as the scroll view is scrolled.
11939
+ * Sets the reference (a `pc-entity`, `pc-model` or `pc-node` name, or a document-wide `#`
11940
+ * selector) to the entity used as the content, which is moved as the scroll view is
11941
+ * scrolled. An exact name resolves against the nearest enclosing entity first, then outward,
11942
+ * then the document. A non-empty reference that does not resolve warns and is ignored.
11693
11943
  * @param value - The content entity reference.
11694
11944
  */
11695
11945
  set content(value) {
11696
11946
  this._content = value;
11697
- const entity = getEntity(value);
11698
- if (this.component && entity) {
11699
- this.component.contentEntity = entity;
11947
+ if (this.component) {
11948
+ const entity = resolveEntity(value, this, 'content', 'reference ignored');
11949
+ if (entity) {
11950
+ this.component.contentEntity = entity;
11951
+ }
11700
11952
  }
11701
11953
  }
11702
11954
  /**
11703
- * Gets the reference to the `<pc-entity>` used as the content.
11955
+ * Gets the reference (a `pc-entity`, `pc-model` or `pc-node` name, or a document-wide `#`
11956
+ * selector) to the entity used as the content.
11704
11957
  * @returns The content entity reference.
11705
11958
  */
11706
11959
  get content() {
11707
11960
  return this._content;
11708
11961
  }
11709
11962
  /**
11710
- * Sets the reference (CSS selector, element id or entity name) to the `<pc-entity>` containing
11711
- * the horizontal `<pc-scrollbar>`.
11963
+ * Sets the reference (a `pc-entity`, `pc-model` or `pc-node` name, or a document-wide `#`
11964
+ * selector) to the entity containing the horizontal `<pc-scrollbar>`. An exact name resolves
11965
+ * against the nearest enclosing entity first, then outward, then the document. A non-empty
11966
+ * reference that does not resolve warns and is ignored.
11712
11967
  * @param value - The horizontal scrollbar entity reference.
11713
11968
  */
11714
11969
  set horizontalScrollbar(value) {
11715
11970
  this._horizontalScrollbar = value;
11716
- const entity = getEntity(value);
11717
- if (this.component && entity) {
11718
- this.component.horizontalScrollbarEntity = entity;
11971
+ if (this.component) {
11972
+ const entity = resolveEntity(value, this, 'horizontal-scrollbar', 'reference ignored');
11973
+ if (entity) {
11974
+ this.component.horizontalScrollbarEntity = entity;
11975
+ }
11719
11976
  }
11720
11977
  }
11721
11978
  /**
11722
- * Gets the reference to the `<pc-entity>` containing the horizontal scrollbar.
11979
+ * Gets the reference (a `pc-entity`, `pc-model` or `pc-node` name, or a document-wide `#`
11980
+ * selector) to the entity containing the horizontal scrollbar.
11723
11981
  * @returns The horizontal scrollbar entity reference.
11724
11982
  */
11725
11983
  get horizontalScrollbar() {
11726
11984
  return this._horizontalScrollbar;
11727
11985
  }
11728
11986
  /**
11729
- * Sets the reference (CSS selector, element id or entity name) to the `<pc-entity>` containing
11730
- * the vertical `<pc-scrollbar>`.
11987
+ * Sets the reference (a `pc-entity`, `pc-model` or `pc-node` name, or a document-wide `#`
11988
+ * selector) to the entity containing the vertical `<pc-scrollbar>`. An exact name resolves
11989
+ * against the nearest enclosing entity first, then outward, then the document. A non-empty
11990
+ * reference that does not resolve warns and is ignored.
11731
11991
  * @param value - The vertical scrollbar entity reference.
11732
11992
  */
11733
11993
  set verticalScrollbar(value) {
11734
11994
  this._verticalScrollbar = value;
11735
- const entity = getEntity(value);
11736
- if (this.component && entity) {
11737
- this.component.verticalScrollbarEntity = entity;
11995
+ if (this.component) {
11996
+ const entity = resolveEntity(value, this, 'vertical-scrollbar', 'reference ignored');
11997
+ if (entity) {
11998
+ this.component.verticalScrollbarEntity = entity;
11999
+ }
11738
12000
  }
11739
12001
  }
11740
12002
  /**
11741
- * Gets the reference to the `<pc-entity>` containing the vertical scrollbar.
12003
+ * Gets the reference (a `pc-entity`, `pc-model` or `pc-node` name, or a document-wide `#`
12004
+ * selector) to the entity containing the vertical scrollbar.
11742
12005
  * @returns The vertical scrollbar entity reference.
11743
12006
  */
11744
12007
  get verticalScrollbar() {
@@ -11821,7 +12084,9 @@ customElements.define('pc-scroll-view', ScrollViewComponentElement);
11821
12084
  * Values are parsed according to the type of the attribute's current value — initially the
11822
12085
  * script's declared default (numbers, booleans, strings, Vec2/3/4, Color, Quat as Euler
11823
12086
  * angles) — and the `asset:`/`entity:`/`vec2:`/`vec3:`/`vec4:`/`color:` prefixes may be used
11824
- * to be explicit.
12087
+ * to be explicit. An `entity:` reference is an entity name — resolved against the nearest
12088
+ * enclosing entity first, then outward, then the document — or a document-wide `#` selector
12089
+ * (`entity:#id`); a bare value is always a name, never an element id.
11825
12090
  * - **The `attributes` JSON attribute**: an object supporting nested structures and attribute
11826
12091
  * names that collide with reserved HTML attribute names (e.g. `title`).
11827
12092
  *
@@ -11837,7 +12102,8 @@ customElements.define('pc-scroll-view', ScrollViewComponentElement);
11837
12102
  *
11838
12103
  * @elementSummary The `<pc-script-instance>` element attaches one script class, named by `name`, to
11839
12104
  * the entity of its parent `<pc-script>`. Its other attributes set script attributes of the same
11840
- * name, and `attributes` takes a JSON object instead. Must be a direct child of `<pc-script>`.
12105
+ * name, and `attributes` takes a JSON object instead. An `entity:` value is an entity name —
12106
+ * write `entity:#id` for an element id. Must be a direct child of `<pc-script>`.
11841
12107
  *
11842
12108
  * @fires {CustomEvent} scriptattributeschange - Fired when the script's attributes change. The
11843
12109
  * `detail` carries the new `attributes` object. Bubbles.
@@ -11857,9 +12123,11 @@ class ScriptInstanceElement extends AsyncElement {
11857
12123
  /**
11858
12124
  * Sets the attributes of the script as an object. Values are converted with the same rules
11859
12125
  * as the `attributes` attribute: `asset:`/`entity:` references and `vec2:`/`vec3:`/`vec4:`/
11860
- * `color:` prefixed strings are resolved, and a plain numeric array is converted to the
11861
- * type of the attribute it targets when that attribute currently holds a Vec2, Vec3, Vec4
11862
- * or Color.
12126
+ * `color:` prefixed strings are resolved (an entity name against the nearest enclosing
12127
+ * entity first, then outward, then the document or a document-wide `#` selector; a bare
12128
+ * value is always a name, never an element id), and a plain numeric array is converted to
12129
+ * the type of the attribute it targets when that attribute currently holds a Vec2, Vec3,
12130
+ * Vec4 or Color.
11863
12131
  * @param value - The attributes of the script.
11864
12132
  */
11865
12133
  set scriptAttributes(value) {
@@ -11870,7 +12138,10 @@ class ScriptInstanceElement extends AsyncElement {
11870
12138
  }));
11871
12139
  }
11872
12140
  /**
11873
- * Gets the attributes of the script.
12141
+ * Gets the attributes of the script as an object whose `asset:`, `entity:`, `vec2:`, `vec3:`,
12142
+ * `vec4:` and `color:` prefixed values are resolved when applied — an `entity:` value being
12143
+ * an entity name (nearest enclosing entity first, then outward, then the document) or a
12144
+ * document-wide `#` selector (`entity:#id`), never a bare element id.
11874
12145
  * @returns The attributes of the script.
11875
12146
  */
11876
12147
  get scriptAttributes() {
@@ -12081,18 +12352,23 @@ const assetConversion = (rest, raw) => {
12081
12352
  return raw;
12082
12353
  };
12083
12354
  /**
12084
- * Resolves an `entity:` prefix to the Entity backing a `pc-entity` element. The reference can be a
12085
- * CSS selector, an element id or an entity name.
12355
+ * Resolves an `entity:` prefix to the Entity backing a `pc-entity`, `pc-model` or `pc-node`
12356
+ * element. The reference is a name resolved against the nearest enclosing entity first, then
12357
+ * outward, then the document — or a document-wide `#` selector. The failure warning names which
12358
+ * of the three causes ({@link unresolvedCause}) it hit.
12086
12359
  * @param rest - The entity reference.
12087
12360
  * @param raw - The raw value, returned unchanged when the reference does not resolve.
12361
+ * @param from - The element the value is declared under, which scopes the reference.
12088
12362
  * @returns The entity, or `raw`.
12089
12363
  */
12090
- const entityConversion = (rest, raw) => {
12091
- const entity = getEntity(rest);
12364
+ const entityConversion = (rest, raw, from) => {
12365
+ const entity = getEntity(rest, from);
12092
12366
  if (entity) {
12093
12367
  return entity;
12094
12368
  }
12095
- console.warn(`Unable to resolve '${raw}' in script attributes - no pc-entity found matching '${rest}'.`);
12369
+ const element = findEntityElement(rest, from);
12370
+ const hint = element ? '' : idHint(rest, 'entity:');
12371
+ console.warn(`Unable to resolve '${raw}' in script attributes - ${unresolvedCause(element)}.${hint ? ` ${hint}` : ''}`);
12096
12372
  return raw;
12097
12373
  };
12098
12374
  /**
@@ -12225,8 +12501,10 @@ class ScriptComponentElement extends ComponentElement {
12225
12501
  /**
12226
12502
  * Recursively converts raw attribute data into proper PlayCanvas types. Supported conversions:
12227
12503
  * - "asset:id" → the Asset created by the `pc-asset` element with that id
12228
- * - "entity:ref" → the Entity backing a `pc-entity` element. The reference can be a CSS
12229
- * selector, an element id or an entity name.
12504
+ * - "entity:ref" → the Entity backing a `pc-entity`, `pc-model` or `pc-node` element. The
12505
+ * reference is a name, resolved against this element's nearest enclosing entity first,
12506
+ * then outward, then the document — or a document-wide `#` selector (`entity:#id`). A bare
12507
+ * value is always a name, never an id.
12230
12508
  * - "vec2:1 2" → new Vec2(1, 2)
12231
12509
  * - "vec3:1 2 3" → new Vec3(1, 2, 3)
12232
12510
  * - "vec4:1 2 3 4" → new Vec4(1, 2, 3, 4)
@@ -12240,7 +12518,7 @@ class ScriptComponentElement extends ComponentElement {
12240
12518
  convertAttributes(item) {
12241
12519
  if (typeof item === 'string') {
12242
12520
  const match = matchConversion(item);
12243
- return match ? match.convert(match.rest, item) : item;
12521
+ return match ? match.convert(match.rest, item, this) : item;
12244
12522
  }
12245
12523
  if (Array.isArray(item)) {
12246
12524
  return item.map((element) => this.convertAttributes(element));