@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.cjs CHANGED
@@ -179,6 +179,136 @@ class WasmElement extends AsyncElement {
179
179
  }
180
180
  customElements.define('pc-wasm', WasmElement);
181
181
 
182
+ /**
183
+ * The event types the containing `<pc-app>` synthesizes on entity-fronting elements via picking:
184
+ * the `pointer*` events, plus `click` — which concludes a primary-button press and release, and
185
+ * is delivered as a `PointerEvent` exactly as modern browsers deliver native clicks.
186
+ * @internal
187
+ */
188
+ const SYNTHESIZED_EVENTS = [
189
+ 'pointerenter',
190
+ 'pointerleave',
191
+ 'pointerdown',
192
+ 'pointerup',
193
+ 'pointermove',
194
+ 'click'
195
+ ];
196
+ const SYNTHESIZED_EVENT_SET = new Set(SYNTHESIZED_EVENTS);
197
+ /**
198
+ * The attribute names of the inline event handlers (`onpointerdown`, `onclick`, ...), shared by
199
+ * every element that fronts an engine entity. Spread into `observedAttributes` by subclasses.
200
+ * @internal
201
+ */
202
+ const EVENT_ATTRIBUTES = SYNTHESIZED_EVENTS.map((type) => `on${type}`);
203
+ /**
204
+ * The base class for elements that front an engine {@link Entity}: `<pc-entity>` and
205
+ * `<pc-model>`, which create one, and `<pc-node>`, which binds to one inside a model's
206
+ * instantiated hierarchy. It carries what all of them need — the `entity` contract, registration
207
+ * with the owning application (which joins picked scene nodes back to elements by identity,
208
+ * never by name), and the pointer listener bookkeeping that lets the application lazily attach
209
+ * its canvas handlers.
210
+ */
211
+ class EntityBaseElement extends AsyncElement {
212
+ _entity = null;
213
+ /**
214
+ * The application element this entity is registered with, cached at registration time so the
215
+ * entity can be unregistered even once this element has left the DOM.
216
+ */
217
+ _appElement = null;
218
+ /**
219
+ * The event listeners registered on the element, by type.
220
+ */
221
+ _listeners = {};
222
+ /**
223
+ * The event types for which an inline handler attribute (`onpointerdown`, `onclick`, ...)
224
+ * is currently present.
225
+ */
226
+ _inlineHandlerTypes = new Set();
227
+ /**
228
+ * The PlayCanvas entity instance. `null` until the element is ready, and again once the
229
+ * entity is gone — await {@link whenReady} or the element's `ready()` promise before
230
+ * accessing it.
231
+ * @returns The entity instance, or `null`.
232
+ */
233
+ get entity() {
234
+ return this._entity;
235
+ }
236
+ /**
237
+ * Registers `entity` as this element's backing entity with the owning application, which
238
+ * joins engine nodes back to elements by identity (never by name).
239
+ *
240
+ * @param entity - The entity to register.
241
+ */
242
+ _registerEntity(entity) {
243
+ this._appElement = this.closestApp;
244
+ this._appElement?._registerEntityElement(entity, this);
245
+ }
246
+ /**
247
+ * Removes the registration for `entity`.
248
+ *
249
+ * @param entity - The entity to unregister.
250
+ */
251
+ _unregisterEntity(entity) {
252
+ this._appElement?._unregisterEntityElement(entity);
253
+ this._appElement = null;
254
+ }
255
+ /**
256
+ * Tracks whether an inline handler attribute is present. The browser itself compiles and
257
+ * runs these attributes — they are standard `GlobalEventHandlers`, so setting one replaces
258
+ * the previous handler and removing it removes the handler, exactly like `onclick` on any
259
+ * HTML element. But because they bypass {@link EventTarget.addEventListener}, the connect/disconnect
260
+ * bookkeeping that lets the application lazily attach its canvas pointer handlers must be
261
+ * kept in sync here.
262
+ *
263
+ * @param name - The attribute name (e.g. 'onpointerdown').
264
+ * @param value - The attribute value, or `null` when the attribute has been removed.
265
+ */
266
+ _updateInlineHandler(name, value) {
267
+ const type = name.substring(2);
268
+ const had = this._inlineHandlerTypes.has(type);
269
+ const has = value !== null;
270
+ if (has && !had) {
271
+ this._inlineHandlerTypes.add(type);
272
+ this.dispatchEvent(new CustomEvent(`${type}:connect`, { bubbles: true }));
273
+ }
274
+ else if (!has && had) {
275
+ this._inlineHandlerTypes.delete(type);
276
+ this.dispatchEvent(new CustomEvent(`${type}:disconnect`, { bubbles: true }));
277
+ }
278
+ }
279
+ addEventListener(type, listener, options) {
280
+ if (!this._listeners[type]) {
281
+ this._listeners[type] = [];
282
+ }
283
+ this._listeners[type].push(listener);
284
+ super.addEventListener(type, listener, options);
285
+ if (SYNTHESIZED_EVENT_SET.has(type)) {
286
+ this.dispatchEvent(new CustomEvent(`${type}:connect`, { bubbles: true }));
287
+ }
288
+ }
289
+ removeEventListener(type, listener, options) {
290
+ if (this._listeners[type]) {
291
+ this._listeners[type] = this._listeners[type].filter((l) => l !== listener);
292
+ }
293
+ super.removeEventListener(type, listener, options);
294
+ if (SYNTHESIZED_EVENT_SET.has(type)) {
295
+ this.dispatchEvent(new CustomEvent(`${type}:disconnect`, { bubbles: true }));
296
+ }
297
+ }
298
+ /**
299
+ * Whether the element has a listener for an event type, registered either with
300
+ * {@link EventTarget.addEventListener} or with the matching inline handler attribute. Read by the
301
+ * containing `<pc-app>` element to gate event synthesis.
302
+ *
303
+ * @param type - The event type.
304
+ * @returns Whether a listener is registered.
305
+ * @internal
306
+ */
307
+ _hasListeners(type) {
308
+ return Boolean(this._listeners[type]?.length) || this._inlineHandlerTypes.has(type);
309
+ }
310
+ }
311
+
182
312
  /** Covers the 0.2s opacity transition; jsdom never fires transitionend, so removal is timed. */
183
313
  const REMOVAL_DELAY_MS = 250;
184
314
  /**
@@ -457,8 +587,13 @@ const CSS_COLORS = {
457
587
  * - `parseBool` and `parseTags` take no attribute name, because every value is valid for them and
458
588
  * so they never warn.
459
589
  *
460
- * `getEntity` is the exception: it resolves a reference to a live entity rather than parsing a
461
- * literal, and returns `null` instead of falling back to a default.
590
+ * `findEntityElement` and `getEntity` are the exceptions: they resolve a reference rather than
591
+ * parsing a literal, and return `null` instead of falling back to a default. A reference
592
+ * beginning with `#` is a document-wide selector (an element id, or any selector rooted in one);
593
+ * anything else is an entity name, resolved lexically through the entity hierarchy first and
594
+ * against the document after — never as a selector or an id. They also do not warn - what an
595
+ * unresolved reference means depends on the element holding it - so elements report through
596
+ * `resolveEntity`, which takes that meaning as parameters.
462
597
  */
463
598
  /**
464
599
  * Splits an attribute value into exactly `count` numeric components. Returns `null` when the
@@ -705,41 +840,259 @@ const parseVec4 = (value, defaultValue, attribute) => {
705
840
  return new playcanvas.Vec4(components);
706
841
  };
707
842
  /**
708
- * Resolves a reference string to the {@link Entity} backing a `<pc-entity>` element. The reference
709
- * can be a CSS selector (e.g. `#my-id`, `pc-entity[name="Foo"]`), a bare element id, or a bare
710
- * entity name. Returns `null` if no matching element (or backing entity) is found.
843
+ * Runs querySelector, absorbing the SyntaxError an unparseable selector throws - references are
844
+ * arbitrary author text, so a lookup must fail to `null`, never throw.
711
845
  *
712
- * @param ref - The reference string to resolve.
713
- * @returns The resolved entity, or `null`.
714
- * @internal
846
+ * @param selector - The selector to query.
847
+ * @returns The matched element, or `null`.
715
848
  */
