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