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