@playcanvas/web-components 0.18.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 (56) hide show
  1. package/dist/app.d.cts +38 -3
  2. package/dist/app.d.ts +38 -3
  3. package/dist/components/anim-clip.d.cts +0 -2
  4. package/dist/components/anim-clip.d.ts +0 -2
  5. package/dist/components/button-component.d.cts +9 -5
  6. package/dist/components/button-component.d.ts +9 -5
  7. package/dist/components/joint-component.d.cts +24 -10
  8. package/dist/components/joint-component.d.ts +24 -10
  9. package/dist/components/script-component.d.cts +4 -2
  10. package/dist/components/script-component.d.ts +4 -2
  11. package/dist/components/script-instance.d.cts +14 -6
  12. package/dist/components/script-instance.d.ts +14 -6
  13. package/dist/components/scroll-view-component.d.cts +24 -12
  14. package/dist/components/scroll-view-component.d.ts +24 -12
  15. package/dist/components/scrollbar-component.d.cts +6 -3
  16. package/dist/components/scrollbar-component.d.ts +6 -3
  17. package/dist/custom-elements.json +93 -23
  18. package/dist/entity-base.d.cts +4 -3
  19. package/dist/entity-base.d.ts +4 -3
  20. package/dist/entity.d.cts +8 -2
  21. package/dist/entity.d.ts +8 -2
  22. package/dist/model.d.cts +6 -0
  23. package/dist/model.d.ts +6 -0
  24. package/dist/node.d.cts +6 -0
  25. package/dist/node.d.ts +6 -0
  26. package/dist/parse.d.cts +7 -2
  27. package/dist/parse.d.ts +7 -2
  28. package/dist/pwc.cjs +706 -289
  29. package/dist/pwc.cjs.map +1 -1
  30. package/dist/pwc.js +706 -289
  31. package/dist/pwc.js.map +1 -1
  32. package/dist/pwc.min.js +1 -1
  33. package/dist/pwc.min.js.map +1 -1
  34. package/dist/pwc.min.mjs +1 -1
  35. package/dist/pwc.min.mjs.map +1 -1
  36. package/dist/pwc.mjs +706 -289
  37. package/dist/pwc.mjs.map +1 -1
  38. package/dist/scene.d.cts +17 -1
  39. package/dist/scene.d.ts +17 -1
  40. package/dist/vscode.html-custom-data.json +34 -14
  41. package/dist/web-types.json +78 -24
  42. package/package.json +3 -3
  43. package/src/app.ts +197 -87
  44. package/src/components/anim-clip.ts +0 -2
  45. package/src/components/button-component.ts +18 -10
  46. package/src/components/joint-component.ts +29 -15
  47. package/src/components/script-component.ts +25 -12
  48. package/src/components/script-instance.ts +14 -6
  49. package/src/components/scroll-view-component.ts +49 -29
  50. package/src/components/scrollbar-component.ts +13 -8
  51. package/src/entity-base.ts +27 -15
  52. package/src/entity.ts +11 -4
  53. package/src/model.ts +9 -2
  54. package/src/node.ts +9 -2
  55. package/src/parse.ts +213 -16
  56. package/src/scene.ts +33 -2
package/dist/pwc.mjs CHANGED
@@ -177,6 +177,136 @@ class WasmElement extends AsyncElement {
177
177
  }
178
178
  customElements.define('pc-wasm', WasmElement);
179
179
 
180
+ /**
181
+ * The event types the containing `<pc-app>` synthesizes on entity-fronting elements via picking:
182
+ * the `pointer*` events, plus `click` — which concludes a primary-button press and release, and
183
+ * is delivered as a `PointerEvent` exactly as modern browsers deliver native clicks.
184
+ * @internal
185
+ */
186
+ const SYNTHESIZED_EVENTS = [
187
+ 'pointerenter',
188
+ 'pointerleave',
189
+ 'pointerdown',
190
+ 'pointerup',
191
+ 'pointermove',
192
+ 'click'
193
+ ];
194
+ const SYNTHESIZED_EVENT_SET = new Set(SYNTHESIZED_EVENTS);
195
+ /**
196
+ * The attribute names of the inline event handlers (`onpointerdown`, `onclick`, ...), shared by
197
+ * every element that fronts an engine entity. Spread into `observedAttributes` by subclasses.
198
+ * @internal
199
+ */
200
+ const EVENT_ATTRIBUTES = SYNTHESIZED_EVENTS.map((type) => `on${type}`);
201
+ /**
202
+ * The base class for elements that front an engine {@link Entity}: `<pc-entity>` and
203
+ * `<pc-model>`, which create one, and `<pc-node>`, which binds to one inside a model's
204
+ * instantiated hierarchy. It carries what all of them need — the `entity` contract, registration
205
+ * with the owning application (which joins picked scene nodes back to elements by identity,
206
+ * never by name), and the pointer listener bookkeeping that lets the application lazily attach
207
+ * its canvas handlers.
208
+ */
209
+ class EntityBaseElement extends AsyncElement {
210
+ _entity = null;
211
+ /**
212
+ * The application element this entity is registered with, cached at registration time so the
213
+ * entity can be unregistered even once this element has left the DOM.
214
+ */
215
+ _appElement = null;
216
+ /**
217
+ * The event listeners registered on the element, by type.
218
+ */
219
+ _listeners = {};
220
+ /**
221
+ * The event types for which an inline handler attribute (`onpointerdown`, `onclick`, ...)
222
+ * is currently present.
223
+ */
224
+ _inlineHandlerTypes = new Set();
225
+ /**
226
+ * The PlayCanvas entity instance. `null` until the element is ready, and again once the
227
+ * entity is gone — await {@link whenReady} or the element's `ready()` promise before
228
+ * accessing it.
229
+ * @returns The entity instance, or `null`.
230
+ */
231
+ get entity() {
232
+ return this._entity;
233
+ }
234
+ /**
235
+ * Registers `entity` as this element's backing entity with the owning application, which
236
+ * joins engine nodes back to elements by identity (never by name).
237
+ *
238
+ * @param entity - The entity to register.
239
+ */
240
+ _registerEntity(entity) {
241
+ this._appElement = this.closestApp;
242
+ this._appElement?._registerEntityElement(entity, this);
243
+ }
244
+ /**
245
+ * Removes the registration for `entity`.
246
+ *
247
+ * @param entity - The entity to unregister.
248
+ */
249
+ _unregisterEntity(entity) {
250
+ this._appElement?._unregisterEntityElement(entity);
251
+ this._appElement = null;
252
+ }
253
+ /**
254
+ * Tracks whether an inline handler attribute is present. The browser itself compiles and
255
+ * runs these attributes — they are standard `GlobalEventHandlers`, so setting one replaces
256
+ * the previous handler and removing it removes the handler, exactly like `onclick` on any
257
+ * HTML element. But because they bypass {@link EventTarget.addEventListener}, the connect/disconnect
258
+ * bookkeeping that lets the application lazily attach its canvas pointer handlers must be
259
+ * kept in sync here.
260
+ *
261
+ * @param name - The attribute name (e.g. 'onpointerdown').
262
+ * @param value - The attribute value, or `null` when the attribute has been removed.
263
+ */
264
+ _updateInlineHandler(name, value) {
265
+ const type = name.substring(2);
266
+ const had = this._inlineHandlerTypes.has(type);
267
+ const has = value !== null;
268
+ if (has && !had) {
269
+ this._inlineHandlerTypes.add(type);
270
+ this.dispatchEvent(new CustomEvent(`${type}:connect`, { bubbles: true }));
271
+ }
272
+ else if (!has && had) {
273
+ this._inlineHandlerTypes.delete(type);
274
+ this.dispatchEvent(new CustomEvent(`${type}:disconnect`, { bubbles: true }));
275
+ }
276
+ }
277
+ addEventListener(type, listener, options) {
278
+ if (!this._listeners[type]) {
279
+ this._listeners[type] = [];
280
+ }
281
+ this._listeners[type].push(listener);
282
+ super.addEventListener(type, listener, options);
283
+ if (SYNTHESIZED_EVENT_SET.has(type)) {
284
+ this.dispatchEvent(new CustomEvent(`${type}:connect`, { bubbles: true }));
285
+ }
286
+ }
287
+ removeEventListener(type, listener, options) {
288
+ if (this._listeners[type]) {
289
+ this._listeners[type] = this._listeners[type].filter((l) => l !== listener);
290
+ }
291
+ super.removeEventListener(type, listener, options);
292
+ if (SYNTHESIZED_EVENT_SET.has(type)) {
293
+ this.dispatchEvent(new CustomEvent(`${type}:disconnect`, { bubbles: true }));
294
+ }
295
+ }
296
+ /**
297
+ * Whether the element has a listener for an event type, registered either with
298
+ * {@link EventTarget.addEventListener} or with the matching inline handler attribute. Read by the
299
+ * containing `<pc-app>` element to gate event synthesis.
300
+ *
301
+ * @param type - The event type.
302
+ * @returns Whether a listener is registered.
303
+ * @internal
304
+ */
305
+ _hasListeners(type) {
306
+ return Boolean(this._listeners[type]?.length) || this._inlineHandlerTypes.has(type);
307
+ }
308
+ }
309
+
180
310
  /** Covers the 0.2s opacity transition; jsdom never fires transitionend, so removal is timed. */
