@playcanvas/web-components 0.18.0 → 0.19.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.
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
  /**
@@ -732,14 +862,51 @@ const getEntity = (ref) => {
732
862
  return element?.entity ?? null;
733
863
  };
734
864
 
735
- /** The pointer event types the application synthesizes on `<pc-entity>` elements via picking. */
736
- const pointerEventTypes = ['pointermove', 'pointerdown', 'pointerup', 'pointerenter', 'pointerleave'];
737
865
  /**
738
866
  * The event types whose listeners make an element a hover target. Hover resolution walks past
739
867
  * elements listening for none of them, so a silent element never swallows an ancestor's
740
868
  * enter/leave pair.
741
869
  */
742
870
  const hoverEventTypes = ['pointerenter', 'pointerleave', 'pointermove'];
871
+ /**
872
+ * The canvas listeners each synthesized event type is driven by. Enter and leave are derived
873
+ * from move picks. A click is concluded from the down/up pair, with pointercancel discarding a
874
+ * press the browser takes back (for example a touch that becomes a scroll).
875
+ */
876
+ const canvasEventsFor = {
877
+ pointermove: ['pointermove'],
878
+ pointerenter: ['pointermove'],
879
+ pointerleave: ['pointermove'],
880
+ pointerdown: ['pointerdown'],
881
+ pointerup: ['pointerup'],
882
+ click: ['pointerdown', 'pointerup', 'pointercancel']
883
+ };
884
+ /**
885
+ * How long after a click a further click on the same target still raises the click count that
886
+ * `detail` carries, approximating the platform's double-click time.
887
+ */
888
+ const CLICK_CHAIN_MS = 500;
889
+ /**
890
+ * Finds the nearest common inclusive ancestor of two picked nodes - the node a click belongs to
891
+ * when the press and the release picked different geometry, exactly as the DOM assigns a click
892
+ * whose down and up have different targets.
893
+ *
894
+ * @param a - The node the press picked, or `null`.
895
+ * @param b - The node the release picked, or `null`.
896
+ * @returns The nearest common inclusive ancestor, or `null` when there is none.
897
+ */
898
+ const commonAncestor = (a, b) => {
899
+ const ancestors = new Set();
900
+ for (let node = a; node !== null; node = node.parent) {
901
+ ancestors.add(node);
902
+ }
903
+ for (let node = b; node !== null; node = node.parent) {
904
+ if (ancestors.has(node)) {
905
+ return node;
906
+ }
907
+ }
908
+ return null;
909
+ };
743
910
  /**
744
911
  * Gives `pc-app` the sizing contract of a replaced element (`<video>`, `<img>`): a block-level
745
912
  * box that the page's CSS sizes, defaulting to the canvas's own 300x150 intrinsic size, with the
@@ -824,21 +991,30 @@ class AppElement extends AsyncElement {
824
991
  */
825
992
  _entityElements = new Map();
826
993
  _picker = null;
827
- _hasPointerListeners = {
828
- pointerenter: false,
829
- pointerleave: false,
830
- pointerdown: false,
831
- pointerup: false,
832
- pointermove: false
833
- };
834
994
  _hoveredEntity = null;
835
995
  // Identifies the newest in-flight hover pick, so out-of-order results can be discarded
836
996
  _pickToken = 0;
837
997
  _pointerHandlers = {
838
998
  pointermove: null,
839
999
  pointerdown: null,
840
- pointerup: null
1000
+ pointerup: null,
1001
+ pointercancel: null
841
1002
  };
1003
+ /**
1004
+ * The pick of each pointer's primary-button press, keyed by pointerId and kept while a click
1005
+ * may still conclude it. The promise is stored rather than its result, so a release can
1006
+ * await a press pick that has not resolved yet. Entries are removed by the matching
1007
+ * pointerup or pointercancel, and only ever stored while some element listens for click -
1008
+ * which is also what keeps those two canvas listeners attached.
1009
+ */
1010
+ _downPicks = new Map();
1011
+ /** Whether any element in the tree listens for click. Maintained by _syncCanvasListeners. */
1012
+ _clickListened = false;
1013
+ /**
1014
+ * The previous click's target, time and count, for chaining successive clicks into the
1015
+ * click count that `detail` carries. `null` until a click has fired.
1016
+ */
1017
+ _lastClick = null;
842
1018
  _app = null;
843
1019
  _loadProgress = 0;
844
1020
  /**
@@ -873,12 +1049,12 @@ class AppElement extends AsyncElement {
873
1049
  */
874
1050
  constructor() {
875
1051
  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));