716
- const getEntity = (ref) => {
717
- if (!ref) {
849
+ const query = (selector) => {
850
+ try {
851
+ return document.querySelector(selector);
852
+ }
853
+ catch {
718
854
  return null;
719
855
  }
720
- let element = null;
721
- // Try the reference as a CSS selector. An invalid selector (e.g. a bare name containing
722
- // spaces) throws, in which case we fall back to id/name lookups below.
856
+ };
857
+ /**
858
+ * Runs a lookup against one scope, checking the scope element itself before its subtree a
859
+ * reference deep in a cloned prefab must be able to name the prefab's root. Absorbs the
860
+ * SyntaxError of an invalid selector like {@link query}: escaping quotes and backslashes does not
861
+ * make arbitrary text a valid CSS string (a reference containing a newline still throws), so a
862
+ * lookup must fail to `null`, never throw.
863
+ *
864
+ * @param scope - The element whose inclusive subtree to search.
865
+ * @param selector - The selector to query.
866
+ * @returns The matched element, or `null`.
867
+ */
868
+ const queryScope = (scope, selector) => {
723
869
  try {
724
- element = document.querySelector(ref);
870
+ return scope.matches(selector) ? scope : scope.querySelector(selector);
725
871
  }
726
872
  catch {
727
- element = null;
873
+ return null;
874
+ }
875
+ };
876
+ /**
877
+ * Reads the entity a resolved element is backing, through the `entity` accessor every
878
+ * entity-fronting element exposes. `null` for no element, and for an element backing nothing.
879
+ *
880
+ * @param element - The element to read, or `null`.
881
+ * @returns The backing entity, or `null`.
882
+ */
883
+ const entityOf = (element) => {
884
+ return element?.entity ?? null;
885
+ };
886
+ /**
887
+ * The elements that front an entity: what a bare name can resolve to, and the scopes of the
888
+ * lexical name lookup.
889
+ */
890
+ const ENTITY_KINDS = ['pc-entity', 'pc-model', 'pc-node'];
891
+ /**
892
+ * The entity-fronting elements as one selector, for the scope walk.
893
+ */
894
+ const ENTITY_SCOPES = ENTITY_KINDS.join(', ');
895
+ /**
896
+ * Resolves a reference string to the element it names. The grammar is closed — every reference
897
+ * has exactly one interpretation:
898
+ *
899
+ * - A reference beginning with `#` is a document-wide CSS selector — an element id (`#body`), or
900
+ * any selector rooted in one (`#hud pc-entity`). It is authoritative: the name lookup never
901
+ * runs for it, so an unusually named entity cannot shadow it.
902
+ * - Any other reference is the name of an entity-fronting element (`<pc-entity>`, `<pc-model>` or
903
+ * `<pc-node>` — for a node, the glTF node name it binds), and nothing else. A bare reference is
904
+ * never interpreted as a selector or an element id, so adding or renaming elements can never
905
+ * change which form it takes.
906
+ *
907
+ * When `from` is supplied, a name resolves lexically first: the closest entity-fronting
908
+ * ancestor's inclusive subtree, then each outer entity-fronting ancestor, then the containing
909
+ * `<pc-app>`, then the document. This is what lets a `<template>` prefab reference its own
910
+ * entities by name — every clone resolves within itself before a document-wide lookup could reach
911
+ * an earlier clone — provided the prefab has a single entity-fronting root to be the enclosing
912
+ * scope.
913
+ *
914
+ * Separate from {@link getEntity} so a caller reporting a failure can tell the causes apart
915
+ * ({@link unresolvedCause} words them): nothing in the document matches the reference, or
916
+ * something matches but is not backing an entity (yet, or ever).
917
+ *
918
+ * @param ref - The reference string to resolve.
919
+ * @param from - The element resolving the reference, whose entity-fronting ancestors scope the
920
+ * name lookup. Omitted, the name lookup is document-wide only.
921
+ * @returns The matched element, or `null`.
922
+ * @internal
923
+ */
924
+ const findEntityElement = (ref, from) => {
925
+ if (!ref) {
926
+ return null;
927
+ }
928
+ // A '#' reference is document-wide and bypasses the name lookup entirely - an entity named
929
+ // '#body' must never shadow the element whose id is 'body'.
930
+ if (ref.startsWith('#')) {
931
+ return query(ref);
932
+ }
933
+ // The name lands inside a quoted CSS string, so its quotes and backslashes are escaped -
934
+ // a name like `say "hi"` must resolve, not turn the lookup into a SyntaxError.
935
+ const escaped = ref.replace(/["\\]/g, '\\$&');
936
+ const nameSelector = ENTITY_KINDS.map(kind => `${kind}[name="${escaped}"]`).join(', ');
937
+ if (from) {
938
+ let scope = from.parentElement?.closest(ENTITY_SCOPES);
939
+ while (scope) {
940
+ const element = queryScope(scope, nameSelector);
941
+ if (element) {
942
+ return element;
943
+ }
944
+ scope = scope.parentElement?.closest(ENTITY_SCOPES);
945
+ }
946
+ const app = from.parentElement?.closest('pc-app');
947
+ if (app) {
948
+ const element = queryScope(app, nameSelector);
949
+ if (element) {
950
+ return element;
951
+ }
952
+ }
728
953
  }
954
+ return query(nameSelector);
955
+ };
956
+ /**
957
+ * Resolves a reference string to the {@link Entity} backing an entity-fronting element
958
+ * (`<pc-entity>`, `<pc-model>` or `<pc-node>`). The reference is a name — resolved lexically
959
+ * through the entity hierarchy first when `from` is supplied — or a document-wide `#` selector
960
+ * ({@link findEntityElement} details the grammar and order). Returns `null` if no matching
961
+ * element (or backing entity) is found.
962
+ *
963
+ * @param ref - The reference string to resolve.
964
+ * @param from - The element resolving the reference, whose entity-fronting ancestors scope the
965
+ * name lookup. Omitted, the name lookup is document-wide only.
966
+ * @returns The resolved entity, or `null`.
967
+ * @internal
968
+ */
969
+ const getEntity = (ref, from) => {
970
+ return entityOf(findEntityElement(ref, from));
971
+ };
972
+ /**
973
+ * Describes why a non-empty reference did not resolve, for a warning. Three causes, because they
974
+ * have three different fixes: nothing matches (usually a typo), the matched element is not backing
975
+ * an entity yet (usually timing - a `pc-node` whose asset has not loaded - so resolving again
976
+ * later can work), or the matched element can never back one (the reference points at the wrong
977
+ * element, so only correcting it can). Capability is the `entity` accessor every entity-backing
978
+ * element inherits from EntityBaseElement.
979
+ *
980
+ * @param element - The element the reference matched, or `null` when nothing did.
981
+ * @returns The cause, phrased to follow `could not resolve ... -`.
982
+ * @internal
983
+ */
984
+ const unresolvedCause = (element) => {
729
985
  if (!element) {
730
- element = document.getElementById(ref) ?? document.querySelector(`pc-entity[name="${ref}"]`);
986
+ return 'nothing in the document matches it';
731
987
  }
732
- return element?.entity ?? null;
988
+ const tag = `<${element.tagName.toLowerCase()}>`;
989
+ return 'entity' in element
990
+ ? `${tag} matches it but is not backing an entity yet`
991
+ : `${tag} matches it but cannot back an entity`;
992
+ };
993
+ /**
994
+ * Builds the migration pointer for a bare reference that names nothing but matches the id of an
995
+ * entity-fronting element - it was almost certainly meant as an id, so point at the form that
996
+ * expresses it, escaped so the suggestion actually parses as a selector (an id like `a:b` must
997
+ * be written `#a\:b`). Empty when the reference is already a `#` form, matches no id, or the id
998
+ * belongs to an element that could never back an entity - suggesting it would only trade this
999
+ * warning for the wrong-target one.
1000
+ *
1001
+ * @param ref - The unresolved reference.
1002
+ * @param prefix - Text the suggested form must carry in the caller's syntax (e.g. `entity:`).
1003
+ * @returns The advice sentence, or an empty string.
1004
+ * @internal
1005
+ */
1006
+ const idHint = (ref, prefix = '') => {
1007
+ const match = !ref.startsWith('#') && document.getElementById(ref);
1008
+ return match && 'entity' in match
1009
+ ? `A bare reference is a name - write '${prefix}#${CSS.escape(ref)}' to reference the element with that id.`
1010
+ : '';
1011
+ };
1012
+ /**
1013
+ * Resolves a reference string to the {@link Entity} backing an entity-fronting element, scoped to
1014
+ * the resolving element ({@link findEntityElement} details the order) and warning when a
1015
+ * non-empty reference does not resolve - otherwise the reference fails silently, invisible
1016
+ * except through the behavior it should have driven. The message names which of the three causes
1017
+ * ({@link unresolvedCause}) it hit, and advises reassigning later only when that can work.
1018
+ *
1019
+ * An empty reference stays silent: it is the unset state of an optional attribute, and on some
1020
+ * elements (`pc-joint` `entity-b`, `pc-button` `image`) a documented value of its own.
1021
+ *
1022
+ * @param ref - The reference string to resolve.
1023
+ * @param from - The element resolving the reference; scopes the lookup and names the message.
1024
+ * @param attribute - The attribute being resolved, for the message.
1025
+ * @param consequence - What the unresolved reference means for the element, for the message.
1026
+ * @returns The resolved entity, or `null`.
1027
+ * @internal
1028
+ */
1029
+ const resolveEntity = (ref, from, attribute, consequence) => {
1030
+ if (!ref) {
1031
+ return null;
1032
+ }
1033
+ const element = findEntityElement(ref, from);
1034
+ const entity = entityOf(element);
1035
+ if (!entity) {
1036
+ let advice = `Assign ${attribute} again once the entity exists.`;
1037
+ if (element && !('entity' in element)) {
1038
+ advice = `Point ${attribute} at a pc-entity, pc-model or pc-node instead.`;
1039
+ }
1040
+ else if (!element) {
1041
+ const hint = idHint(ref);
1042
+ if (hint) {
1043
+ advice = hint;
1044
+ }
1045
+ }
1046
+ console.warn(`${from.tagName.toLowerCase()} could not resolve ${attribute} '${ref}' - ${unresolvedCause(element)} - ${consequence}. ${advice}`);
1047
+ }
1048
+ return entity;
733
1049
  };
734
1050
 
735
- /** The pointer event types the application synthesizes on `<pc-entity>` elements via picking. */
736
- const pointerEventTypes = ['pointermove', 'pointerdown', 'pointerup', 'pointerenter', 'pointerleave'];
737
1051
  /**
738
1052
  * The event types whose listeners make an element a hover target. Hover resolution walks past
739
1053
  * elements listening for none of them, so a silent element never swallows an ancestor's
740
1054
  * enter/leave pair.
741
1055
  */
742
1056
  const hoverEventTypes = ['pointerenter', 'pointerleave', 'pointermove'];
1057
+ /**
1058
+ * The canvas listeners each synthesized event type is driven by. Enter and leave are derived
1059
+ * from move picks. A click is concluded from the down/up pair, with pointercancel discarding a
1060
+ * press the browser takes back (for example a touch that becomes a scroll).
1061
+ */
1062
+ const canvasEventsFor = {
1063
+ pointermove: ['pointermove'],
1064
+ pointerenter: ['pointermove'],
1065
+ pointerleave: ['pointermove'],
1066
+ pointerdown: ['pointerdown'],
1067
+ pointerup: ['pointerup'],
1068
+ click: ['pointerdown', 'pointerup', 'pointercancel']
1069
+ };
1070
+ /**
1071
+ * How long after a click a further click on the same target still raises the click count that
1072
+ * `detail` carries, approximating the platform's double-click time.
1073
+ */
1074
+ const CLICK_CHAIN_MS = 500;
1075
+ /**
1076
+ * Finds the nearest common inclusive ancestor of two picked nodes - the node a click belongs to
1077
+ * when the press and the release picked different geometry, exactly as the DOM assigns a click
1078
+ * whose down and up have different targets.
1079
+ *
1080
+ * @param a - The node the press picked, or `null`.
1081
+ * @param b - The node the release picked, or `null`.
1082
+ * @returns The nearest common inclusive ancestor, or `null` when there is none.
1083
+ */
1084
+ const commonAncestor = (a, b) => {
1085
+ const ancestors = new Set();
1086
+ for (let node = a; node !== null; node = node.parent) {
1087
+ ancestors.add(node);
1088
+ }
1089
+ for (let node = b; node !== null; node = node.parent) {
1090
+ if (ancestors.has(node)) {
1091
+ return node;
1092
+ }
1093
+ }
1094
+ return null;
1095
+ };
743
1096
  /**
744
1097
  * Gives `pc-app` the sizing contract of a replaced element (`<video>`, `<img>`): a block-level
745
1098
  * box that the page's CSS sizes, defaulting to the canvas's own 300x150 intrinsic size, with the
@@ -824,21 +1177,36 @@ class AppElement extends AsyncElement {
824
1177
  */
825
1178
  _entityElements = new Map();
826
1179
  _picker = null;
827
- _hasPointerListeners = {
828
- pointerenter: false,
829
- pointerleave: false,
830
- pointerdown: false,
831
- pointerup: false,
832
- pointermove: false
833
- };
834
1180
  _hoveredEntity = null;
835
1181
  // Identifies the newest in-flight hover pick, so out-of-order results can be discarded
836
1182
  _pickToken = 0;
837
1183
  _pointerHandlers = {
838
1184
  pointermove: null,
839
1185
  pointerdown: null,
840
- pointerup: null
1186
+ pointerup: null,
1187
+ pointercancel: null
841
1188
  };
1189
+ /**
1190
+ * The pick of each pointer's primary-button press, keyed by pointerId and kept while a click
1191
+ * may still conclude it. The promise is stored rather than its result, so a release can
1192
+ * await a press pick that has not resolved yet. Entries are removed by the matching
1193
+ * pointerup or pointercancel, and only ever stored while some element listens for click -
1194
+ * which is also what keeps those two canvas listeners attached.
1195
+ */
1196
+ _downPicks = new Map();
1197
+ /** Whether any element in the tree listens for click. Maintained by _syncCanvasListeners. */
1198
+ _clickListened = false;
1199
+ /**
1200
+ * The previous click's target, time and count, for chaining successive clicks into the
1201
+ * click count that `detail` carries. `null` until a click has fired.
1202
+ */
1203
+ _lastClick = null;
1204
+ /**
1205
+ * Serializes dispatch of the discrete synthesized events (pointerdown, pointerup, click),
1206
+ * whose picks resolve in GPU order, not canvas-event order. Replaced on teardown, so a pick
1207
+ * that never resolves cannot stall the dispatches of a later boot.
1208
+ */
1209
+ _dispatchChain = Promise.resolve();
842
1210
  _app = null;
843
1211
  _loadProgress = 0;
844
1212
  /**
@@ -873,12 +1241,12 @@ class AppElement extends AsyncElement {
873
1241
  */
874
1242
  constructor() {
875
1243
  super();
876
- // Track pointer listeners being added to and removed from descendant entities.
877
- // Registered once here rather than on every boot - the handlers no-op while there is no
878
- // canvas, and a re-booted element must not stack a second set.
879
- pointerEventTypes.forEach((type) => {
880
- this.addEventListener(`${type}:connect`, () => this._onPointerListenerAdded(type));
881
- this.addEventListener(`${type}:disconnect`, () => this._onPointerListenerRemoved(type));
1244
+ // Track listeners for the synthesized events being added to and removed from descendant
1245
+ // entities. Registered once here rather than on every boot - the sync no-ops while there
1246
+ // is no canvas, and a re-booted element must not stack a second set.
1247
+ SYNTHESIZED_EVENTS.forEach((type) => {
1248
+ this.addEventListener(`${type}:connect`, () => this._syncCanvasListeners());
1249
+ this.addEventListener(`${type}:disconnect`, () => this._syncCanvasListeners());
882
1250
  });
883
1251
  }
884
1252
  async connectedCallback() {
@@ -1185,9 +1553,8 @@ class AppElement extends AsyncElement {
1185
1553
  _pickerCreate() {
1186
1554
  const { width, height } = this.app.graphicsDevice;
1187
1555
  this._picker = new playcanvas.Picker(this.app, width, height);
1188
- // Create bound handlers but don't attach them yet. The handlers pick asynchronously, so
1189
- // each is wrapped to discard the promise - a listener must not return one, and nothing
1190
- // awaits the result.
1556
+ // Create bound handlers but don't attach them yet. The move handler is async, so it is
1557
+ // wrapped to discard the promise - a listener must not return one.
1191
1558
  const listener = (handler) => {
1192
1559
  return (event) => {
1193
1560
  handler.call(this, event);
@@ -1196,15 +1563,13 @@ class AppElement extends AsyncElement {
1196
1563
  this._pointerHandlers.pointermove = listener(this._onPointerMove);
1197
1564
  this._pointerHandlers.pointerdown = listener(this._onPointerDown);
1198
1565
  this._pointerHandlers.pointerup = listener(this._onPointerUp);
1199
- // Attach canvas handlers for listeners registered before this boot (e.g. handlers
1200
- // created from onpointer* attributes when their elements were first upgraded, or
1566
+ this._pointerHandlers.pointercancel = (event) => {
1567
+ this._downPicks.delete(event.pointerId);
1568
+ };
1569
+ // Attach canvas listeners for element listeners registered before this boot (e.g.
1570
+ // handlers created from inline attributes when their elements were first upgraded, or
1201
1571
  // listeners carried over from before a re-boot)
1202
- pointerEventTypes.forEach((type) => {
1203
- const anyListeners = Array.from(this.querySelectorAll('pc-entity, pc-model, pc-node')).some((entity) => entity._hasListeners(type));
1204
- if (anyListeners) {
1205
- this._onPointerListenerAdded(type);
1206
- }
1207
- });
1572
+ this._syncCanvasListeners();
1208
1573
  }
1209
1574
  _pickerDestroy() {
1210
1575
  if (this._canvas) {
@@ -1219,15 +1584,14 @@ class AppElement extends AsyncElement {
1219
1584
  this._pointerHandlers = {
1220
1585
  pointermove: null,
1221
1586
  pointerdown: null,
1222
- pointerup: null
1223
- };
1224
- this._hasPointerListeners = {
1225
- pointerenter: false,
1226
- pointerleave: false,
1227
- pointerdown: false,
1228
- pointerup: false,
1229
- pointermove: false
1587
+ pointerup: null,
1588
+ pointercancel: null
1230
1589
  };
1590
+ this._downPicks.clear();
1591
+ this._clickListened = false;
1592
+ this._lastClick = null;
1593
+ // Replace the chain: a pick that never resolves must not stall a later boot's dispatches
1594
+ this._dispatchChain = Promise.resolve();
1231
1595
  }
1232
1596
  /**
1233
1597
  * Registers the element that fronts an entity. Called by EntityElement when it creates its
@@ -1430,51 +1794,116 @@ class AppElement extends AsyncElement {
1430
1794
  newHoverEntity.dispatchEvent(new PointerEvent('pointermove', event));
1431
1795
  }
1432
1796
  }
1433
- async _onPointerDown(event) {
1797
+ /**
1798
+ * Appends a dispatch step to {@link _dispatchChain}. Must be called synchronously from the
1799
+ * canvas event handler - the order of appends is what carries canvas-event order. A step
1800
+ * that rejects is reported and released, so the steps queued behind it still dispatch.
1801
+ *
1802
+ * @param step - The dispatch work to run once every earlier step has finished.
1803
+ */
1804
+ _chainDispatch(step) {
1805
+ this._dispatchChain = this._dispatchChain.then(step).catch((error) => {
1806
+ console.error(error);
1807
+ });
1808
+ }
1809
+ _onPointerDown(event) {
1434
1810
  if (!this._picker || !this.app)
1435
1811
  return;
1436
- const node = await this._pickNode(event);
1437
- if (!this._picker)
1438
- return; // the element disconnected while the pick was in flight
1439
- const entityElement = this._elementWithListener(node, 'pointerdown');
1440
- if (entityElement) {
1441
- entityElement.dispatchEvent(new PointerEvent('pointerdown', event));
1442
- }
1812
+ // Picks stay concurrent - only the dispatch of the results is serialized
1813
+ const pick = this._pickNode(event);
1814
+ // A click concludes on the matching pointerup, which needs to know what the press
1815
+ // picked. Primary button only - the only button a click can conclude from - and only
1816
+ // while click is listened for, since it is the click mapping that keeps the pointerup
1817
+ // and pointercancel listeners attached to clean the entry up again.
1818
+ if (this._clickListened && event.button === 0) {
1819
+ this._downPicks.set(event.pointerId, pick);
1820
+ }
1821
+ this._chainDispatch(async () => {
1822
+ const node = await pick;
1823
+ if (!this._picker)
1824
+ return; // the element disconnected while the pick was in flight
1825
+ const entityElement = this._elementWithListener(node, 'pointerdown');
1826
+ if (entityElement) {
1827
+ entityElement.dispatchEvent(new PointerEvent('pointerdown', event));
1828
+ }
1829
+ });
1443
1830
  }
1444
- async _onPointerUp(event) {
1831
+ _onPointerUp(event) {
1445
1832
  if (!this._picker || !this.app)
1446
1833
  return;
1447
- const node = await this._pickNode(event);
1448
- if (!this._picker)
1449
- return; // the element disconnected while the pick was in flight
1450
- const entityElement = this._elementWithListener(node, 'pointerup');
1451
- if (entityElement) {
1452
- entityElement.dispatchEvent(new PointerEvent('pointerup', event));
1453
- }
1454
- }
1455
- _onPointerListenerAdded(type) {
1456
- if (!this._hasPointerListeners[type] && this._canvas) {
1457
- this._hasPointerListeners[type] = true;
1458
- // For enter/leave events, we need the move handler
1459
- const handler = type === 'pointerenter' || type === 'pointerleave'
1460
- ? this._pointerHandlers.pointermove
1461
- : this._pointerHandlers[type];
1462
- if (handler) {
1463
- this._canvas.addEventListener(type === 'pointerenter' || type === 'pointerleave' ? 'pointermove' : type, handler);
1834
+ // The press pick this release may conclude as a click. Claimed synchronously, so the
1835
+ // entry is gone before any other event for this pointer can be handled.
1836
+ const downPick = this._downPicks.get(event.pointerId);
1837
+ this._downPicks.delete(event.pointerId);
1838
+ const pick = this._pickNode(event);
1839
+ this._chainDispatch(async () => {
1840
+ const node = await pick;
1841
+ if (!this._picker)
1842
+ return; // the element disconnected while the pick was in flight
1843
+ const entityElement = this._elementWithListener(node, 'pointerup');
1844
+ if (entityElement) {
1845
+ entityElement.dispatchEvent(new PointerEvent('pointerup', event));
1464
1846
  }
1465
- }
1847
+ });
1848
+ // A click fires where the DOM fires it: at the nearest common inclusive ancestor of
1849
+ // what the press and the release picked, for the primary button only. Appended after
1850
+ // the release's own step, so it dispatches after the pointerup that concludes it.
1851
+ if (!downPick || event.button !== 0)
1852
+ return;
1853
+ this._chainDispatch(async () => {
1854
+ // A rejected pick was already reported by the press or release step that awaited it;
1855
+ // here it just means no click can conclude.
1856
+ const picked = await Promise.all([downPick, pick]).catch(() => null);
1857
+ if (!picked || !this._picker)
1858
+ return;
1859
+ const [downNode, upNode] = picked;
1860
+ const clickElement = this._elementWithListener(commonAncestor(downNode, upNode), 'click');
1861
+ if (clickElement) {
1862
+ const click = new PointerEvent('click', event);
1863
+ // The init above copied pointerup's `detail`, which the Pointer Events spec fixes
1864
+ // at 0 - but click is exempt: its detail is the click count, chained here as the
1865
+ // platform chains it (same target, within the double-click window). Overridden
1866
+ // with defineProperty because an event instance used as an init dict cannot have
1867
+ // single fields replaced.
1868
+ const time = performance.now();
1869
+ const last = this._lastClick;
1870
+ const count = last && last.element === clickElement && time - last.time <= CLICK_CHAIN_MS ? last.count + 1 : 1;
1871
+ this._lastClick = { element: clickElement, time, count };
1872
+ Object.defineProperty(click, 'detail', { value: count });
1873
+ clickElement.dispatchEvent(click);
1874
+ }
1875
+ });
1466
1876
  }
1467
- _onPointerListenerRemoved(type) {
1468
- const hasListeners = Array.from(this.querySelectorAll('pc-entity, pc-model, pc-node')).some((entity) => entity._hasListeners(type));
1469
- if (!hasListeners && this._canvas) {
1470
- this._hasPointerListeners[type] = false;
1471
- const handler = type === 'pointerenter' || type === 'pointerleave'
1472
- ? this._pointerHandlers.pointermove
1473
- : this._pointerHandlers[type];
1474
- if (handler) {
1475
- this._canvas.removeEventListener(type === 'pointerenter' || type === 'pointerleave' ? 'pointermove' : type, handler);
1877
+ /**
1878
+ * Attaches exactly the canvas listeners the tree's current element listeners need, and
1879
+ * detaches the rest. Recomputed whenever a listener connects or disconnects anywhere under
1880
+ * this element: several synthesized types can need the same canvas listener (enter, leave
1881
+ * and move all ride the move pick; click rides the down/up pair), so one type's removal
1882
+ * must not detach a listener another type still uses. Re-attaching an attached listener is
1883
+ * a no-op by EventTarget semantics, so no attach state is kept.
1884
+ */
1885
+ _syncCanvasListeners() {
1886
+ const canvas = this._canvas;
1887
+ if (!canvas)
1888
+ return; // not booted yet: _pickerCreate syncs once the handlers exist
1889
+ const elements = Array.from(this.querySelectorAll('pc-entity, pc-model, pc-node'));
1890
+ const needed = new Set();
1891
+ for (const type of SYNTHESIZED_EVENTS) {
1892
+ if (elements.some((element) => element._hasListeners(type))) {
1893
+ canvasEventsFor[type].forEach((canvasType) => needed.add(canvasType));
1476
1894
  }
1477
1895
  }
1896
+ this._clickListened = elements.some((element) => element._hasListeners('click'));
1897
+ Object.entries(this._pointerHandlers).forEach(([canvasType, handler]) => {
1898
+ if (!handler)
1899
+ return;
1900
+ if (needed.has(canvasType)) {
1901
+ canvas.addEventListener(canvasType, handler);
1902
+ }
1903
+ else {
1904
+ canvas.removeEventListener(canvasType, handler);
1905
+ }
1906
+ });
1478
1907
  }
1479
1908
  /**
1480
1909
  * Warns that a graphics option was written too late to have any effect. These options are read
@@ -1643,126 +2072,6 @@ class AppElement extends AsyncElement {
1643
2072
  }
1644
2073
  customElements.define('pc-app', AppElement);
1645
2074
 
1646
- /**
1647
- * The attribute names of the inline `onpointer*` event handlers, shared by every element that
1648
- * fronts an engine entity. Spread into `observedAttributes` by subclasses.
1649
- * @internal
1650
- */
1651
- const POINTER_ATTRIBUTES = [
1652
- 'onpointerenter',
1653
- 'onpointerleave',
1654
- 'onpointerdown',
1655
- 'onpointerup',
1656
- 'onpointermove'
1657
- ];
1658
- /**
1659
- * The base class for elements that front an engine {@link Entity}: `<pc-entity>` and
1660
- * `<pc-model>`, which create one, and `<pc-node>`, which binds to one inside a model's
1661
- * instantiated hierarchy. It carries what all of them need — the `entity` contract, registration
1662
- * with the owning application (which joins picked scene nodes back to elements by identity,
1663
- * never by name), and the pointer listener bookkeeping that lets the application lazily attach
1664
- * its canvas handlers.
1665
- */
1666
- class EntityBaseElement extends AsyncElement {
1667
- _entity = null;
1668
- /**
1669
- * The application element this entity is registered with, cached at registration time so the
1670
- * entity can be unregistered even once this element has left the DOM.
1671
- */
1672
- _appElement = null;
1673
- /**
1674
- * The pointer event listeners for the entity.
1675
- */
1676
- _listeners = {};
1677
- /**
1678
- * The event types for which an inline `onpointer*` attribute is currently present.
1679
- */
1680
- _inlineHandlerTypes = new Set();
1681
- /**
1682
- * The PlayCanvas entity instance. `null` until the element is ready, and again once the
1683
- * entity is gone — await {@link whenReady} or the element's `ready()` promise before
1684
- * accessing it.
1685
- * @returns The entity instance, or `null`.
1686
- */
1687
- get entity() {
1688
- return this._entity;
1689
- }
1690
- /**
1691
- * Registers `entity` as this element's backing entity with the owning application, which
1692
- * joins engine nodes back to elements by identity (never by name).
1693
- *
1694
- * @param entity - The entity to register.
1695
- */
1696
- _registerEntity(entity) {
1697
- this._appElement = this.closestApp;
1698
- this._appElement?._registerEntityElement(entity, this);
1699
- }
1700
- /**
1701
- * Removes the registration for `entity`.
1702
- *
1703
- * @param entity - The entity to unregister.
1704
- */
1705
- _unregisterEntity(entity) {
1706
- this._appElement?._unregisterEntityElement(entity);
1707
- this._appElement = null;
1708
- }
1709
- /**
1710
- * Tracks whether an inline `onpointer*` attribute is present. The browser itself compiles and
1711
- * runs these attributes — they are standard `GlobalEventHandlers`, so setting one replaces
1712
- * the previous handler and removing it removes the handler, exactly like `onclick` on any
1713
- * HTML element. But because they bypass {@link EventTarget.addEventListener}, the connect/disconnect
1714
- * bookkeeping that lets the application lazily attach its canvas pointer handlers must be
1715
- * kept in sync here.
1716
- *
1717
- * @param name - The attribute name (e.g. 'onpointerdown').
1718
- * @param value - The attribute value, or `null` when the attribute has been removed.
1719
- */
1720
- _updateInlineHandler(name, value) {
1721
- const type = name.substring(2);
1722
- const had = this._inlineHandlerTypes.has(type);
1723
- const has = value !== null;
1724
- if (has && !had) {
1725
- this._inlineHandlerTypes.add(type);
1726
- this.dispatchEvent(new CustomEvent(`${type}:connect`, { bubbles: true }));
1727
- }
1728
- else if (!has && had) {
1729
- this._inlineHandlerTypes.delete(type);
1730
- this.dispatchEvent(new CustomEvent(`${type}:disconnect`, { bubbles: true }));
1731
- }
1732
- }
1733
- addEventListener(type, listener, options) {
1734
- if (!this._listeners[type]) {
1735
- this._listeners[type] = [];
1736
- }
1737
- this._listeners[type].push(listener);
1738
- super.addEventListener(type, listener, options);
1739
- if (type.startsWith('pointer')) {
1740
- this.dispatchEvent(new CustomEvent(`${type}:connect`, { bubbles: true }));
1741
- }
1742
- }
1743
- removeEventListener(type, listener, options) {
1744
- if (this._listeners[type]) {
1745
- this._listeners[type] = this._listeners[type].filter((l) => l !== listener);
1746
- }
1747
- super.removeEventListener(type, listener, options);
1748
- if (type.startsWith('pointer')) {
1749
- this.dispatchEvent(new CustomEvent(`${type}:disconnect`, { bubbles: true }));
1750
- }
1751
- }
1752
- /**
1753
- * Whether the element has a listener for an event type, registered either with
1754
- * {@link EventTarget.addEventListener} or with the matching inline `onpointer*` attribute. Read by the
1755
- * containing `<pc-app>` element to gate pointer event synthesis.
1756
- *
1757
- * @param type - The event type.
1758
- * @returns Whether a listener is registered.
1759
- * @internal
1760
- */
1761
- _hasListeners(type) {
1762
- return Boolean(this._listeners[type]?.length) || this._inlineHandlerTypes.has(type);
1763
- }
1764
- }
1765
-
1766
2075
  /**
1767
2076
  * Creates and parents the entities of every descendant entity-owning element of `root`, in two
1768
2077
  * passes so that no parent's existence depends on document order. Called wherever a subtree could
@@ -2030,8 +2339,8 @@ class EntityOwnerElement extends EntityBaseElement {
2030
2339
  *
2031
2340
  * The pointer events below are dispatched by the containing `<pc-app>` element when the pointer
2032
2341
  * intersects this entity's geometry. They are only generated while the entity has a listener for
2033
- * them, registered either with {@link EventTarget.addEventListener} or with the matching inline `onpointer*`
2034
- * attribute.
2342
+ * them, registered either with {@link EventTarget.addEventListener} or with the matching inline
2343
+ * attribute (`onpointerdown`, `onclick`, ...).
2035
2344
  *
2036
2345
  * @elementSummary The `<pc-entity>` element creates an entity: a named, transformable node of the
2037
2346
  * scene hierarchy, and the host for component elements such as `<pc-camera>`, `<pc-light>` and
@@ -2051,11 +2360,17 @@ class EntityOwnerElement extends EntityBaseElement {
2051
2360
  * entity.
2052
2361
  * @attribute {string} onpointerup - Script to run when a pointer button is released over the
2053
2362
  * entity.
2363
+ * @attribute {string} onclick - Script to run when the entity is clicked: a primary pointer
2364
+ * button pressed and then released over it.
2054
2365
  * @fires {PointerEvent} pointerenter - Fired when the pointer moves onto the entity.
2055
2366
  * @fires {PointerEvent} pointerleave - Fired when the pointer moves off the entity.
2056
2367
  * @fires {PointerEvent} pointermove - Fired when the pointer moves over the entity.
2057
2368
  * @fires {PointerEvent} pointerdown - Fired when a pointer button is pressed over the entity.
2058
2369
  * @fires {PointerEvent} pointerup - Fired when a pointer button is released over the entity.
2370
+ * @fires {PointerEvent} click - Fired when a primary pointer button is pressed and then released
2371
+ * over the entity. A press and release that picked different entities fires on their nearest
2372
+ * common ancestor instead, as in the DOM. `detail` carries the click count, so a double click
2373
+ * arrives as a click whose `detail` is 2.
2059
2374
  */
2060
2375
  class EntityElement extends EntityOwnerElement {
2061
2376
  connectedCallback() {
@@ -2090,7 +2405,7 @@ class EntityElement extends EntityOwnerElement {
2090
2405
  this._entity?.destroy();
2091
2406
  }
2092
2407
  static get observedAttributes() {
2093
- return ['enabled', 'name', 'position', 'rotation', 'scale', 'tags', ...POINTER_ATTRIBUTES];
2408
+ return ['enabled', 'name', 'position', 'rotation', 'scale', 'tags', ...EVENT_ATTRIBUTES];
2094
2409
  }
2095
2410
  attributeChangedCallback(name, _oldValue, newValue) {
2096
2411
  switch (name) {
@@ -2117,6 +2432,7 @@ class EntityElement extends EntityOwnerElement {
2117
2432
  case 'onpointerdown':
2118
2433
  case 'onpointerup':
2119
2434
  case 'onpointermove':
2435
+ case 'onclick':
2120
2436
  this._updateInlineHandler(name, newValue);
2121
2437
  break;
2122
2438
  }
@@ -3066,11 +3382,17 @@ const formatHierarchy = (root, counts) => {
3066
3382
  * model.
3067
3383
  * @attribute {string} onpointerup - Script to run when a pointer button is released over the
3068
3384
  * model.
3385
+ * @attribute {string} onclick - Script to run when the model is clicked: a primary pointer
3386
+ * button pressed and then released over it.
3069
3387
  * @fires {PointerEvent} pointerenter - Fired when the pointer moves onto the model.
3070
3388
  * @fires {PointerEvent} pointerleave - Fired when the pointer moves off the model.
3071
3389
  * @fires {PointerEvent} pointermove - Fired when the pointer moves over the model.
3072
3390
  * @fires {PointerEvent} pointerdown - Fired when a pointer button is pressed over the model.
3073
3391
  * @fires {PointerEvent} pointerup - Fired when a pointer button is released over the model.
3392
+ * @fires {PointerEvent} click - Fired when a primary pointer button is pressed and then released
3393
+ * over the model. A press and release that picked different entities fires on their nearest
3394
+ * common ancestor instead, as in the DOM. `detail` carries the click count, so a double click
3395
+ * arrives as a click whose `detail` is 2.
3074
3396
  * @fires {Event} load - Fired each time a container asset finishes instantiating, including
3075
3397
  * re-instantiation after `asset` changes. Does not bubble — listen on this element, or use a
3076
3398
  * capture-phase listener on an ancestor.
@@ -3319,7 +3641,7 @@ class ModelElement extends EntityOwnerElement {
3319
3641
  return this._asset;
3320
3642
  }
3321
3643
  static get observedAttributes() {
3322
- return ['asset', 'enabled', 'name', 'position', 'rotation', 'scale', 'tags', ...POINTER_ATTRIBUTES];
3644
+ return ['asset', 'enabled', 'name', 'position', 'rotation', 'scale', 'tags', ...EVENT_ATTRIBUTES];
3323
3645
  }
3324
3646
  attributeChangedCallback(name, _oldValue, newValue) {
3325
3647
  switch (name) {
@@ -3349,6 +3671,7 @@ class ModelElement extends EntityOwnerElement {
3349
3671
  case 'onpointerdown':
3350
3672
  case 'onpointerup':
3351
3673
  case 'onpointermove':
3674
+ case 'onclick':
3352
3675
  this._updateInlineHandler(name, newValue);
3353
3676
  break;
3354
3677
  }
@@ -4212,8 +4535,6 @@ customElements.define('pc-anim', AnimComponentElement);
4212
4535
  * @elementSummary The `<pc-anim-clip>` element declares one named animation clip on its parent
4213
4536
  * `<pc-anim>`, taken from the `asset` it names or, without one, from the enclosing `<pc-model>`'s
4214
4537
  * own animations. Must be a direct child of `<pc-anim>`.
4215
- *
4216
- * @category Components
4217
4538
  */
4218
4539
  class AnimClipElement extends AsyncElement {
4219
4540
  /**
@@ -4615,7 +4936,9 @@ class ButtonComponentElement extends ComponentElement {
4615
4936
  };
4616
4937
  // The image entity defaults to the button's own entity (which carries the image element)
4617
4938
  // when no explicit reference is provided.
4618
- const imageEntity = this._image ? getEntity(this._image) : this.closestEntity?.entity;
4939
+ const imageEntity = this._image
4940
+ ? resolveEntity(this._image, this, 'image', 'reference ignored')
4941
+ : this.closestEntity?.entity;
4619
4942
  if (imageEntity) {
4620
4943
  data.imageEntity = imageEntity;
4621
4944
  }
@@ -4658,21 +4981,27 @@ class ButtonComponentElement extends ComponentElement {
4658
4981
  return this._active;
4659
4982
  }
4660
4983
  /**
4661
- * Sets the reference (CSS selector, element id or entity name) to the `<pc-entity>` whose image
4662
- * element is used for visual transitions. Defaults to the button's own entity — inside a
4663
- * `<pc-model>`, that is the model's host entity, so supply an explicit reference to target a
4664
- * UI entity instead.
4984
+ * Sets the reference (a `pc-entity`, `pc-model` or `pc-node` name, or a document-wide `#`
4985
+ * selector) to the entity whose image element is used for visual transitions. An exact name
4986
+ * resolves against the nearest enclosing entity first, then outward, then the document.
4987
+ * Defaults to the button's own entity — inside a `<pc-model>`, that is the model's host
4988
+ * entity, so supply an explicit reference to target a UI entity instead. A non-empty
4989
+ * reference that does not resolve warns and is ignored.
4665
4990
  * @param value - The image entity reference.
4666
4991
  */
4667
4992
  set image(value) {
4668
4993
  this._image = value;
4669
- const entity = getEntity(value);
4670
- if (this.component && entity) {
4671
- this.component.imageEntity = entity;
4994
+ if (this.component) {
4995
+ const entity = resolveEntity(value, this, 'image', 'reference ignored');
4996
+ if (entity) {
4997
+ this.component.imageEntity = entity;
4998
+ }
4672
4999
  }
4673
5000
  }
4674
5001
  /**
4675
- * Gets the reference to the `<pc-entity>` whose image element is used for visual transitions.
5002
+ * Gets the reference (a `pc-entity`, `pc-model` or `pc-node` name, or a document-wide `#`
5003
+ * selector) to the entity whose image element is used for visual transitions, or empty for
5004
+ * the button's own entity.
4676
5005
  * @returns The image entity reference.
4677
5006
  */
4678
5007
  get image() {
@@ -6400,7 +6729,12 @@ customElements.define('pc-element', ElementComponentElement);
6400
6729
  * primary axis: a hinge rotates about it, a slider translates along it and a ball joint twists
6401
6730
  * about it. The constrained bodies are referenced by `entity-a` and `entity-b`, both of which need
6402
6731
  * a rigid body component; leaving `entity-b` empty constrains `entity-a` to a fixed point in world
6403
- * space. The underlying engine component is in alpha, so its API may change.
6732
+ * space. A reference can name any entity-fronting element `<pc-entity>`, `<pc-model>` or
6733
+ * `<pc-node>`, so a ragdoll can join a model's own skeleton nodes by name — and a name resolves
6734
+ * against the nearest enclosing entity first, then outward through the entity hierarchy, then the
6735
+ * document, while a `#` selector resolves document-wide. A `<template>` prefab with one
6736
+ * entity-fronting root can therefore wire its joints by name and stay self-contained when cloned.
6737
+ * The underlying engine component is in alpha, so its API may change.
6404
6738
  *
6405
6739
  * @elementSummary The `<pc-joint>` element constrains two rigid bodies to each other — a hinged
6406
6740
  * door, a swinging chain, a sliding drawer. Its entity's transform is the joint frame, and
@@ -6554,8 +6888,8 @@ class JointComponentElement extends ComponentElement {
6554
6888
  breakImpulse: this._breakImpulse,
6555
6889
  enableCollision: this._enableCollision,
6556
6890
  enableLimits: this._enableLimits,
6557
- entityA: getEntity(this._entityA),
6558
- entityB: getEntity(this._entityB),
6891
+ entityA: resolveEntity(this._entityA, this, 'entity-a', 'constraint not created'),
6892
+ entityB: resolveEntity(this._entityB, this, 'entity-b', 'constraint not created'),
6559
6893
  limits: this._limits,
6560
6894
  linearDamping: this._linearDamping,
6561
6895
  linearEquilibrium: this._linearEquilibrium,
@@ -6810,39 +7144,48 @@ class JointComponentElement extends ComponentElement {
6810
7144
  return this._enableLimits;
6811
7145
  }
6812
7146
  /**
6813
- * Sets the reference (CSS selector, element id or entity name) to the `<pc-entity>` providing
6814
- * the first constrained body. The reference resolves when it is set, so an entity created
6815
- * later is picked up by setting the attribute again.
7147
+ * Sets the reference (a `pc-entity`, `pc-model` or `pc-node` name, or a document-wide `#`
7148
+ * selector) to the element providing the first constrained body. An exact name resolves
7149
+ * against the nearest enclosing entity first, then outward, then the document. The reference
7150
+ * resolves when it is set, so an entity created later is picked up by setting the attribute
7151
+ * again. A non-empty reference that does not resolve warns, naming which of the two causes it
7152
+ * hit.
6816
7153
  * @param value - The first body's entity reference.
6817
7154
  */
6818
7155
  set entityA(value) {
6819
7156
  this._entityA = value;
6820
7157
  if (this.component) {
6821
- this.component.entityA = getEntity(value);
7158
+ this.component.entityA = resolveEntity(value, this, 'entity-a', 'constraint not created');
6822
7159
  }
6823
7160
  }
6824
7161
  /**
6825
- * Gets the reference to the `<pc-entity>` providing the first constrained body.
7162
+ * Gets the reference (a `pc-entity`, `pc-model` or `pc-node` name, or a document-wide `#`
7163
+ * selector) to the element providing the first constrained body.
6826
7164
  * @returns The first body's entity reference.
6827
7165
  */
6828
7166
  get entityA() {
6829
7167
  return this._entityA;
6830
7168
  }
6831
7169
  /**
6832
- * Sets the reference (CSS selector, element id or entity name) to the `<pc-entity>` providing
6833
- * the second constrained body, or empty to constrain the first body to a fixed point in world
6834
- * space. The reference resolves when it is set, so an entity created later is picked up by
6835
- * setting the attribute again.
7170
+ * Sets the reference (a `pc-entity`, `pc-model` or `pc-node` name, or a document-wide `#`
7171
+ * selector) to the element providing the second constrained body, or empty to constrain the
7172
+ * first body to a fixed point in world space. An exact name resolves against the nearest
7173
+ * enclosing entity first, then outward, then the document. The reference resolves when it is
7174
+ * set, so an entity created later is picked up by setting the attribute again. A non-empty
7175
+ * reference that does not resolve warns; an empty one is the documented world-space case and
7176
+ * stays silent.
6836
7177
  * @param value - The second body's entity reference.
6837
7178
  */
6838
7179
  set entityB(value) {
6839
7180
  this._entityB = value;
6840
7181
  if (this.component) {
6841
- this.component.entityB = getEntity(value);
7182
+ this.component.entityB = resolveEntity(value, this, 'entity-b', 'constraint not created');
6842
7183
  }
6843
7184
  }
6844
7185
  /**
6845
- * Gets the reference to the `<pc-entity>` providing the second constrained body.
7186
+ * Gets the reference (a `pc-entity`, `pc-model` or `pc-node` name, or a document-wide `#`
7187
+ * selector) to the element providing the second constrained body, or empty for the
7188
+ * world-space case.
6846
7189
  * @returns The second body's entity reference.
6847
7190
  */
6848
7191
  get entityB() {
@@ -11216,7 +11559,7 @@ class ScrollbarComponentElement extends ComponentElement {
11216
11559
  value: this._value,
11217
11560
  handleSize: this._handleSize
11218
11561
  };
11219
- const handle = getEntity(this._handle);
11562
+ const handle = resolveEntity(this._handle, this, 'handle', 'reference ignored');
11220
11563
  if (handle) {
11221
11564
  data.handleEntity = handle;
11222
11565
  }
@@ -11282,19 +11625,24 @@ class ScrollbarComponentElement extends ComponentElement {
11282
11625
  return this._handleSize;
11283
11626
  }
11284
11627
  /**
11285
- * Sets the reference (CSS selector, element id or entity name) to the `<pc-entity>` used as the
11286
- * scrollbar handle.
11628
+ * Sets the reference (a `pc-entity`, `pc-model` or `pc-node` name, or a document-wide `#`
11629
+ * selector) to the entity used as the scrollbar handle. An exact name resolves against the
11630
+ * nearest enclosing entity first, then outward, then the document. A non-empty reference that
11631
+ * does not resolve warns and is ignored.
11287
11632
  * @param value - The handle entity reference.
11288
11633
  */
11289
11634
  set handle(value) {
11290
11635
  this._handle = value;
11291
- const entity = getEntity(value);
11292
- if (this.component && entity) {
11293
- this.component.handleEntity = entity;
11636
+ if (this.component) {
11637
+ const entity = resolveEntity(value, this, 'handle', 'reference ignored');
11638
+ if (entity) {
11639
+ this.component.handleEntity = entity;
11640
+ }
11294
11641
  }
11295
11642
  }
11296
11643
  /**
11297
- * Gets the reference to the `<pc-entity>` used as the scrollbar handle.
11644
+ * Gets the reference (a `pc-entity`, `pc-model` or `pc-node` name, or a document-wide `#`
11645
+ * selector) to the entity used as the scrollbar handle.
11298
11646
  * @returns The handle entity reference.
11299
11647
  */
11300
11648
  get handle() {
@@ -11376,19 +11724,19 @@ class ScrollViewComponentElement extends ComponentElement {
11376
11724
  horizontalScrollbarVisibility: visibilities.get(this._horizontalScrollbarVisibility),
11377
11725
  verticalScrollbarVisibility: visibilities.get(this._verticalScrollbarVisibility)
11378
11726
  };
11379
- const viewport = getEntity(this._viewport);
11727
+ const viewport = resolveEntity(this._viewport, this, 'viewport', 'reference ignored');
11380
11728
  if (viewport) {
11381
11729
  data.viewportEntity = viewport;
11382
11730
  }
11383
- const content = getEntity(this._content);
11731
+ const content = resolveEntity(this._content, this, 'content', 'reference ignored');
11384
11732
  if (content) {
11385
11733
  data.contentEntity = content;
11386
11734
  }
11387
- const horizontalScrollbar = getEntity(this._horizontalScrollbar);
11735
+ const horizontalScrollbar = resolveEntity(this._horizontalScrollbar, this, 'horizontal-scrollbar', 'reference ignored');
11388
11736
  if (horizontalScrollbar) {
11389
11737
  data.horizontalScrollbarEntity = horizontalScrollbar;
11390
11738
  }
11391
- const verticalScrollbar = getEntity(this._verticalScrollbar);
11739
+ const verticalScrollbar = resolveEntity(this._verticalScrollbar, this, 'vertical-scrollbar', 'reference ignored');
11392
11740
  if (verticalScrollbar) {
11393
11741
  data.verticalScrollbarEntity = verticalScrollbar;
11394
11742
  }
@@ -11564,76 +11912,96 @@ class ScrollViewComponentElement extends ComponentElement {
11564
11912
  return this._verticalScrollbarVisibility;
11565
11913
  }
11566
11914
  /**
11567
- * Sets the reference (CSS selector, element id or entity name) to the `<pc-entity>` used as the
11568
- * viewport, which clips the content to the scroll view's bounds.
11915
+ * Sets the reference (a `pc-entity`, `pc-model` or `pc-node` name, or a document-wide `#`
11916
+ * selector) to the entity used as the viewport, which clips the content to the scroll view's
11917
+ * bounds. An exact name resolves against the nearest enclosing entity first, then outward,
11918
+ * then the document. A non-empty reference that does not resolve warns and is ignored.
11569
11919
  * @param value - The viewport entity reference.
11570
11920
  */
11571
11921
  set viewport(value) {
11572
11922
  this._viewport = value;
11573
- const entity = getEntity(value);
11574
- if (this.component && entity) {
11575
- this.component.viewportEntity = entity;
11923
+ if (this.component) {
11924
+ const entity = resolveEntity(value, this, 'viewport', 'reference ignored');
11925
+ if (entity) {
11926
+ this.component.viewportEntity = entity;
11927
+ }
11576
11928
  }
11577
11929
  }
11578
11930
  /**
11579
- * Gets the reference to the `<pc-entity>` used as the viewport.
11931
+ * Gets the reference (a `pc-entity`, `pc-model` or `pc-node` name, or a document-wide `#`
11932
+ * selector) to the entity used as the viewport.
11580
11933
  * @returns The viewport entity reference.
11581
11934
  */
11582
11935
  get viewport() {
11583
11936
  return this._viewport;
11584
11937
  }
11585
11938
  /**
11586
- * Sets the reference (CSS selector, element id or entity name) to the `<pc-entity>` used as the
11587
- * content, which is moved as the scroll view is scrolled.
11939
+ * Sets the reference (a `pc-entity`, `pc-model` or `pc-node` name, or a document-wide `#`
11940
+ * selector) to the entity used as the content, which is moved as the scroll view is
11941
+ * scrolled. An exact name resolves against the nearest enclosing entity first, then outward,
11942
+ * then the document. A non-empty reference that does not resolve warns and is ignored.
11588
11943
  * @param value - The content entity reference.
11589
11944
  */
11590
11945
  set content(value) {
11591
11946
  this._content = value;
11592
- const entity = getEntity(value);
11593
- if (this.component && entity) {
11594
- this.component.contentEntity = entity;
11947
+ if (this.component) {
11948
+ const entity = resolveEntity(value, this, 'content', 'reference ignored');
11949
+ if (entity) {
11950
+ this.component.contentEntity = entity;
11951
+ }
11595
11952
  }
11596
11953
  }
11597
11954
  /**
11598
- * Gets the reference to the `<pc-entity>` used as the content.
11955
+ * Gets the reference (a `pc-entity`, `pc-model` or `pc-node` name, or a document-wide `#`
11956
+ * selector) to the entity used as the content.
11599
11957
  * @returns The content entity reference.
11600
11958
  */
11601
11959
  get content() {
11602
11960
  return this._content;
11603
11961
  }
11604
11962
  /**
11605
- * Sets the reference (CSS selector, element id or entity name) to the `<pc-entity>` containing
11606
- * the horizontal `<pc-scrollbar>`.
11963
+ * Sets the reference (a `pc-entity`, `pc-model` or `pc-node` name, or a document-wide `#`
11964
+ * selector) to the entity containing the horizontal `<pc-scrollbar>`. An exact name resolves
11965
+ * against the nearest enclosing entity first, then outward, then the document. A non-empty
11966
+ * reference that does not resolve warns and is ignored.
11607
11967
  * @param value - The horizontal scrollbar entity reference.
11608
11968
  */
11609
11969
  set horizontalScrollbar(value) {
11610
11970
  this._horizontalScrollbar = value;
11611
- const entity = getEntity(value);
11612
- if (this.component && entity) {
11613
- this.component.horizontalScrollbarEntity = entity;
11971
+ if (this.component) {
11972
+ const entity = resolveEntity(value, this, 'horizontal-scrollbar', 'reference ignored');
11973
+ if (entity) {
11974
+ this.component.horizontalScrollbarEntity = entity;
11975
+ }
11614
11976
  }
11615
11977
  }
11616
11978
  /**
11617
- * Gets the reference to the `<pc-entity>` containing the horizontal scrollbar.
11979
+ * Gets the reference (a `pc-entity`, `pc-model` or `pc-node` name, or a document-wide `#`
11980
+ * selector) to the entity containing the horizontal scrollbar.
11618
11981
  * @returns The horizontal scrollbar entity reference.
11619
11982
  */
11620
11983
  get horizontalScrollbar() {
11621
11984
  return this._horizontalScrollbar;
11622
11985
  }
11623
11986
  /**
11624
- * Sets the reference (CSS selector, element id or entity name) to the `<pc-entity>` containing
11625
- * the vertical `<pc-scrollbar>`.
11987
+ * Sets the reference (a `pc-entity`, `pc-model` or `pc-node` name, or a document-wide `#`
11988
+ * selector) to the entity containing the vertical `<pc-scrollbar>`. An exact name resolves
11989
+ * against the nearest enclosing entity first, then outward, then the document. A non-empty
11990
+ * reference that does not resolve warns and is ignored.
11626
11991
  * @param value - The vertical scrollbar entity reference.
11627
11992
  */
11628
11993
  set verticalScrollbar(value) {
11629
11994
  this._verticalScrollbar = value;
11630
- const entity = getEntity(value);
11631
- if (this.component && entity) {
11632
- this.component.verticalScrollbarEntity = entity;
11995
+ if (this.component) {
11996
+ const entity = resolveEntity(value, this, 'vertical-scrollbar', 'reference ignored');
11997
+ if (entity) {
11998
+ this.component.verticalScrollbarEntity = entity;
11999
+ }
11633
12000
  }
11634
12001
  }
11635
12002
  /**
11636
- * Gets the reference to the `<pc-entity>` containing the vertical scrollbar.
12003
+ * Gets the reference (a `pc-entity`, `pc-model` or `pc-node` name, or a document-wide `#`
12004
+ * selector) to the entity containing the vertical scrollbar.
11637
12005
  * @returns The vertical scrollbar entity reference.
11638
12006
  */
11639
12007
  get verticalScrollbar() {
@@ -11716,7 +12084,9 @@ customElements.define('pc-scroll-view', ScrollViewComponentElement);
11716
12084
  * Values are parsed according to the type of the attribute's current value — initially the
11717
12085
  * script's declared default (numbers, booleans, strings, Vec2/3/4, Color, Quat as Euler
11718
12086
  * angles) — and the `asset:`/`entity:`/`vec2:`/`vec3:`/`vec4:`/`color:` prefixes may be used
11719
- * to be explicit.
12087
+ * to be explicit. An `entity:` reference is an entity name — resolved against the nearest
12088
+ * enclosing entity first, then outward, then the document — or a document-wide `#` selector
12089
+ * (`entity:#id`); a bare value is always a name, never an element id.
11720
12090
  * - **The `attributes` JSON attribute**: an object supporting nested structures and attribute
11721
12091
  * names that collide with reserved HTML attribute names (e.g. `title`).
11722
12092
  *
@@ -11732,7 +12102,8 @@ customElements.define('pc-scroll-view', ScrollViewComponentElement);
11732
12102
  *
11733
12103
  * @elementSummary The `<pc-script-instance>` element attaches one script class, named by `name`, to
11734
12104
  * the entity of its parent `<pc-script>`. Its other attributes set script attributes of the same
11735
- * name, and `attributes` takes a JSON object instead. Must be a direct child of `<pc-script>`.
12105
+ * name, and `attributes` takes a JSON object instead. An `entity:` value is an entity name —
12106
+ * write `entity:#id` for an element id. Must be a direct child of `<pc-script>`.
11736
12107
  *
11737
12108
  * @fires {CustomEvent} scriptattributeschange - Fired when the script's attributes change. The
11738
12109
  * `detail` carries the new `attributes` object. Bubbles.
@@ -11752,9 +12123,11 @@ class ScriptInstanceElement extends AsyncElement {
11752
12123
  /**
11753
12124
  * Sets the attributes of the script as an object. Values are converted with the same rules
11754
12125
  * as the `attributes` attribute: `asset:`/`entity:` references and `vec2:`/`vec3:`/`vec4:`/
11755
- * `color:` prefixed strings are resolved, and a plain numeric array is converted to the
11756
- * type of the attribute it targets when that attribute currently holds a Vec2, Vec3, Vec4
11757
- * or Color.
12126
+ * `color:` prefixed strings are resolved (an entity name against the nearest enclosing
12127
+ * entity first, then outward, then the document or a document-wide `#` selector; a bare
12128
+ * value is always a name, never an element id), and a plain numeric array is converted to
12129
+ * the type of the attribute it targets when that attribute currently holds a Vec2, Vec3,
12130
+ * Vec4 or Color.
11758
12131
  * @param value - The attributes of the script.
11759
12132
  */
11760
12133
  set scriptAttributes(value) {
@@ -11765,7 +12138,10 @@ class ScriptInstanceElement extends AsyncElement {
11765
12138
  }));
11766
12139
  }
11767
12140
  /**
11768
- * Gets the attributes of the script.
12141
+ * Gets the attributes of the script as an object whose `asset:`, `entity:`, `vec2:`, `vec3:`,
12142
+ * `vec4:` and `color:` prefixed values are resolved when applied — an `entity:` value being
12143
+ * an entity name (nearest enclosing entity first, then outward, then the document) or a
12144
+ * document-wide `#` selector (`entity:#id`), never a bare element id.
11769
12145
  * @returns The attributes of the script.
11770
12146
  */
11771
12147
  get scriptAttributes() {
@@ -11976,18 +12352,23 @@ const assetConversion = (rest, raw) => {
11976
12352
  return raw;
11977
12353
  };
11978
12354
  /**
11979
- * Resolves an `entity:` prefix to the Entity backing a `pc-entity` element. The reference can be a
11980
- * CSS selector, an element id or an entity name.
12355
+ * Resolves an `entity:` prefix to the Entity backing a `pc-entity`, `pc-model` or `pc-node`
12356
+ * element. The reference is a name resolved against the nearest enclosing entity first, then
12357
+ * outward, then the document — or a document-wide `#` selector. The failure warning names which
12358
+ * of the three causes ({@link unresolvedCause}) it hit.
11981
12359
  * @param rest - The entity reference.
11982
12360
  * @param raw - The raw value, returned unchanged when the reference does not resolve.
12361
+ * @param from - The element the value is declared under, which scopes the reference.
11983
12362
  * @returns The entity, or `raw`.
11984
12363
  */
11985
- const entityConversion = (rest, raw) => {
11986
- const entity = getEntity(rest);
12364
+ const entityConversion = (rest, raw, from) => {
12365
+ const entity = getEntity(rest, from);
11987
12366
  if (entity) {
11988
12367
  return entity;
11989
12368
  }
11990
- console.warn(`Unable to resolve '${raw}' in script attributes - no pc-entity found matching '${rest}'.`);
12369
+ const element = findEntityElement(rest, from);
12370
+ const hint = element ? '' : idHint(rest, 'entity:');
12371
+ console.warn(`Unable to resolve '${raw}' in script attributes - ${unresolvedCause(element)}.${hint ? ` ${hint}` : ''}`);
11991
12372
  return raw;
11992
12373
  };
11993
12374
  /**
@@ -12120,8 +12501,10 @@ class ScriptComponentElement extends ComponentElement {
12120
12501
  /**
12121
12502
  * Recursively converts raw attribute data into proper PlayCanvas types. Supported conversions:
12122
12503
  * - "asset:id" → the Asset created by the `pc-asset` element with that id
12123
- * - "entity:ref" → the Entity backing a `pc-entity` element. The reference can be a CSS
12124
- * selector, an element id or an entity name.
12504
+ * - "entity:ref" → the Entity backing a `pc-entity`, `pc-model` or `pc-node` element. The
12505
+ * reference is a name, resolved against this element's nearest enclosing entity first,
12506
+ * then outward, then the document — or a document-wide `#` selector (`entity:#id`). A bare
12507
+ * value is always a name, never an id.
12125
12508
  * - "vec2:1 2" → new Vec2(1, 2)
12126
12509
  * - "vec3:1 2 3" → new Vec3(1, 2, 3)
12127
12510
  * - "vec4:1 2 3 4" → new Vec4(1, 2, 3, 4)
@@ -12135,7 +12518,7 @@ class ScriptComponentElement extends ComponentElement {
12135
12518
  convertAttributes(item) {
12136
12519
  if (typeof item === 'string') {
12137
12520
  const match = matchConversion(item);
12138
- return match ? match.convert(match.rest, item) : item;
12521
+ return match ? match.convert(match.rest, item, this) : item;
12139
12522
  }
12140
12523
  if (Array.isArray(item)) {
12141
12524
  return item.map((element) => this.convertAttributes(element));
@@ -13342,11 +13725,17 @@ const levenshtein = (a, b) => {
13342
13725
  * node.
13343
13726
  * @attribute {string} onpointerup - Script to run when a pointer button is released over the
13344
13727
  * node.
13728
+ * @attribute {string} onclick - Script to run when the node is clicked: a primary pointer
13729
+ * button pressed and then released over it.
13345
13730
  * @fires {PointerEvent} pointerenter - Fired when the pointer moves onto the node.
13346
13731
  * @fires {PointerEvent} pointerleave - Fired when the pointer moves off the node.
13347
13732
  * @fires {PointerEvent} pointermove - Fired when the pointer moves over the node.
13348
13733
  * @fires {PointerEvent} pointerdown - Fired when a pointer button is pressed over the node.
13349
13734
  * @fires {PointerEvent} pointerup - Fired when a pointer button is released over the node.
13735
+ * @fires {PointerEvent} click - Fired when a primary pointer button is pressed and then released
13736
+ * over the node. A press and release that picked different entities fires on their nearest
13737
+ * common ancestor instead, as in the DOM. `detail` carries the click count, so a double click
13738
+ * arrives as a click whose `detail` is 2.
13350
13739
  */
13351
13740
  class NodeElement extends EntityBaseElement {
13352
13741
  _name = '';
@@ -13996,7 +14385,7 @@ class NodeElement extends EntityBaseElement {
13996
14385
  'rotation',
13997
14386
  'scale',
13998
14387
  'tags',
13999
- ...POINTER_ATTRIBUTES
14388
+ ...EVENT_ATTRIBUTES
14000
14389
  ];
14001
14390
  }
14002
14391
  attributeChangedCallback(name, _oldValue, newValue) {
@@ -14046,6 +14435,7 @@ class NodeElement extends EntityBaseElement {
14046
14435
  case 'onpointerdown':
14047
14436
  case 'onpointerup':
14048
14437
  case 'onpointermove':
14438
+ case 'onclick':
14049
14439
  this._updateInlineHandler(name, newValue);
14050
14440
  break;
14051
14441
  }
@@ -14060,9 +14450,14 @@ customElements.define('pc-node', NodeElement);
14060
14450
  * {@link HTMLElement} interface.
14061
14451
  *
14062
14452
  * @elementSummary The `<pc-scene>` element holds the entity hierarchy the application renders,
14063
- * along with the scene-wide fog and gravity settings. Must be a direct child of `<pc-app>`.
14453
+ * along with the scene-wide fog, exposure and gravity settings. Must be a direct child of
14454
+ * `<pc-app>`.
14064
14455
  */
14065
14456
  class SceneElement extends AsyncElement {
14457
+ /**
14458
+ * The exposure of the scene.
14459
+ */
14460
+ _exposure = 1;
14066
14461
  /**
14067
14462
  * The fog type of the scene.
14068
14463
  */
@@ -14130,6 +14525,7 @@ class SceneElement extends AsyncElement {
14130
14525
  }
14131
14526
  _updateSceneSettings() {
14132
14527
  if (this._scene) {
14528
+ this._scene.exposure = this._exposure;
14133
14529
  this._scene.fog.type = this._fog;
14134
14530
  this._scene.fog.color = this._fogColor;
14135
14531
  this._scene.fog.density = this._fogDensity;
@@ -14148,6 +14544,24 @@ class SceneElement extends AsyncElement {
14148
14544
  _applyGravity(value) {
14149
14545
  this.closestApp?.app?.systems.rigidbody?.gravity.copy(value);
14150
14546
  }
14547
+ /**
14548
+ * Sets the exposure of the scene, which tweaks the overall brightness of the rendered image.
14549
+ * Ignored if the scene is using physical units. Defaults to 1.
14550
+ * @param value - The exposure.
14551
+ */
14552
+ set exposure(value) {
14553
+ this._exposure = value;
14554
+ if (this.scene) {
14555
+ this.scene.exposure = value;
14556
+ }
14557
+ }
14558
+ /**
14559
+ * Gets the exposure of the scene.
14560
+ * @returns The exposure.
14561
+ */
14562
+ get exposure() {
14563
+ return this._exposure;
14564
+ }
14151
14565
  /**
14152
14566
  * Sets the fog type of the scene. Can be `none`, `linear`, `exp` or `exp2`. Defaults to
14153
14567
  * `none`.
@@ -14252,10 +14666,13 @@ class SceneElement extends AsyncElement {
14252
14666
  return this._gravity;
14253
14667
  }
14254
14668
  static get observedAttributes() {
14255
- return ['fog', 'fog-color', 'fog-density', 'fog-start', 'fog-end', 'gravity'];
14669
+ return ['exposure', 'fog', 'fog-color', 'fog-density', 'fog-start', 'fog-end', 'gravity'];
14256
14670
  }
14257
14671
  attributeChangedCallback(name, _oldValue, newValue) {
14258
14672
  switch (name) {
14673
+ case 'exposure':
14674
+ this.exposure = parseNumber(newValue, 1, name);
14675
+ break;
14259
14676
  case 'fog':
14260
14677
  this.fog = parseEnum(newValue, ['none', 'linear', 'exp', 'exp2'], 'none', name);
14261
14678
  break;