181
311
  const REMOVAL_DELAY_MS = 250;
182
312
  /**
@@ -455,8 +585,13 @@ const CSS_COLORS = {
455
585
  * - `parseBool` and `parseTags` take no attribute name, because every value is valid for them and
456
586
  * so they never warn.
457
587
  *
458
- * `getEntity` is the exception: it resolves a reference to a live entity rather than parsing a
459
- * 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.
460
595
  */
461
596
  /**
462
597
  * Splits an attribute value into exactly `count` numeric components. Returns `null` when the
@@ -703,41 +838,259 @@ const parseVec4 = (value, defaultValue, attribute) => {
703
838
  return new Vec4(components);
704
839
  };
705
840
  /**
706
- * Resolves a reference string to the {@link Entity} backing a `<pc-entity>` element. The reference
707
- * can be a CSS selector (e.g. `#my-id`, `pc-entity[name="Foo"]`), a bare element id, or a bare
708
- * 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.
709
843
  *
710
- * @param ref - The reference string to resolve.
711
- * @returns The resolved entity, or `null`.
712
- * @internal
844
+ * @param selector - The selector to query.
845
+ * @returns The matched element, or `null`.
713
846
  */
714
- const getEntity = (ref) => {
715
- if (!ref) {
847
+ const query = (selector) => {
848
+ try {
849
+ return document.querySelector(selector);
850
+ }
851
+ catch {
716
852
  return null;
717
853
  }
718
- let element = null;
719
- // Try the reference as a CSS selector. An invalid selector (e.g. a bare name containing
720
- // 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) => {
721
867
  try {
722
- element = document.querySelector(ref);
868
+ return scope.matches(selector) ? scope : scope.querySelector(selector);
723
869
  }
724
870
  catch {
725
- element = null;
871
+ return null;
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
+ }
726
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) => {
727
983
  if (!element) {
728
- element = document.getElementById(ref) ?? document.querySelector(`pc-entity[name="${ref}"]`);
984
+ return 'nothing in the document matches it';
729
985
  }
730
- 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;
731
1047
  };
732
1048
 
733
- /** The pointer event types the application synthesizes on `<pc-entity>` elements via picking. */
734
- const pointerEventTypes = ['pointermove', 'pointerdown', 'pointerup', 'pointerenter', 'pointerleave'];
735
1049
  /**
736
1050
  * The event types whose listeners make an element a hover target. Hover resolution walks past
737
1051
  * elements listening for none of them, so a silent element never swallows an ancestor's
738
1052
  * enter/leave pair.
739
1053
  */
740
1054
  const hoverEventTypes = ['pointerenter', 'pointerleave', 'pointermove'];
1055
+ /**
1056
+ * The canvas listeners each synthesized event type is driven by. Enter and leave are derived
1057
+ * from move picks. A click is concluded from the down/up pair, with pointercancel discarding a
1058
+ * press the browser takes back (for example a touch that becomes a scroll).
1059
+ */
1060
+ const canvasEventsFor = {
1061
+ pointermove: ['pointermove'],
1062
+ pointerenter: ['pointermove'],
1063
+ pointerleave: ['pointermove'],
1064
+ pointerdown: ['pointerdown'],
1065
+ pointerup: ['pointerup'],
1066
+ click: ['pointerdown', 'pointerup', 'pointercancel']
1067
+ };
1068
+ /**
1069
+ * How long after a click a further click on the same target still raises the click count that
1070
+ * `detail` carries, approximating the platform's double-click time.
1071
+ */
1072
+ const CLICK_CHAIN_MS = 500;
1073
+ /**
1074
+ * Finds the nearest common inclusive ancestor of two picked nodes - the node a click belongs to
1075
+ * when the press and the release picked different geometry, exactly as the DOM assigns a click
1076
+ * whose down and up have different targets.
1077
+ *
1078
+ * @param a - The node the press picked, or `null`.
1079
+ * @param b - The node the release picked, or `null`.
1080
+ * @returns The nearest common inclusive ancestor, or `null` when there is none.
1081
+ */
1082
+ const commonAncestor = (a, b) => {
1083
+ const ancestors = new Set();
1084
+ for (let node = a; node !== null; node = node.parent) {
1085
+ ancestors.add(node);
1086
+ }
1087
+ for (let node = b; node !== null; node = node.parent) {
1088
+ if (ancestors.has(node)) {
1089
+ return node;
1090
+ }
1091
+ }
1092
+ return null;
1093
+ };
741
1094
  /**
742
1095
  * Gives `pc-app` the sizing contract of a replaced element (`<video>`, `<img>`): a block-level
743
1096
  * box that the page's CSS sizes, defaulting to the canvas's own 300x150 intrinsic size, with the
@@ -822,21 +1175,36 @@ class AppElement extends AsyncElement {
822
1175
  */
823
1176
  _entityElements = new Map();
824
1177
  _picker = null;
825
- _hasPointerListeners = {
826
- pointerenter: false,
827
- pointerleave: false,
828
- pointerdown: false,
829
- pointerup: false,
830
- pointermove: false
831
- };
832
1178
  _hoveredEntity = null;
833
1179
  // Identifies the newest in-flight hover pick, so out-of-order results can be discarded
834
1180
  _pickToken = 0;
835
1181
  _pointerHandlers = {
836
1182
  pointermove: null,
837
1183
  pointerdown: null,
838
- pointerup: null
1184
+ pointerup: null,
1185
+ pointercancel: null
839
1186
  };
1187
+ /**
1188
+ * The pick of each pointer's primary-button press, keyed by pointerId and kept while a click
1189
+ * may still conclude it. The promise is stored rather than its result, so a release can
1190
+ * await a press pick that has not resolved yet. Entries are removed by the matching
1191
+ * pointerup or pointercancel, and only ever stored while some element listens for click -
1192
+ * which is also what keeps those two canvas listeners attached.
1193
+ */
1194
+ _downPicks = new Map();
1195
+ /** Whether any element in the tree listens for click. Maintained by _syncCanvasListeners. */
1196
+ _clickListened = false;
1197
+ /**
1198
+ * The previous click's target, time and count, for chaining successive clicks into the
1199
+ * click count that `detail` carries. `null` until a click has fired.
1200
+ */
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();
840
1208
  _app = null;
841
1209
  _loadProgress = 0;
842
1210
  /**
@@ -871,12 +1239,12 @@ class AppElement extends AsyncElement {
871
1239
  */
872
1240
  constructor() {
873
1241
  super();
874
- // Track pointer listeners being added to and removed from descendant entities.
875
- // Registered once here rather than on every boot - the handlers no-op while there is no
876
- // canvas, and a re-booted element must not stack a second set.
877
- pointerEventTypes.forEach((type) => {
878
- this.addEventListener(`${type}:connect`, () => this._onPointerListenerAdded(type));
879
- this.addEventListener(`${type}:disconnect`, () => this._onPointerListenerRemoved(type));
1242
+ // Track listeners for the synthesized events being added to and removed from descendant
1243
+ // entities. Registered once here rather than on every boot - the sync no-ops while there
1244
+ // is no canvas, and a re-booted element must not stack a second set.
1245
+ SYNTHESIZED_EVENTS.forEach((type) => {
1246
+ this.addEventListener(`${type}:connect`, () => this._syncCanvasListeners());
1247
+ this.addEventListener(`${type}:disconnect`, () => this._syncCanvasListeners());
880
1248
  });
881
1249
  }
882
1250
  async connectedCallback() {
@@ -1183,9 +1551,8 @@ class AppElement extends AsyncElement {
1183
1551
  _pickerCreate() {
1184
1552
  const { width, height } = this.app.graphicsDevice;
1185
1553
  this._picker = new Picker(this.app, width, height);
1186
- // Create bound handlers but don't attach them yet. The handlers pick asynchronously, so
1187
- // each is wrapped to discard the promise - a listener must not return one, and nothing
1188
- // 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.
1189
1556
  const listener = (handler) => {
1190
1557
  return (event) => {
1191
1558
  handler.call(this, event);
@@ -1194,15 +1561,13 @@ class AppElement extends AsyncElement {
1194
1561
  this._pointerHandlers.pointermove = listener(this._onPointerMove);
1195
1562
  this._pointerHandlers.pointerdown = listener(this._onPointerDown);
1196
1563
  this._pointerHandlers.pointerup = listener(this._onPointerUp);
1197
- // Attach canvas handlers for listeners registered before this boot (e.g. handlers
1198
- // created from onpointer* attributes when their elements were first upgraded, or
1564
+ this._pointerHandlers.pointercancel = (event) => {
1565
+ this._downPicks.delete(event.pointerId);
1566
+ };
1567
+ // Attach canvas listeners for element listeners registered before this boot (e.g.
1568
+ // handlers created from inline attributes when their elements were first upgraded, or
1199
1569
  // listeners carried over from before a re-boot)
1200
- pointerEventTypes.forEach((type) => {
1201
- const anyListeners = Array.from(this.querySelectorAll('pc-entity, pc-model, pc-node')).some((entity) => entity._hasListeners(type));
1202
- if (anyListeners) {
1203
- this._onPointerListenerAdded(type);
1204
- }
1205
- });
1570
+ this._syncCanvasListeners();
1206
1571
  }
1207
1572
  _pickerDestroy() {
1208
1573
  if (this._canvas) {
@@ -1217,15 +1582,14 @@ class AppElement extends AsyncElement {
1217
1582
  this._pointerHandlers = {
1218
1583
  pointermove: null,
1219
1584
  pointerdown: null,
1220
- pointerup: null
1221
- };
1222
- this._hasPointerListeners = {
1223
- pointerenter: false,
1224
- pointerleave: false,
1225
- pointerdown: false,
1226
- pointerup: false,
1227
- pointermove: false
1585
+ pointerup: null,
1586
+ pointercancel: null
1228
1587
  };
1588
+ this._downPicks.clear();
1589
+ this._clickListened = false;
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();
1229
1593
  }
1230
1594
  /**
1231
1595
  * Registers the element that fronts an entity. Called by EntityElement when it creates its
@@ -1428,51 +1792,116 @@ class AppElement extends AsyncElement {
1428
1792
  newHoverEntity.dispatchEvent(new PointerEvent('pointermove', event));
1429
1793
  }
1430
1794
  }
1431
- 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) {
1432
1808
  if (!this._picker || !this.app)
1433
1809
  return;
1434
- const node = await this._pickNode(event);
1435
- if (!this._picker)
1436
- return; // the element disconnected while the pick was in flight
1437
- const entityElement = this._elementWithListener(node, 'pointerdown');
1438
- if (entityElement) {
1439
- entityElement.dispatchEvent(new PointerEvent('pointerdown', event));
1440
- }
1810
+ // Picks stay concurrent - only the dispatch of the results is serialized
1811
+ const pick = this._pickNode(event);
1812
+ // A click concludes on the matching pointerup, which needs to know what the press
1813
+ // picked. Primary button only - the only button a click can conclude from - and only
1814
+ // while click is listened for, since it is the click mapping that keeps the pointerup
1815
+ // and pointercancel listeners attached to clean the entry up again.
1816
+ if (this._clickListened && event.button === 0) {
1817
+ this._downPicks.set(event.pointerId, pick);
1818
+ }
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
+ });
1441
1828
  }
1442
- async _onPointerUp(event) {
1829
+ _onPointerUp(event) {
1443
1830
  if (!this._picker || !this.app)
1444
1831
  return;
1445
- const node = await this._pickNode(event);
1446
- if (!this._picker)
1447
- return; // the element disconnected while the pick was in flight
1448
- const entityElement = this._elementWithListener(node, 'pointerup');
1449
- if (entityElement) {
1450
- entityElement.dispatchEvent(new PointerEvent('pointerup', event));
1451
- }
1452
- }
1453
- _onPointerListenerAdded(type) {
1454
- if (!this._hasPointerListeners[type] && this._canvas) {
1455
- this._hasPointerListeners[type] = true;
1456
- // For enter/leave events, we need the move handler
1457
- const handler = type === 'pointerenter' || type === 'pointerleave'
1458
- ? this._pointerHandlers.pointermove
1459
- : this._pointerHandlers[type];
1460
- if (handler) {
1461
- this._canvas.addEventListener(type === 'pointerenter' || type === 'pointerleave' ? 'pointermove' : type, handler);
1832
+ // The press pick this release may conclude as a click. Claimed synchronously, so the
1833
+ // entry is gone before any other event for this pointer can be handled.
1834
+ const downPick = this._downPicks.get(event.pointerId);
1835
+ this._downPicks.delete(event.pointerId);
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));
1462
1844
  }
1463
- }
1845
+ });
1846
+ // A click fires where the DOM fires it: at the nearest common inclusive ancestor of
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.
1849
+ if (!downPick || event.button !== 0)
1850
+ return;
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
+ });
1464
1874
  }
1465
- _onPointerListenerRemoved(type) {
1466
- const hasListeners = Array.from(this.querySelectorAll('pc-entity, pc-model, pc-node')).some((entity) => entity._hasListeners(type));
1467
- if (!hasListeners && this._canvas) {
1468
- this._hasPointerListeners[type] = false;
1469
- const handler = type === 'pointerenter' || type === 'pointerleave'
1470
- ? this._pointerHandlers.pointermove
1471
- : this._pointerHandlers[type];
1472
- if (handler) {
1473
- this._canvas.removeEventListener(type === 'pointerenter' || type === 'pointerleave' ? 'pointermove' : type, handler);
1875
+ /**
1876
+ * Attaches exactly the canvas listeners the tree's current element listeners need, and
1877
+ * detaches the rest. Recomputed whenever a listener connects or disconnects anywhere under
1878
+ * this element: several synthesized types can need the same canvas listener (enter, leave
1879
+ * and move all ride the move pick; click rides the down/up pair), so one type's removal
1880
+ * must not detach a listener another type still uses. Re-attaching an attached listener is
1881
+ * a no-op by EventTarget semantics, so no attach state is kept.
1882
+ */
1883
+ _syncCanvasListeners() {
1884
+ const canvas = this._canvas;
1885
+ if (!canvas)
1886
+ return; // not booted yet: _pickerCreate syncs once the handlers exist
1887
+ const elements = Array.from(this.querySelectorAll('pc-entity, pc-model, pc-node'));
1888
+ const needed = new Set();
1889
+ for (const type of SYNTHESIZED_EVENTS) {
1890
+ if (elements.some((element) => element._hasListeners(type))) {
1891
+ canvasEventsFor[type].forEach((canvasType) => needed.add(canvasType));
1474
1892
  }
1475
1893
  }
1894
+ this._clickListened = elements.some((element) => element._hasListeners('click'));
1895
+ Object.entries(this._pointerHandlers).forEach(([canvasType, handler]) => {
1896
+ if (!handler)
1897
+ return;
1898
+ if (needed.has(canvasType)) {
1899
+ canvas.addEventListener(canvasType, handler);
1900
+ }
1901
+ else {
1902
+ canvas.removeEventListener(canvasType, handler);
1903
+ }
1904
+ });
1476
1905
  }
1477
1906
  /**
1478
1907
  * Warns that a graphics option was written too late to have any effect. These options are read
@@ -1641,126 +2070,6 @@ class AppElement extends AsyncElement {
1641
2070
  }
1642
2071
  customElements.define('pc-app', AppElement);
1643
2072
 
1644
- /**
1645
- * The attribute names of the inline `onpointer*` event handlers, shared by every element that
1646
- * fronts an engine entity. Spread into `observedAttributes` by subclasses.
1647
- * @internal
1648
- */
1649
- const POINTER_ATTRIBUTES = [
1650
- 'onpointerenter',
1651
- 'onpointerleave',
1652
- 'onpointerdown',
1653
- 'onpointerup',
1654
- 'onpointermove'
1655
- ];
1656
- /**
1657
- * The base class for elements that front an engine {@link Entity}: `<pc-entity>` and
1658
- * `<pc-model>`, which create one, and `<pc-node>`, which binds to one inside a model's
1659
- * instantiated hierarchy. It carries what all of them need — the `entity` contract, registration
1660
- * with the owning application (which joins picked scene nodes back to elements by identity,
1661
- * never by name), and the pointer listener bookkeeping that lets the application lazily attach
1662
- * its canvas handlers.
1663
- */
1664
- class EntityBaseElement extends AsyncElement {
1665
- _entity = null;
1666
- /**
1667
- * The application element this entity is registered with, cached at registration time so the
1668
- * entity can be unregistered even once this element has left the DOM.
1669
- */
1670
- _appElement = null;
1671
- /**
1672
- * The pointer event listeners for the entity.
1673
- */
1674
- _listeners = {};
1675
- /**
1676
- * The event types for which an inline `onpointer*` attribute is currently present.
1677
- */
1678
- _inlineHandlerTypes = new Set();
1679
- /**
1680
- * The PlayCanvas entity instance. `null` until the element is ready, and again once the
1681
- * entity is gone — await {@link whenReady} or the element's `ready()` promise before
1682
- * accessing it.
1683
- * @returns The entity instance, or `null`.
1684
- */
1685
- get entity() {
1686
- return this._entity;
1687
- }
1688
- /**
1689
- * Registers `entity` as this element's backing entity with the owning application, which
1690
- * joins engine nodes back to elements by identity (never by name).
1691
- *
1692
- * @param entity - The entity to register.
1693
- */
1694
- _registerEntity(entity) {
1695
- this._appElement = this.closestApp;
1696
- this._appElement?._registerEntityElement(entity, this);
1697
- }
1698
- /**
1699
- * Removes the registration for `entity`.
1700
- *
1701
- * @param entity - The entity to unregister.
1702
- */
1703
- _unregisterEntity(entity) {
1704
- this._appElement?._unregisterEntityElement(entity);
1705
- this._appElement = null;
1706
- }
1707
- /**
1708
- * Tracks whether an inline `onpointer*` attribute is present. The browser itself compiles and
1709
- * runs these attributes — they are standard `GlobalEventHandlers`, so setting one replaces
1710
- * the previous handler and removing it removes the handler, exactly like `onclick` on any
1711
- * HTML element. But because they bypass {@link EventTarget.addEventListener}, the connect/disconnect
1712
- * bookkeeping that lets the application lazily attach its canvas pointer handlers must be
1713
- * kept in sync here.
1714
- *
1715
- * @param name - The attribute name (e.g. 'onpointerdown').
1716
- * @param value - The attribute value, or `null` when the attribute has been removed.
1717
- */
1718
- _updateInlineHandler(name, value) {
1719
- const type = name.substring(2);
1720
- const had = this._inlineHandlerTypes.has(type);
1721
- const has = value !== null;
1722
- if (has && !had) {
1723
- this._inlineHandlerTypes.add(type);
1724
- this.dispatchEvent(new CustomEvent(`${type}:connect`, { bubbles: true }));
1725
- }
1726
- else if (!has && had) {
1727
- this._inlineHandlerTypes.delete(type);
1728
- this.dispatchEvent(new CustomEvent(`${type}:disconnect`, { bubbles: true }));
1729
- }
1730
- }
1731
- addEventListener(type, listener, options) {
1732
- if (!this._listeners[type]) {
1733
- this._listeners[type] = [];
1734
- }
1735
- this._listeners[type].push(listener);
1736
- super.addEventListener(type, listener, options);
1737
- if (type.startsWith('pointer')) {
1738
- this.dispatchEvent(new CustomEvent(`${type}:connect`, { bubbles: true }));
1739
- }
1740
- }
1741
- removeEventListener(type, listener, options) {
1742
- if (this._listeners[type]) {
1743
- this._listeners[type] = this._listeners[type].filter((l) => l !== listener);
1744
- }
1745
- super.removeEventListener(type, listener, options);
1746
- if (type.startsWith('pointer')) {
1747
- this.dispatchEvent(new CustomEvent(`${type}:disconnect`, { bubbles: true }));
1748
- }
1749
- }
1750
- /**
1751
- * Whether the element has a listener for an event type, registered either with
1752
- * {@link EventTarget.addEventListener} or with the matching inline `onpointer*` attribute. Read by the
1753
- * containing `<pc-app>` element to gate pointer event synthesis.
1754
- *
1755
- * @param type - The event type.
1756
- * @returns Whether a listener is registered.
1757
- * @internal
1758
- */
1759
- _hasListeners(type) {
1760
- return Boolean(this._listeners[type]?.length) || this._inlineHandlerTypes.has(type);
1761
- }
1762
- }
1763
-
1764
2073
  /**
1765
2074
  * Creates and parents the entities of every descendant entity-owning element of `root`, in two
1766
2075
  * passes so that no parent's existence depends on document order. Called wherever a subtree could
@@ -2028,8 +2337,8 @@ class EntityOwnerElement extends EntityBaseElement {
2028
2337
  *
2029
2338
  * The pointer events below are dispatched by the containing `<pc-app>` element when the pointer
2030
2339
  * intersects this entity's geometry. They are only generated while the entity has a listener for
2031
- * them, registered either with {@link EventTarget.addEventListener} or with the matching inline `onpointer*`
2032
- * attribute.
2340
+ * them, registered either with {@link EventTarget.addEventListener} or with the matching inline
2341
+ * attribute (`onpointerdown`, `onclick`, ...).
2033
2342
  *
2034
2343
  * @elementSummary The `<pc-entity>` element creates an entity: a named, transformable node of the
2035
2344
  * scene hierarchy, and the host for component elements such as `<pc-camera>`, `<pc-light>` and
@@ -2049,11 +2358,17 @@ class EntityOwnerElement extends EntityBaseElement {
2049
2358
  * entity.
2050
2359
  * @attribute {string} onpointerup - Script to run when a pointer button is released over the
2051
2360
  * entity.
2361
+ * @attribute {string} onclick - Script to run when the entity is clicked: a primary pointer
2362
+ * button pressed and then released over it.
2052
2363
  * @fires {PointerEvent} pointerenter - Fired when the pointer moves onto the entity.
2053
2364
  * @fires {PointerEvent} pointerleave - Fired when the pointer moves off the entity.
2054
2365
  * @fires {PointerEvent} pointermove - Fired when the pointer moves over the entity.
2055
2366
  * @fires {PointerEvent} pointerdown - Fired when a pointer button is pressed over the entity.
2056
2367
  * @fires {PointerEvent} pointerup - Fired when a pointer button is released over the entity.
2368
+ * @fires {PointerEvent} click - Fired when a primary pointer button is pressed and then released
2369
+ * over the entity. A press and release that picked different entities fires on their nearest
2370
+ * common ancestor instead, as in the DOM. `detail` carries the click count, so a double click
2371
+ * arrives as a click whose `detail` is 2.
2057
2372
  */
2058
2373
  class EntityElement extends EntityOwnerElement {
2059
2374
  connectedCallback() {
@@ -2088,7 +2403,7 @@ class EntityElement extends EntityOwnerElement {
2088
2403
  this._entity?.destroy();
2089
2404
  }
2090
2405
  static get observedAttributes() {
2091
- return ['enabled', 'name', 'position', 'rotation', 'scale', 'tags', ...POINTER_ATTRIBUTES];
2406
+ return ['enabled', 'name', 'position', 'rotation', 'scale', 'tags', ...EVENT_ATTRIBUTES];
2092
2407
  }
2093
2408
  attributeChangedCallback(name, _oldValue, newValue) {
2094
2409
  switch (name) {
@@ -2115,6 +2430,7 @@ class EntityElement extends EntityOwnerElement {
2115
2430
  case 'onpointerdown':
2116
2431
  case 'onpointerup':
2117
2432
  case 'onpointermove':
2433
+ case 'onclick':
2118
2434
  this._updateInlineHandler(name, newValue);
2119
2435
  break;
2120
2436
  }
@@ -3064,11 +3380,17 @@ const formatHierarchy = (root, counts) => {
3064
3380
  * model.
3065
3381
  * @attribute {string} onpointerup - Script to run when a pointer button is released over the
3066
3382
  * model.
3383
+ * @attribute {string} onclick - Script to run when the model is clicked: a primary pointer
3384
+ * button pressed and then released over it.
3067
3385
  * @fires {PointerEvent} pointerenter - Fired when the pointer moves onto the model.
3068
3386
  * @fires {PointerEvent} pointerleave - Fired when the pointer moves off the model.
3069
3387
  * @fires {PointerEvent} pointermove - Fired when the pointer moves over the model.
3070
3388
  * @fires {PointerEvent} pointerdown - Fired when a pointer button is pressed over the model.
3071
3389
  * @fires {PointerEvent} pointerup - Fired when a pointer button is released over the model.
3390
+ * @fires {PointerEvent} click - Fired when a primary pointer button is pressed and then released
3391
+ * over the model. A press and release that picked different entities fires on their nearest
3392
+ * common ancestor instead, as in the DOM. `detail` carries the click count, so a double click
3393
+ * arrives as a click whose `detail` is 2.
3072
3394
  * @fires {Event} load - Fired each time a container asset finishes instantiating, including
3073
3395
  * re-instantiation after `asset` changes. Does not bubble — listen on this element, or use a
3074
3396
  * capture-phase listener on an ancestor.
@@ -3317,7 +3639,7 @@ class ModelElement extends EntityOwnerElement {
3317
3639
  return this._asset;
3318
3640
  }
3319
3641
  static get observedAttributes() {
3320
- return ['asset', 'enabled', 'name', 'position', 'rotation', 'scale', 'tags', ...POINTER_ATTRIBUTES];
3642
+ return ['asset', 'enabled', 'name', 'position', 'rotation', 'scale', 'tags', ...EVENT_ATTRIBUTES];
3321
3643
  }
3322
3644
  attributeChangedCallback(name, _oldValue, newValue) {
3323
3645
  switch (name) {
@@ -3347,6 +3669,7 @@ class ModelElement extends EntityOwnerElement {
3347
3669
  case 'onpointerdown':
3348
3670
  case 'onpointerup':
3349
3671
  case 'onpointermove':
3672
+ case 'onclick':
3350
3673
  this._updateInlineHandler(name, newValue);
3351
3674
  break;
3352
3675
  }
@@ -4210,8 +4533,6 @@ customElements.define('pc-anim', AnimComponentElement);
4210
4533
  * @elementSummary The `<pc-anim-clip>` element declares one named animation clip on its parent
4211
4534
  * `<pc-anim>`, taken from the `asset` it names or, without one, from the enclosing `<pc-model>`'s
4212
4535
  * own animations. Must be a direct child of `<pc-anim>`.
4213
- *
4214
- * @category Components
4215
4536
  */
4216
4537
  class AnimClipElement extends AsyncElement {
4217
4538
  /**
@@ -4613,7 +4934,9 @@ class ButtonComponentElement extends ComponentElement {
4613
4934
  };
4614
4935
  // The image entity defaults to the button's own entity (which carries the image element)
4615
4936
  // when no explicit reference is provided.
4616
- 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;
4617
4940
  if (imageEntity) {
4618
4941
  data.imageEntity = imageEntity;
4619
4942
  }
@@ -4656,21 +4979,27 @@ class ButtonComponentElement extends ComponentElement {
4656
4979
  return this._active;
4657
4980
  }
4658
4981
  /**
4659
- * Sets the reference (CSS selector, element id or entity name) to the `<pc-entity>` whose image
4660
- * element is used for visual transitions. Defaults to the button's own entity — inside a
4661
- * `<pc-model>`, that is the model's host entity, so supply an explicit reference to target a
4662
- * 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.
4663
4988
  * @param value - The image entity reference.
4664
4989
  */
4665
4990
  set image(value) {
4666
4991
  this._image = value;
4667
- const entity = getEntity(value);
4668
- if (this.component && entity) {
4669
- 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
+ }
4670
4997
  }
4671
4998
  }
4672
4999
  /**
4673
- * 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.
4674
5003
  * @returns The image entity reference.
4675
5004
  */
4676
5005
  get image() {
@@ -6398,7 +6727,12 @@ customElements.define('pc-element', ElementComponentElement);
6398
6727
  * primary axis: a hinge rotates about it, a slider translates along it and a ball joint twists
6399
6728
  * about it. The constrained bodies are referenced by `entity-a` and `entity-b`, both of which need
6400
6729
  * a rigid body component; leaving `entity-b` empty constrains `entity-a` to a fixed point in world
6401
- * 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.
6402
6736
  *
6403
6737
  * @elementSummary The `<pc-joint>` element constrains two rigid bodies to each other — a hinged
6404
6738
  * door, a swinging chain, a sliding drawer. Its entity's transform is the joint frame, and
@@ -6552,8 +6886,8 @@ class JointComponentElement extends ComponentElement {
6552
6886
  breakImpulse: this._breakImpulse,
6553
6887
  enableCollision: this._enableCollision,
6554
6888
  enableLimits: this._enableLimits,
6555
- entityA: getEntity(this._entityA),
6556
- 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'),
6557
6891
  limits: this._limits,
6558
6892
  linearDamping: this._linearDamping,
6559
6893
  linearEquilibrium: this._linearEquilibrium,
@@ -6808,39 +7142,48 @@ class JointComponentElement extends ComponentElement {
6808
7142
  return this._enableLimits;
6809
7143
  }
6810
7144
  /**
6811
- * Sets the reference (CSS selector, element id or entity name) to the `<pc-entity>` providing
6812
- * the first constrained body. The reference resolves when it is set, so an entity created
6813
- * 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.
6814
7151
  * @param value - The first body's entity reference.
6815
7152
  */
6816
7153
  set entityA(value) {
6817
7154
  this._entityA = value;
6818
7155
  if (this.component) {
6819
- this.component.entityA = getEntity(value);
7156
+ this.component.entityA = resolveEntity(value, this, 'entity-a', 'constraint not created');
6820
7157
  }
6821
7158
  }
6822
7159
  /**
6823
- * 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.
6824
7162
  * @returns The first body's entity reference.
6825
7163
  */
6826
7164
  get entityA() {
6827
7165
  return this._entityA;
6828
7166
  }
6829
7167
  /**
6830
- * Sets the reference (CSS selector, element id or entity name) to the `<pc-entity>` providing
6831
- * the second constrained body, or empty to constrain the first body to a fixed point in world
6832
- * space. The reference resolves when it is set, so an entity created later is picked up by
6833
- * 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.
6834
7175
  * @param value - The second body's entity reference.
6835
7176
  */
6836
7177
  set entityB(value) {
6837
7178
  this._entityB = value;
6838
7179
  if (this.component) {
6839
- this.component.entityB = getEntity(value);
7180
+ this.component.entityB = resolveEntity(value, this, 'entity-b', 'constraint not created');
6840
7181
  }
6841
7182
  }
6842
7183
  /**
6843
- * 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.
6844
7187
  * @returns The second body's entity reference.
6845
7188
  */
6846
7189
  get entityB() {
@@ -11214,7 +11557,7 @@ class ScrollbarComponentElement extends ComponentElement {
11214
11557
  value: this._value,
11215
11558
  handleSize: this._handleSize
11216
11559
  };
11217
- const handle = getEntity(this._handle);
11560
+ const handle = resolveEntity(this._handle, this, 'handle', 'reference ignored');
11218
11561
  if (handle) {
11219
11562
  data.handleEntity = handle;
11220
11563
  }
@@ -11280,19 +11623,24 @@ class ScrollbarComponentElement extends ComponentElement {
11280
11623
  return this._handleSize;
11281
11624
  }
11282
11625
  /**
11283
- * Sets the reference (CSS selector, element id or entity name) to the `<pc-entity>` used as the
11284
- * 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.
11285
11630
  * @param value - The handle entity reference.
11286
11631
  */
11287
11632
  set handle(value) {
11288
11633
  this._handle = value;
11289
- const entity = getEntity(value);
11290
- if (this.component && entity) {
11291
- 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
+ }
11292
11639
  }
11293
11640
  }
11294
11641
  /**
11295
- * 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.
11296
11644
  * @returns The handle entity reference.
11297
11645
  */
11298
11646
  get handle() {
@@ -11374,19 +11722,19 @@ class ScrollViewComponentElement extends ComponentElement {
11374
11722
  horizontalScrollbarVisibility: visibilities.get(this._horizontalScrollbarVisibility),
11375
11723
  verticalScrollbarVisibility: visibilities.get(this._verticalScrollbarVisibility)
11376
11724
  };
11377
- const viewport = getEntity(this._viewport);
11725
+ const viewport = resolveEntity(this._viewport, this, 'viewport', 'reference ignored');
11378
11726
  if (viewport) {
11379
11727
  data.viewportEntity = viewport;
11380
11728
  }
11381
- const content = getEntity(this._content);
11729
+ const content = resolveEntity(this._content, this, 'content', 'reference ignored');
11382
11730
  if (content) {
11383
11731
  data.contentEntity = content;
11384
11732
  }
11385
- const horizontalScrollbar = getEntity(this._horizontalScrollbar);
11733
+ const horizontalScrollbar = resolveEntity(this._horizontalScrollbar, this, 'horizontal-scrollbar', 'reference ignored');
11386
11734
  if (horizontalScrollbar) {
11387
11735
  data.horizontalScrollbarEntity = horizontalScrollbar;
11388
11736
  }
11389
- const verticalScrollbar = getEntity(this._verticalScrollbar);
11737
+ const verticalScrollbar = resolveEntity(this._verticalScrollbar, this, 'vertical-scrollbar', 'reference ignored');
11390
11738
  if (verticalScrollbar) {
11391
11739
  data.verticalScrollbarEntity = verticalScrollbar;
11392
11740
  }
@@ -11562,76 +11910,96 @@ class ScrollViewComponentElement extends ComponentElement {
11562
11910
  return this._verticalScrollbarVisibility;
11563
11911
  }
11564
11912
  /**
11565
- * Sets the reference (CSS selector, element id or entity name) to the `<pc-entity>` used as the
11566
- * 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.
11567
11917
  * @param value - The viewport entity reference.
11568
11918
  */
11569
11919
  set viewport(value) {
11570
11920
  this._viewport = value;
11571
- const entity = getEntity(value);
11572
- if (this.component && entity) {
11573
- 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
+ }
11574
11926
  }
11575
11927
  }
11576
11928
  /**
11577
- * 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.
11578
11931
  * @returns The viewport entity reference.
11579
11932
  */
11580
11933
  get viewport() {
11581
11934
  return this._viewport;
11582
11935
  }
11583
11936
  /**
11584
- * Sets the reference (CSS selector, element id or entity name) to the `<pc-entity>` used as the
11585
- * 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.
11586
11941
  * @param value - The content entity reference.
11587
11942
  */
11588
11943
  set content(value) {
11589
11944
  this._content = value;
11590
- const entity = getEntity(value);
11591
- if (this.component && entity) {
11592
- 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
+ }
11593
11950
  }
11594
11951
  }
11595
11952
  /**
11596
- * 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.
11597
11955
  * @returns The content entity reference.
11598
11956
  */
11599
11957
  get content() {
11600
11958
  return this._content;
11601
11959
  }
11602
11960
  /**
11603
- * Sets the reference (CSS selector, element id or entity name) to the `<pc-entity>` containing
11604
- * 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.
11605
11965
  * @param value - The horizontal scrollbar entity reference.
11606
11966
  */
11607
11967
  set horizontalScrollbar(value) {
11608
11968
  this._horizontalScrollbar = value;
11609
- const entity = getEntity(value);
11610
- if (this.component && entity) {
11611
- 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
+ }
11612
11974
  }
11613
11975
  }
11614
11976
  /**
11615
- * 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.
11616
11979
  * @returns The horizontal scrollbar entity reference.
11617
11980
  */
11618
11981
  get horizontalScrollbar() {
11619
11982
  return this._horizontalScrollbar;
11620
11983
  }
11621
11984
  /**
11622
- * Sets the reference (CSS selector, element id or entity name) to the `<pc-entity>` containing
11623
- * 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.
11624
11989
  * @param value - The vertical scrollbar entity reference.
11625
11990
  */
11626
11991
  set verticalScrollbar(value) {
11627
11992
  this._verticalScrollbar = value;
11628
- const entity = getEntity(value);
11629
- if (this.component && entity) {
11630
- 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
+ }
11631
11998
  }
11632
11999
  }
11633
12000
  /**
11634
- * 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.
11635
12003
  * @returns The vertical scrollbar entity reference.
11636
12004
  */
11637
12005
  get verticalScrollbar() {
@@ -11714,7 +12082,9 @@ customElements.define('pc-scroll-view', ScrollViewComponentElement);
11714
12082
  * Values are parsed according to the type of the attribute's current value — initially the
11715
12083
  * script's declared default (numbers, booleans, strings, Vec2/3/4, Color, Quat as Euler
11716
12084
  * angles) — and the `asset:`/`entity:`/`vec2:`/`vec3:`/`vec4:`/`color:` prefixes may be used
11717
- * 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.
11718
12088
  * - **The `attributes` JSON attribute**: an object supporting nested structures and attribute
11719
12089
  * names that collide with reserved HTML attribute names (e.g. `title`).
11720
12090
  *
@@ -11730,7 +12100,8 @@ customElements.define('pc-scroll-view', ScrollViewComponentElement);
11730
12100
  *
11731
12101
  * @elementSummary The `<pc-script-instance>` element attaches one script class, named by `name`, to
11732
12102
  * the entity of its parent `<pc-script>`. Its other attributes set script attributes of the same
11733
- * 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>`.
11734
12105
  *
11735
12106
  * @fires {CustomEvent} scriptattributeschange - Fired when the script's attributes change. The
11736
12107
  * `detail` carries the new `attributes` object. Bubbles.
@@ -11750,9 +12121,11 @@ class ScriptInstanceElement extends AsyncElement {
11750
12121
  /**
11751
12122
  * Sets the attributes of the script as an object. Values are converted with the same rules
11752
12123
  * as the `attributes` attribute: `asset:`/`entity:` references and `vec2:`/`vec3:`/`vec4:`/
11753
- * `color:` prefixed strings are resolved, and a plain numeric array is converted to the
11754
- * type of the attribute it targets when that attribute currently holds a Vec2, Vec3, Vec4
11755
- * 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.
11756
12129
  * @param value - The attributes of the script.
11757
12130
  */
11758
12131
  set scriptAttributes(value) {
@@ -11763,7 +12136,10 @@ class ScriptInstanceElement extends AsyncElement {
11763
12136
  }));
11764
12137
  }
11765
12138
  /**
11766
- * 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.
11767
12143
  * @returns The attributes of the script.
11768
12144
  */
11769
12145
  get scriptAttributes() {
@@ -11974,18 +12350,23 @@ const assetConversion = (rest, raw) => {
11974
12350
  return raw;
11975
12351
  };
11976
12352
  /**
11977
- * Resolves an `entity:` prefix to the Entity backing a `pc-entity` element. The reference can be a
11978
- * 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.
11979
12357
  * @param rest - The entity reference.
11980
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.
11981
12360
  * @returns The entity, or `raw`.
11982
12361
  */
11983
- const entityConversion = (rest, raw) => {
11984
- const entity = getEntity(rest);
12362
+ const entityConversion = (rest, raw, from) => {
12363
+ const entity = getEntity(rest, from);
11985
12364
  if (entity) {
11986
12365
  return entity;
11987
12366
  }
11988
- 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}` : ''}`);
11989
12370
  return raw;
11990
12371
  };
11991
12372
  /**
@@ -12118,8 +12499,10 @@ class ScriptComponentElement extends ComponentElement {
12118
12499
  /**
12119
12500
  * Recursively converts raw attribute data into proper PlayCanvas types. Supported conversions:
12120
12501
  * - "asset:id" → the Asset created by the `pc-asset` element with that id
12121
- * - "entity:ref" → the Entity backing a `pc-entity` element. The reference can be a CSS
12122
- * 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.
12123
12506
  * - "vec2:1 2" → new Vec2(1, 2)
12124
12507
  * - "vec3:1 2 3" → new Vec3(1, 2, 3)
12125
12508
  * - "vec4:1 2 3 4" → new Vec4(1, 2, 3, 4)
@@ -12133,7 +12516,7 @@ class ScriptComponentElement extends ComponentElement {
12133
12516
  convertAttributes(item) {
12134
12517
  if (typeof item === 'string') {
12135
12518
  const match = matchConversion(item);
12136
- return match ? match.convert(match.rest, item) : item;
12519
+ return match ? match.convert(match.rest, item, this) : item;
12137
12520
  }
12138
12521
  if (Array.isArray(item)) {
12139
12522
  return item.map((element) => this.convertAttributes(element));
@@ -13340,11 +13723,17 @@ const levenshtein = (a, b) => {
13340
13723
  * node.
13341
13724
  * @attribute {string} onpointerup - Script to run when a pointer button is released over the
13342
13725
  * node.
13726
+ * @attribute {string} onclick - Script to run when the node is clicked: a primary pointer
13727
+ * button pressed and then released over it.
13343
13728
  * @fires {PointerEvent} pointerenter - Fired when the pointer moves onto the node.
13344
13729
  * @fires {PointerEvent} pointerleave - Fired when the pointer moves off the node.
13345
13730
  * @fires {PointerEvent} pointermove - Fired when the pointer moves over the node.
13346
13731
  * @fires {PointerEvent} pointerdown - Fired when a pointer button is pressed over the node.
13347
13732
  * @fires {PointerEvent} pointerup - Fired when a pointer button is released over the node.
13733
+ * @fires {PointerEvent} click - Fired when a primary pointer button is pressed and then released
13734
+ * over the node. A press and release that picked different entities fires on their nearest
13735
+ * common ancestor instead, as in the DOM. `detail` carries the click count, so a double click
13736
+ * arrives as a click whose `detail` is 2.
13348
13737
  */
13349
13738
  class NodeElement extends EntityBaseElement {
13350
13739
  _name = '';
@@ -13994,7 +14383,7 @@ class NodeElement extends EntityBaseElement {
13994
14383
  'rotation',
13995
14384
  'scale',
13996
14385
  'tags',
13997
- ...POINTER_ATTRIBUTES
14386
+ ...EVENT_ATTRIBUTES
13998
14387
  ];
13999
14388
  }
14000
14389
  attributeChangedCallback(name, _oldValue, newValue) {
@@ -14044,6 +14433,7 @@ class NodeElement extends EntityBaseElement {
14044
14433
  case 'onpointerdown':
14045
14434
  case 'onpointerup':
14046
14435
  case 'onpointermove':
14436
+ case 'onclick':
14047
14437
  this._updateInlineHandler(name, newValue);
14048
14438
  break;
14049
14439
  }
@@ -14058,9 +14448,14 @@ customElements.define('pc-node', NodeElement);
14058
14448
  * {@link HTMLElement} interface.
14059
14449
  *
14060
14450
  * @elementSummary The `<pc-scene>` element holds the entity hierarchy the application renders,
14061
- * along with the scene-wide fog and gravity settings. Must be a direct child of `<pc-app>`.
14451
+ * along with the scene-wide fog, exposure and gravity settings. Must be a direct child of
14452
+ * `<pc-app>`.
14062
14453
  */
14063
14454
  class SceneElement extends AsyncElement {
14455
+ /**
14456
+ * The exposure of the scene.
14457
+ */
14458
+ _exposure = 1;
14064
14459
  /**
14065
14460
  * The fog type of the scene.
14066
14461
  */
@@ -14128,6 +14523,7 @@ class SceneElement extends AsyncElement {
14128
14523
  }
14129
14524
  _updateSceneSettings() {
14130
14525
  if (this._scene) {
14526
+ this._scene.exposure = this._exposure;
14131
14527
  this._scene.fog.type = this._fog;
14132
14528
  this._scene.fog.color = this._fogColor;
14133
14529
  this._scene.fog.density = this._fogDensity;
@@ -14146,6 +14542,24 @@ class SceneElement extends AsyncElement {
14146
14542
  _applyGravity(value) {
14147
14543
  this.closestApp?.app?.systems.rigidbody?.gravity.copy(value);
14148
14544
  }
14545
+ /**
14546
+ * Sets the exposure of the scene, which tweaks the overall brightness of the rendered image.
14547
+ * Ignored if the scene is using physical units. Defaults to 1.
14548
+ * @param value - The exposure.
14549
+ */
14550
+ set exposure(value) {
14551
+ this._exposure = value;
14552
+ if (this.scene) {
14553
+ this.scene.exposure = value;
14554
+ }
14555
+ }
14556
+ /**
14557
+ * Gets the exposure of the scene.
14558
+ * @returns The exposure.
14559
+ */
14560
+ get exposure() {
14561
+ return this._exposure;
14562
+ }
14149
14563
  /**
14150
14564
  * Sets the fog type of the scene. Can be `none`, `linear`, `exp` or `exp2`. Defaults to
14151
14565
  * `none`.
@@ -14250,10 +14664,13 @@ class SceneElement extends AsyncElement {
14250
14664
  return this._gravity;
14251
14665
  }
14252
14666
  static get observedAttributes() {
14253
- return ['fog', 'fog-color', 'fog-density', 'fog-start', 'fog-end', 'gravity'];
14667
+ return ['exposure', 'fog', 'fog-color', 'fog-density', 'fog-start', 'fog-end', 'gravity'];
14254
14668
  }
14255
14669
  attributeChangedCallback(name, _oldValue, newValue) {
14256
14670
  switch (name) {
14671
+ case 'exposure':
14672
+ this.exposure = parseNumber(newValue, 1, name);
14673
+ break;
14257
14674
  case 'fog':
14258
14675
  this.fog = parseEnum(newValue, ['none', 'linear', 'exp', 'exp2'], 'none', name);
14259
14676
  break;