1052
+ // Track listeners for the synthesized events being added to and removed from descendant
1053
+ // entities. Registered once here rather than on every boot - the sync no-ops while there
1054
+ // is no canvas, and a re-booted element must not stack a second set.
1055
+ SYNTHESIZED_EVENTS.forEach((type) => {
1056
+ this.addEventListener(`${type}:connect`, () => this._syncCanvasListeners());
1057
+ this.addEventListener(`${type}:disconnect`, () => this._syncCanvasListeners());
882
1058
  });
883
1059
  }
884
1060
  async connectedCallback() {
@@ -1196,15 +1372,13 @@ class AppElement extends AsyncElement {
1196
1372
  this._pointerHandlers.pointermove = listener(this._onPointerMove);
1197
1373
  this._pointerHandlers.pointerdown = listener(this._onPointerDown);
1198
1374
  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
1375
+ this._pointerHandlers.pointercancel = (event) => {
1376
+ this._downPicks.delete(event.pointerId);
1377
+ };
1378
+ // Attach canvas listeners for element listeners registered before this boot (e.g.
1379
+ // handlers created from inline attributes when their elements were first upgraded, or
1201
1380
  // 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
- });
1381
+ this._syncCanvasListeners();
1208
1382
  }
1209
1383
  _pickerDestroy() {
1210
1384
  if (this._canvas) {
@@ -1219,15 +1393,12 @@ class AppElement extends AsyncElement {
1219
1393
  this._pointerHandlers = {
1220
1394
  pointermove: null,
1221
1395
  pointerdown: null,
1222
- pointerup: null
1223
- };
1224
- this._hasPointerListeners = {
1225
- pointerenter: false,
1226
- pointerleave: false,
1227
- pointerdown: false,
1228
- pointerup: false,
1229
- pointermove: false
1396
+ pointerup: null,
1397
+ pointercancel: null
1230
1398
  };
1399
+ this._downPicks.clear();
1400
+ this._clickListened = false;
1401
+ this._lastClick = null;
1231
1402
  }
1232
1403
  /**
1233
1404
  * Registers the element that fronts an entity. Called by EntityElement when it creates its
@@ -1433,7 +1604,15 @@ class AppElement extends AsyncElement {
1433
1604
  async _onPointerDown(event) {
1434
1605
  if (!this._picker || !this.app)
1435
1606
  return;
1436
- const node = await this._pickNode(event);
1607
+ const pick = this._pickNode(event);
1608
+ // A click concludes on the matching pointerup, which needs to know what the press
1609
+ // picked. Primary button only - the only button a click can conclude from - and only
1610
+ // while click is listened for, since it is the click mapping that keeps the pointerup
1611
+ // and pointercancel listeners attached to clean the entry up again.
1612
+ if (this._clickListened && event.button === 0) {
1613
+ this._downPicks.set(event.pointerId, pick);
1614
+ }
1615
+ const node = await pick;
1437
1616
  if (!this._picker)
1438
1617
  return; // the element disconnected while the pick was in flight
1439
1618
  const entityElement = this._elementWithListener(node, 'pointerdown');
@@ -1444,6 +1623,10 @@ class AppElement extends AsyncElement {
1444
1623
  async _onPointerUp(event) {
1445
1624
  if (!this._picker || !this.app)
1446
1625
  return;
1626
+ // The press pick this release may conclude as a click. Claimed synchronously, so the
1627
+ // entry is gone before any other event for this pointer can be handled.
1628
+ const downPick = this._downPicks.get(event.pointerId);
1629
+ this._downPicks.delete(event.pointerId);
1447
1630
  const node = await this._pickNode(event);
1448
1631
  if (!this._picker)
1449
1632
  return; // the element disconnected while the pick was in flight
@@ -1451,30 +1634,60 @@ class AppElement extends AsyncElement {
1451
1634
  if (entityElement) {
1452
1635
  entityElement.dispatchEvent(new PointerEvent('pointerup', event));
1453
1636
  }
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);
1637
+ // A click fires where the DOM fires it: at the nearest common inclusive ancestor of
1638
+ // what the press and the release picked, for the primary button only. The press pick
1639
+ // may still be in flight - a quick tap resolves in pick order, not event order.
1640
+ if (!downPick || event.button !== 0)
1641
+ return;
1642
+ const downNode = await downPick;
1643
+ if (!this._picker)
1644
+ return;
1645
+ const clickElement = this._elementWithListener(commonAncestor(downNode, node), 'click');
1646
+ if (clickElement) {
1647
+ const click = new PointerEvent('click', event);
1648
+ // The init above copied pointerup's `detail`, which the Pointer Events spec fixes
1649
+ // at 0 - but click is exempt: its detail is the click count, chained here as the
1650
+ // platform chains it (same target, within the double-click window). Overridden
1651
+ // with defineProperty because an event instance used as an init dict cannot have
1652
+ // single fields replaced.
1653
+ const time = performance.now();
1654
+ const last = this._lastClick;
1655
+ const count = last && last.element === clickElement && time - last.time <= CLICK_CHAIN_MS ? last.count + 1 : 1;
1656
+ this._lastClick = { element: clickElement, time, count };
1657
+ Object.defineProperty(click, 'detail', { value: count });
1658
+ clickElement.dispatchEvent(click);
1659
+ }
1660
+ }
1661
+ /**
1662
+ * Attaches exactly the canvas listeners the tree's current element listeners need, and
1663
+ * detaches the rest. Recomputed whenever a listener connects or disconnects anywhere under
1664
+ * this element: several synthesized types can need the same canvas listener (enter, leave
1665
+ * and move all ride the move pick; click rides the down/up pair), so one type's removal
1666
+ * must not detach a listener another type still uses. Re-attaching an attached listener is
1667
+ * a no-op by EventTarget semantics, so no attach state is kept.
1668
+ */
1669
+ _syncCanvasListeners() {
1670
+ const canvas = this._canvas;
1671
+ if (!canvas)
1672
+ return; // not booted yet: _pickerCreate syncs once the handlers exist
1673
+ const elements = Array.from(this.querySelectorAll('pc-entity, pc-model, pc-node'));
1674
+ const needed = new Set();
1675
+ for (const type of SYNTHESIZED_EVENTS) {
1676
+ if (elements.some((element) => element._hasListeners(type))) {
1677
+ canvasEventsFor[type].forEach((canvasType) => needed.add(canvasType));
1464
1678
  }
1465
1679
  }
1466
- }
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);
1680
+ this._clickListened = elements.some((element) => element._hasListeners('click'));
1681
+ Object.entries(this._pointerHandlers).forEach(([canvasType, handler]) => {
1682
+ if (!handler)
1683
+ return;
1684
+ if (needed.has(canvasType)) {
1685
+ canvas.addEventListener(canvasType, handler);
1476
1686
  }
1477
- }
1687
+ else {
1688
+ canvas.removeEventListener(canvasType, handler);
1689
+ }
1690
+ });
1478
1691
  }
