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