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