1479
1692
  /**
1480
1693
  * Warns that a graphics option was written too late to have any effect. These options are read
@@ -1643,126 +1856,6 @@ class AppElement extends AsyncElement {
1643
1856
  }
1644
1857
  customElements.define('pc-app', AppElement);
1645
1858
 
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
1859
  /**
1767
1860
  * Creates and parents the entities of every descendant entity-owning element of `root`, in two
1768
1861
  * passes so that no parent's existence depends on document order. Called wherever a subtree could
@@ -2030,8 +2123,8 @@ class EntityOwnerElement extends EntityBaseElement {
2030
2123
  *
2031
2124
  * The pointer events below are dispatched by the containing `<pc-app>` element when the pointer
2032
2125
  * 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.
2126
+ * them, registered either with {@link EventTarget.addEventListener} or with the matching inline
2127
+ * attribute (`onpointerdown`, `onclick`, ...).
2035
2128
  *
2036
2129
  * @elementSummary The `<pc-entity>` element creates an entity: a named, transformable node of the
2037
2130
  * scene hierarchy, and the host for component elements such as `<pc-camera>`, `<pc-light>` and
@@ -2051,11 +2144,17 @@ class EntityOwnerElement extends EntityBaseElement {
2051
2144
  * entity.
2052
2145
  * @attribute {string} onpointerup - Script to run when a pointer button is released over the
2053
2146
  * entity.
2147
+ * @attribute {string} onclick - Script to run when the entity is clicked: a primary pointer
2148
+ * button pressed and then released over it.
2054
2149
  * @fires {PointerEvent} pointerenter - Fired when the pointer moves onto the entity.
2055
2150
  * @fires {PointerEvent} pointerleave - Fired when the pointer moves off the entity.
2056
2151
  * @fires {PointerEvent} pointermove - Fired when the pointer moves over the entity.
2057
2152
  * @fires {PointerEvent} pointerdown - Fired when a pointer button is pressed over the entity.
2058
2153
  * @fires {PointerEvent} pointerup - Fired when a pointer button is released over the entity.
2154
+ * @fires {PointerEvent} click - Fired when a primary pointer button is pressed and then released
2155
+ * over the entity. A press and release that picked different entities fires on their nearest
2156
+ * common ancestor instead, as in the DOM. `detail` carries the click count, so a double click
2157
+ * arrives as a click whose `detail` is 2.
2059
2158
  */
2060
2159
  class EntityElement extends EntityOwnerElement {
2061
2160
  connectedCallback() {
@@ -2090,7 +2189,7 @@ class EntityElement extends EntityOwnerElement {
2090
2189
  this._entity?.destroy();
2091
2190
  }
2092
2191
  static get observedAttributes() {
2093
- return ['enabled', 'name', 'position', 'rotation', 'scale', 'tags', ...POINTER_ATTRIBUTES];
2192
+ return ['enabled', 'name', 'position', 'rotation', 'scale', 'tags', ...EVENT_ATTRIBUTES];
2094
2193
  }
2095
2194
  attributeChangedCallback(name, _oldValue, newValue) {
2096
2195
  switch (name) {
@@ -2117,6 +2216,7 @@ class EntityElement extends EntityOwnerElement {
2117
2216
  case 'onpointerdown':
2118
2217
  case 'onpointerup':
2119
2218
  case 'onpointermove':
2219
+ case 'onclick':
2120
2220
  this._updateInlineHandler(name, newValue);
2121
2221
  break;
2122
2222
  }
@@ -3066,11 +3166,17 @@ const formatHierarchy = (root, counts) => {
3066
3166
  * model.
3067
3167
  * @attribute {string} onpointerup - Script to run when a pointer button is released over the
3068
3168
  * model.
3169
+ * @attribute {string} onclick - Script to run when the model is clicked: a primary pointer
3170
+ * button pressed and then released over it.
3069
3171
  * @fires {PointerEvent} pointerenter - Fired when the pointer moves onto the model.
3070
3172
  * @fires {PointerEvent} pointerleave - Fired when the pointer moves off the model.
3071
3173
  * @fires {PointerEvent} pointermove - Fired when the pointer moves over the model.
3072
3174
  * @fires {PointerEvent} pointerdown - Fired when a pointer button is pressed over the model.
3073
3175
  * @fires {PointerEvent} pointerup - Fired when a pointer button is released over the model.
3176
+ * @fires {PointerEvent} click - Fired when a primary pointer button is pressed and then released
3177
+ * over the model. A press and release that picked different entities fires on their nearest
3178
+ * common ancestor instead, as in the DOM. `detail` carries the click count, so a double click
3179
+ * arrives as a click whose `detail` is 2.
3074
3180
  * @fires {Event} load - Fired each time a container asset finishes instantiating, including
3075
3181
  * re-instantiation after `asset` changes. Does not bubble — listen on this element, or use a
3076
3182
  * capture-phase listener on an ancestor.
@@ -3319,7 +3425,7 @@ class ModelElement extends EntityOwnerElement {
3319
3425
  return this._asset;
3320
3426
  }
3321
3427
  static get observedAttributes() {
3322
- return ['asset', 'enabled', 'name', 'position', 'rotation', 'scale', 'tags', ...POINTER_ATTRIBUTES];
3428
+ return ['asset', 'enabled', 'name', 'position', 'rotation', 'scale', 'tags', ...EVENT_ATTRIBUTES];
3323
3429
  }
3324
3430
  attributeChangedCallback(name, _oldValue, newValue) {
3325
3431
  switch (name) {
@@ -3349,6 +3455,7 @@ class ModelElement extends EntityOwnerElement {
3349
3455
  case 'onpointerdown':
3350
3456
  case 'onpointerup':
3351
3457
  case 'onpointermove':
3458
+ case 'onclick':
3352
3459
  this._updateInlineHandler(name, newValue);
3353
3460
  break;
3354
3461
  }
@@ -4212,8 +4319,6 @@ customElements.define('pc-anim', AnimComponentElement);
4212
4319
  * @elementSummary The `<pc-anim-clip>` element declares one named animation clip on its parent
4213
4320
  * `<pc-anim>`, taken from the `asset` it names or, without one, from the enclosing `<pc-model>`'s
4214
4321
  * own animations. Must be a direct child of `<pc-anim>`.
4215
- *
4216
- * @category Components
4217
4322
  */
4218
4323
  class AnimClipElement extends AsyncElement {
4219
4324
  /**
@@ -13342,11 +13447,17 @@ const levenshtein = (a, b) => {
13342
13447
  * node.
13343
13448
  * @attribute {string} onpointerup - Script to run when a pointer button is released over the
13344
13449
  * node.
13450
+ * @attribute {string} onclick - Script to run when the node is clicked: a primary pointer
13451
+ * button pressed and then released over it.
13345
13452
  * @fires {PointerEvent} pointerenter - Fired when the pointer moves onto the node.
13346
13453
  * @fires {PointerEvent} pointerleave - Fired when the pointer moves off the node.
13347
13454
  * @fires {PointerEvent} pointermove - Fired when the pointer moves over the node.
13348
13455
  * @fires {PointerEvent} pointerdown - Fired when a pointer button is pressed over the node.
13349
13456
  * @fires {PointerEvent} pointerup - Fired when a pointer button is released over the node.
13457
+ * @fires {PointerEvent} click - Fired when a primary pointer button is pressed and then released
13458
+ * over the node. A press and release that picked different entities fires on their nearest
13459
+ * common ancestor instead, as in the DOM. `detail` carries the click count, so a double click
13460
+ * arrives as a click whose `detail` is 2.
13350
13461
  */
13351
13462
  class NodeElement extends EntityBaseElement {
13352
13463
  _name = '';
@@ -13996,7 +14107,7 @@ class NodeElement extends EntityBaseElement {
13996
14107
  'rotation',
13997
14108
  'scale',
13998
14109
  'tags',
13999
- ...POINTER_ATTRIBUTES
14110
+ ...EVENT_ATTRIBUTES
14000
14111
  ];
14001
14112
  }
14002
14113
  attributeChangedCallback(name, _oldValue, newValue) {
@@ -14046,6 +14157,7 @@ class NodeElement extends EntityBaseElement {
14046
14157
  case 'onpointerdown':
14047
14158
  case 'onpointerup':
14048
14159
  case 'onpointermove':
14160
+ case 'onclick':
14049
14161
  this._updateInlineHandler(name, newValue);
14050
14162
  break;
14051
14163
  }
@@ -14060,9 +14172,14 @@ customElements.define('pc-node', NodeElement);
14060
14172
  * {@link HTMLElement} interface.
14061
14173
  *
14062
14174
  * @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>`.
14175
+ * along with the scene-wide fog, exposure and gravity settings. Must be a direct child of
14176
+ * `<pc-app>`.
14064
14177
  */
14065
14178
  class SceneElement extends AsyncElement {
14179
+ /**
14180
+ * The exposure of the scene.
14181
+ */
14182
+ _exposure = 1;
14066
14183
  /**
14067
14184
  * The fog type of the scene.
14068
14185
  */
@@ -14130,6 +14247,7 @@ class SceneElement extends AsyncElement {
14130
14247
  }
14131
14248
  _updateSceneSettings() {
14132
14249
  if (this._scene) {
14250
+ this._scene.exposure = this._exposure;
14133
14251
  this._scene.fog.type = this._fog;
14134
14252
  this._scene.fog.color = this._fogColor;
14135
14253
  this._scene.fog.density = this._fogDensity;
@@ -14148,6 +14266,24 @@ class SceneElement extends AsyncElement {
14148
14266
  _applyGravity(value) {
14149
14267
  this.closestApp?.app?.systems.rigidbody?.gravity.copy(value);
14150
14268
  }
14269
+ /**
14270
+ * Sets the exposure of the scene, which tweaks the overall brightness of the rendered image.
14271
+ * Ignored if the scene is using physical units. Defaults to 1.
14272
+ * @param value - The exposure.
14273
+ */
14274
+ set exposure(value) {
14275
+ this._exposure = value;
14276
+ if (this.scene) {
14277
+ this.scene.exposure = value;
14278
+ }
14279
+ }
14280
+ /**
14281
+ * Gets the exposure of the scene.
14282
+ * @returns The exposure.
14283
+ */
14284
+ get exposure() {
14285
+ return this._exposure;
14286
+ }
14151
14287
  /**
14152
14288
  * Sets the fog type of the scene. Can be `none`, `linear`, `exp` or `exp2`. Defaults to
14153
14289
  * `none`.
@@ -14252,10 +14388,13 @@ class SceneElement extends AsyncElement {
14252
14388
  return this._gravity;
14253
14389
  }
14254
14390
  static get observedAttributes() {
14255
- return ['fog', 'fog-color', 'fog-density', 'fog-start', 'fog-end', 'gravity'];
14391
+ return ['exposure', 'fog', 'fog-color', 'fog-density', 'fog-start', 'fog-end', 'gravity'];
14256
14392
  }
14257
14393
  attributeChangedCallback(name, _oldValue, newValue) {
14258
14394
  switch (name) {
14395
+ case 'exposure':
14396
+ this.exposure = parseNumber(newValue, 1, name);
14397
+ break;
14259
14398
  case 'fog':
14260
14399
  this.fog = parseEnum(newValue, ['none', 'linear', 'exp', 'exp2'], 'none', name);
14261
14400
  break;