@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/app.d.cts +24 -3
- package/dist/app.d.ts +24 -3
- package/dist/components/anim-clip.d.cts +0 -2
- package/dist/components/anim-clip.d.ts +0 -2
- package/dist/custom-elements.json +72 -2
- package/dist/entity-base.d.cts +4 -3
- package/dist/entity-base.d.ts +4 -3
- package/dist/entity.d.cts +8 -2
- package/dist/entity.d.ts +8 -2
- package/dist/model.d.cts +6 -0
- package/dist/model.d.ts +6 -0
- package/dist/node.d.cts +6 -0
- package/dist/node.d.ts +6 -0
- package/dist/pwc.cjs +322 -183
- package/dist/pwc.cjs.map +1 -1
- package/dist/pwc.js +322 -183
- package/dist/pwc.js.map +1 -1
- package/dist/pwc.min.js +1 -1
- package/dist/pwc.min.js.map +1 -1
- package/dist/pwc.min.mjs +1 -1
- package/dist/pwc.min.mjs.map +1 -1
- package/dist/pwc.mjs +322 -183
- package/dist/pwc.mjs.map +1 -1
- package/dist/scene.d.cts +17 -1
- package/dist/scene.d.ts +17 -1
- package/dist/vscode.html-custom-data.json +24 -4
- package/dist/web-types.json +59 -5
- package/package.json +3 -3
- package/src/app.ts +146 -71
- package/src/components/anim-clip.ts +0 -2
- package/src/entity-base.ts +27 -15
- package/src/entity.ts +11 -4
- package/src/model.ts +9 -2
- package/src/node.ts +9 -2
- package/src/scene.ts +33 -2
package/dist/pwc.mjs
CHANGED
|
@@ -177,6 +177,136 @@ class WasmElement extends AsyncElement {
|
|
|
177
177
|
}
|
|
178
178
|
customElements.define('pc-wasm', WasmElement);
|
|
179
179
|
|
|
180
|
+
/**
|
|
181
|
+
* The event types the containing `<pc-app>` synthesizes on entity-fronting elements via picking:
|
|
182
|
+
* the `pointer*` events, plus `click` — which concludes a primary-button press and release, and
|
|
183
|
+
* is delivered as a `PointerEvent` exactly as modern browsers deliver native clicks.
|
|
184
|
+
* @internal
|
|
185
|
+
*/
|
|
186
|
+
const SYNTHESIZED_EVENTS = [
|
|
187
|
+
'pointerenter',
|
|
188
|
+
'pointerleave',
|
|
189
|
+
'pointerdown',
|
|
190
|
+
'pointerup',
|
|
191
|
+
'pointermove',
|
|
192
|
+
'click'
|
|
193
|
+
];
|
|
194
|
+
const SYNTHESIZED_EVENT_SET = new Set(SYNTHESIZED_EVENTS);
|
|
195
|
+
/**
|
|
196
|
+
* The attribute names of the inline event handlers (`onpointerdown`, `onclick`, ...), shared by
|
|
197
|
+
* every element that fronts an engine entity. Spread into `observedAttributes` by subclasses.
|
|
198
|
+
* @internal
|
|
199
|
+
*/
|
|
200
|
+
const EVENT_ATTRIBUTES = SYNTHESIZED_EVENTS.map((type) => `on${type}`);
|
|
201
|
+
/**
|
|
202
|
+
* The base class for elements that front an engine {@link Entity}: `<pc-entity>` and
|
|
203
|
+
* `<pc-model>`, which create one, and `<pc-node>`, which binds to one inside a model's
|
|
204
|
+
* instantiated hierarchy. It carries what all of them need — the `entity` contract, registration
|
|
205
|
+
* with the owning application (which joins picked scene nodes back to elements by identity,
|
|
206
|
+
* never by name), and the pointer listener bookkeeping that lets the application lazily attach
|
|
207
|
+
* its canvas handlers.
|
|
208
|
+
*/
|
|
209
|
+
class EntityBaseElement extends AsyncElement {
|
|
210
|
+
_entity = null;
|
|
211
|
+
/**
|
|
212
|
+
* The application element this entity is registered with, cached at registration time so the
|
|
213
|
+
* entity can be unregistered even once this element has left the DOM.
|
|
214
|
+
*/
|
|
215
|
+
_appElement = null;
|
|
216
|
+
/**
|
|
217
|
+
* The event listeners registered on the element, by type.
|
|
218
|
+
*/
|
|
219
|
+
_listeners = {};
|
|
220
|
+
/**
|
|
221
|
+
* The event types for which an inline handler attribute (`onpointerdown`, `onclick`, ...)
|
|
222
|
+
* is currently present.
|
|
223
|
+
*/
|
|
224
|
+
_inlineHandlerTypes = new Set();
|
|
225
|
+
/**
|
|
226
|
+
* The PlayCanvas entity instance. `null` until the element is ready, and again once the
|
|
227
|
+
* entity is gone — await {@link whenReady} or the element's `ready()` promise before
|
|
228
|
+
* accessing it.
|
|
229
|
+
* @returns The entity instance, or `null`.
|
|
230
|
+
*/
|
|
231
|
+
get entity() {
|
|
232
|
+
return this._entity;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Registers `entity` as this element's backing entity with the owning application, which
|
|
236
|
+
* joins engine nodes back to elements by identity (never by name).
|
|
237
|
+
*
|
|
238
|
+
* @param entity - The entity to register.
|
|
239
|
+
*/
|
|
240
|
+
_registerEntity(entity) {
|
|
241
|
+
this._appElement = this.closestApp;
|
|
242
|
+
this._appElement?._registerEntityElement(entity, this);
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Removes the registration for `entity`.
|
|
246
|
+
*
|
|
247
|
+
* @param entity - The entity to unregister.
|
|
248
|
+
*/
|
|
249
|
+
_unregisterEntity(entity) {
|
|
250
|
+
this._appElement?._unregisterEntityElement(entity);
|
|
251
|
+
this._appElement = null;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Tracks whether an inline handler attribute is present. The browser itself compiles and
|
|
255
|
+
* runs these attributes — they are standard `GlobalEventHandlers`, so setting one replaces
|
|
256
|
+
* the previous handler and removing it removes the handler, exactly like `onclick` on any
|
|
257
|
+
* HTML element. But because they bypass {@link EventTarget.addEventListener}, the connect/disconnect
|
|
258
|
+
* bookkeeping that lets the application lazily attach its canvas pointer handlers must be
|
|
259
|
+
* kept in sync here.
|
|
260
|
+
*
|
|
261
|
+
* @param name - The attribute name (e.g. 'onpointerdown').
|
|
262
|
+
* @param value - The attribute value, or `null` when the attribute has been removed.
|
|
263
|
+
*/
|
|
264
|
+
_updateInlineHandler(name, value) {
|
|
265
|
+
const type = name.substring(2);
|
|
266
|
+
const had = this._inlineHandlerTypes.has(type);
|
|
267
|
+
const has = value !== null;
|
|
268
|
+
if (has && !had) {
|
|
269
|
+
this._inlineHandlerTypes.add(type);
|
|
270
|
+
this.dispatchEvent(new CustomEvent(`${type}:connect`, { bubbles: true }));
|
|
271
|
+
}
|
|
272
|
+
else if (!has && had) {
|
|
273
|
+
this._inlineHandlerTypes.delete(type);
|
|
274
|
+
this.dispatchEvent(new CustomEvent(`${type}:disconnect`, { bubbles: true }));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
addEventListener(type, listener, options) {
|
|
278
|
+
if (!this._listeners[type]) {
|
|
279
|
+
this._listeners[type] = [];
|
|
280
|
+
}
|
|
281
|
+
this._listeners[type].push(listener);
|
|
282
|
+
super.addEventListener(type, listener, options);
|
|
283
|
+
if (SYNTHESIZED_EVENT_SET.has(type)) {
|
|
284
|
+
this.dispatchEvent(new CustomEvent(`${type}:connect`, { bubbles: true }));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
removeEventListener(type, listener, options) {
|
|
288
|
+
if (this._listeners[type]) {
|
|
289
|
+
this._listeners[type] = this._listeners[type].filter((l) => l !== listener);
|
|
290
|
+
}
|
|
291
|
+
super.removeEventListener(type, listener, options);
|
|
292
|
+
if (SYNTHESIZED_EVENT_SET.has(type)) {
|
|
293
|
+
this.dispatchEvent(new CustomEvent(`${type}:disconnect`, { bubbles: true }));
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Whether the element has a listener for an event type, registered either with
|
|
298
|
+
* {@link EventTarget.addEventListener} or with the matching inline handler attribute. Read by the
|
|
299
|
+
* containing `<pc-app>` element to gate event synthesis.
|
|
300
|
+
*
|
|
301
|
+
* @param type - The event type.
|
|
302
|
+
* @returns Whether a listener is registered.
|
|
303
|
+
* @internal
|
|
304
|
+
*/
|
|
305
|
+
_hasListeners(type) {
|
|
306
|
+
return Boolean(this._listeners[type]?.length) || this._inlineHandlerTypes.has(type);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
180
310
|
/** Covers the 0.2s opacity transition; jsdom never fires transitionend, so removal is timed. */
|
|
181
311
|
const REMOVAL_DELAY_MS = 250;
|
|
182
312
|
/**
|
|
@@ -730,14 +860,51 @@ const getEntity = (ref) => {
|
|
|
730
860
|
return element?.entity ?? null;
|
|
731
861
|
};
|
|
732
862
|
|
|
733
|
-
/** The pointer event types the application synthesizes on `<pc-entity>` elements via picking. */
|
|
734
|
-
const pointerEventTypes = ['pointermove', 'pointerdown', 'pointerup', 'pointerenter', 'pointerleave'];
|
|
735
863
|
/**
|
|
736
864
|
* The event types whose listeners make an element a hover target. Hover resolution walks past
|
|
737
865
|
* elements listening for none of them, so a silent element never swallows an ancestor's
|
|
738
866
|
* enter/leave pair.
|
|
739
867
|
*/
|
|
740
868
|
const hoverEventTypes = ['pointerenter', 'pointerleave', 'pointermove'];
|
|
869
|
+
/**
|
|
870
|
+
* The canvas listeners each synthesized event type is driven by. Enter and leave are derived
|
|
871
|
+
* from move picks. A click is concluded from the down/up pair, with pointercancel discarding a
|
|
872
|
+
* press the browser takes back (for example a touch that becomes a scroll).
|
|
873
|
+
*/
|
|
874
|
+
const canvasEventsFor = {
|
|
875
|
+
pointermove: ['pointermove'],
|
|
876
|
+
pointerenter: ['pointermove'],
|
|
877
|
+
pointerleave: ['pointermove'],
|
|
878
|
+
pointerdown: ['pointerdown'],
|
|
879
|
+
pointerup: ['pointerup'],
|
|
880
|
+
click: ['pointerdown', 'pointerup', 'pointercancel']
|
|
881
|
+
};
|
|
882
|
+
/**
|
|
883
|
+
* How long after a click a further click on the same target still raises the click count that
|
|
884
|
+
* `detail` carries, approximating the platform's double-click time.
|
|
885
|
+
*/
|
|
886
|
+
const CLICK_CHAIN_MS = 500;
|
|
887
|
+
/**
|
|
888
|
+
* Finds the nearest common inclusive ancestor of two picked nodes - the node a click belongs to
|
|
889
|
+
* when the press and the release picked different geometry, exactly as the DOM assigns a click
|
|
890
|
+
* whose down and up have different targets.
|
|
891
|
+
*
|
|
892
|
+
* @param a - The node the press picked, or `null`.
|
|
893
|
+
* @param b - The node the release picked, or `null`.
|
|
894
|
+
* @returns The nearest common inclusive ancestor, or `null` when there is none.
|
|
895
|
+
*/
|
|
896
|
+
const commonAncestor = (a, b) => {
|
|
897
|
+
const ancestors = new Set();
|
|
898
|
+
for (let node = a; node !== null; node = node.parent) {
|
|
899
|
+
ancestors.add(node);
|
|
900
|
+
}
|
|
901
|
+
for (let node = b; node !== null; node = node.parent) {
|
|
902
|
+
if (ancestors.has(node)) {
|
|
903
|
+
return node;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
return null;
|
|
907
|
+
};
|
|
741
908
|
/**
|
|
742
909
|
* Gives `pc-app` the sizing contract of a replaced element (`<video>`, `<img>`): a block-level
|
|
743
910
|
* box that the page's CSS sizes, defaulting to the canvas's own 300x150 intrinsic size, with the
|
|
@@ -822,21 +989,30 @@ class AppElement extends AsyncElement {
|
|
|
822
989
|
*/
|
|
823
990
|
_entityElements = new Map();
|
|
824
991
|
_picker = null;
|
|
825
|
-
_hasPointerListeners = {
|
|
826
|
-
pointerenter: false,
|
|
827
|
-
pointerleave: false,
|
|
828
|
-
pointerdown: false,
|
|
829
|
-
pointerup: false,
|
|
830
|
-
pointermove: false
|
|
831
|
-
};
|
|
832
992
|
_hoveredEntity = null;
|
|
833
993
|
// Identifies the newest in-flight hover pick, so out-of-order results can be discarded
|
|
834
994
|
_pickToken = 0;
|
|
835
995
|
_pointerHandlers = {
|
|
836
996
|
pointermove: null,
|
|
837
997
|
pointerdown: null,
|
|
838
|
-
pointerup: null
|
|
998
|
+
pointerup: null,
|
|
999
|
+
pointercancel: null
|
|
839
1000
|
};
|
|
1001
|
+
/**
|
|
1002
|
+
* The pick of each pointer's primary-button press, keyed by pointerId and kept while a click
|
|
1003
|
+
* may still conclude it. The promise is stored rather than its result, so a release can
|
|
1004
|
+
* await a press pick that has not resolved yet. Entries are removed by the matching
|
|
1005
|
+
* pointerup or pointercancel, and only ever stored while some element listens for click -
|
|
1006
|
+
* which is also what keeps those two canvas listeners attached.
|
|
1007
|
+
*/
|
|
1008
|
+
_downPicks = new Map();
|
|
1009
|
+
/** Whether any element in the tree listens for click. Maintained by _syncCanvasListeners. */
|
|
1010
|
+
_clickListened = false;
|
|
1011
|
+
/**
|
|
1012
|
+
* The previous click's target, time and count, for chaining successive clicks into the
|
|
1013
|
+
* click count that `detail` carries. `null` until a click has fired.
|
|
1014
|
+
*/
|
|
1015
|
+
_lastClick = null;
|
|
840
1016
|
_app = null;
|
|
841
1017
|
_loadProgress = 0;
|
|
842
1018
|
/**
|
|
@@ -871,12 +1047,12 @@ class AppElement extends AsyncElement {
|
|
|
871
1047
|
*/
|
|
872
1048
|
constructor() {
|
|
873
1049
|
super();
|
|
874
|
-
// Track
|
|
875
|
-
// Registered once here rather than on every boot - the
|
|
876
|
-
// canvas, and a re-booted element must not stack a second set.
|
|
877
|
-
|
|
878
|
-
this.addEventListener(`${type}:connect`, () => this.
|
|
879
|
-
this.addEventListener(`${type}:disconnect`, () => this.
|
|
1050
|
+
// Track listeners for the synthesized events being added to and removed from descendant
|
|
1051
|
+
// entities. Registered once here rather than on every boot - the sync no-ops while there
|
|
1052
|
+
// is no canvas, and a re-booted element must not stack a second set.
|
|
1053
|
+
SYNTHESIZED_EVENTS.forEach((type) => {
|
|
1054
|
+
this.addEventListener(`${type}:connect`, () => this._syncCanvasListeners());
|
|
1055
|
+
this.addEventListener(`${type}:disconnect`, () => this._syncCanvasListeners());
|
|
880
1056
|
});
|
|
881
1057
|
}
|
|
882
1058
|
async connectedCallback() {
|
|
@@ -1194,15 +1370,13 @@ class AppElement extends AsyncElement {
|
|
|
1194
1370
|
this._pointerHandlers.pointermove = listener(this._onPointerMove);
|
|
1195
1371
|
this._pointerHandlers.pointerdown = listener(this._onPointerDown);
|
|
1196
1372
|
this._pointerHandlers.pointerup = listener(this._onPointerUp);
|
|
1197
|
-
|
|
1198
|
-
|
|
1373
|
+
this._pointerHandlers.pointercancel = (event) => {
|
|
1374
|
+
this._downPicks.delete(event.pointerId);
|
|
1375
|
+
};
|
|
1376
|
+
// Attach canvas listeners for element listeners registered before this boot (e.g.
|
|
1377
|
+
// handlers created from inline attributes when their elements were first upgraded, or
|
|
1199
1378
|
// listeners carried over from before a re-boot)
|
|
1200
|
-
|
|
1201
|
-
const anyListeners = Array.from(this.querySelectorAll('pc-entity, pc-model, pc-node')).some((entity) => entity._hasListeners(type));
|
|
1202
|
-
if (anyListeners) {
|
|
1203
|
-
this._onPointerListenerAdded(type);
|
|
1204
|
-
}
|
|
1205
|
-
});
|
|
1379
|
+
this._syncCanvasListeners();
|
|
1206
1380
|
}
|
|
1207
1381
|
_pickerDestroy() {
|
|
1208
1382
|
if (this._canvas) {
|
|
@@ -1217,15 +1391,12 @@ class AppElement extends AsyncElement {
|
|
|
1217
1391
|
this._pointerHandlers = {
|
|
1218
1392
|
pointermove: null,
|
|
1219
1393
|
pointerdown: null,
|
|
1220
|
-
pointerup: null
|
|
1221
|
-
|
|
1222
|
-
this._hasPointerListeners = {
|
|
1223
|
-
pointerenter: false,
|
|
1224
|
-
pointerleave: false,
|
|
1225
|
-
pointerdown: false,
|
|
1226
|
-
pointerup: false,
|
|
1227
|
-
pointermove: false
|
|
1394
|
+
pointerup: null,
|
|
1395
|
+
pointercancel: null
|
|
1228
1396
|
};
|
|
1397
|
+
this._downPicks.clear();
|
|
1398
|
+
this._clickListened = false;
|
|
1399
|
+
this._lastClick = null;
|
|
1229
1400
|
}
|
|
1230
1401
|
/**
|
|
1231
1402
|
* Registers the element that fronts an entity. Called by EntityElement when it creates its
|
|
@@ -1431,7 +1602,15 @@ class AppElement extends AsyncElement {
|
|
|
1431
1602
|
async _onPointerDown(event) {
|
|
1432
1603
|
if (!this._picker || !this.app)
|
|
1433
1604
|
return;
|
|
1434
|
-
const
|
|
1605
|
+
const pick = this._pickNode(event);
|
|
1606
|
+
// A click concludes on the matching pointerup, which needs to know what the press
|
|
1607
|
+
// picked. Primary button only - the only button a click can conclude from - and only
|
|
1608
|
+
// while click is listened for, since it is the click mapping that keeps the pointerup
|
|
1609
|
+
// and pointercancel listeners attached to clean the entry up again.
|
|
1610
|
+
if (this._clickListened && event.button === 0) {
|
|
1611
|
+
this._downPicks.set(event.pointerId, pick);
|
|
1612
|
+
}
|
|
1613
|
+
const node = await pick;
|
|
1435
1614
|
if (!this._picker)
|
|
1436
1615
|
return; // the element disconnected while the pick was in flight
|
|
1437
1616
|
const entityElement = this._elementWithListener(node, 'pointerdown');
|
|
@@ -1442,6 +1621,10 @@ class AppElement extends AsyncElement {
|
|
|
1442
1621
|
async _onPointerUp(event) {
|
|
1443
1622
|
if (!this._picker || !this.app)
|
|
1444
1623
|
return;
|
|
1624
|
+
// The press pick this release may conclude as a click. Claimed synchronously, so the
|
|
1625
|
+
// entry is gone before any other event for this pointer can be handled.
|
|
1626
|
+
const downPick = this._downPicks.get(event.pointerId);
|
|
1627
|
+
this._downPicks.delete(event.pointerId);
|
|
1445
1628
|
const node = await this._pickNode(event);
|
|
1446
1629
|
if (!this._picker)
|
|
1447
1630
|
return; // the element disconnected while the pick was in flight
|
|
@@ -1449,30 +1632,60 @@ class AppElement extends AsyncElement {
|
|
|
1449
1632
|
if (entityElement) {
|
|
1450
1633
|
entityElement.dispatchEvent(new PointerEvent('pointerup', event));
|
|
1451
1634
|
}
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1635
|
+
// A click fires where the DOM fires it: at the nearest common inclusive ancestor of
|
|
1636
|
+
// what the press and the release picked, for the primary button only. The press pick
|
|
1637
|
+
// may still be in flight - a quick tap resolves in pick order, not event order.
|
|
1638
|
+
if (!downPick || event.button !== 0)
|
|
1639
|
+
return;
|
|
1640
|
+
const downNode = await downPick;
|
|
1641
|
+
if (!this._picker)
|
|
1642
|
+
return;
|
|
1643
|
+
const clickElement = this._elementWithListener(commonAncestor(downNode, node), 'click');
|
|
1644
|
+
if (clickElement) {
|
|
1645
|
+
const click = new PointerEvent('click', event);
|
|
1646
|
+
// The init above copied pointerup's `detail`, which the Pointer Events spec fixes
|
|
1647
|
+
// at 0 - but click is exempt: its detail is the click count, chained here as the
|
|
1648
|
+
// platform chains it (same target, within the double-click window). Overridden
|
|
1649
|
+
// with defineProperty because an event instance used as an init dict cannot have
|
|
1650
|
+
// single fields replaced.
|
|
1651
|
+
const time = performance.now();
|
|
1652
|
+
const last = this._lastClick;
|
|
1653
|
+
const count = last && last.element === clickElement && time - last.time <= CLICK_CHAIN_MS ? last.count + 1 : 1;
|
|
1654
|
+
this._lastClick = { element: clickElement, time, count };
|
|
1655
|
+
Object.defineProperty(click, 'detail', { value: count });
|
|
1656
|
+
clickElement.dispatchEvent(click);
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
/**
|
|
1660
|
+
* Attaches exactly the canvas listeners the tree's current element listeners need, and
|
|
1661
|
+
* detaches the rest. Recomputed whenever a listener connects or disconnects anywhere under
|
|
1662
|
+
* this element: several synthesized types can need the same canvas listener (enter, leave
|
|
1663
|
+
* and move all ride the move pick; click rides the down/up pair), so one type's removal
|
|
1664
|
+
* must not detach a listener another type still uses. Re-attaching an attached listener is
|
|
1665
|
+
* a no-op by EventTarget semantics, so no attach state is kept.
|
|
1666
|
+
*/
|
|
1667
|
+
_syncCanvasListeners() {
|
|
1668
|
+
const canvas = this._canvas;
|
|
1669
|
+
if (!canvas)
|
|
1670
|
+
return; // not booted yet: _pickerCreate syncs once the handlers exist
|
|
1671
|
+
const elements = Array.from(this.querySelectorAll('pc-entity, pc-model, pc-node'));
|
|
1672
|
+
const needed = new Set();
|
|
1673
|
+
for (const type of SYNTHESIZED_EVENTS) {
|
|
1674
|
+
if (elements.some((element) => element._hasListeners(type))) {
|
|
1675
|
+
canvasEventsFor[type].forEach((canvasType) => needed.add(canvasType));
|
|
1462
1676
|
}
|
|
1463
1677
|
}
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
? this._pointerHandlers.pointermove
|
|
1471
|
-
: this._pointerHandlers[type];
|
|
1472
|
-
if (handler) {
|
|
1473
|
-
this._canvas.removeEventListener(type === 'pointerenter' || type === 'pointerleave' ? 'pointermove' : type, handler);
|
|
1678
|
+
this._clickListened = elements.some((element) => element._hasListeners('click'));
|
|
1679
|
+
Object.entries(this._pointerHandlers).forEach(([canvasType, handler]) => {
|
|
1680
|
+
if (!handler)
|
|
1681
|
+
return;
|
|
1682
|
+
if (needed.has(canvasType)) {
|
|
1683
|
+
canvas.addEventListener(canvasType, handler);
|
|
1474
1684
|
}
|
|
1475
|
-
|
|
1685
|
+
else {
|
|
1686
|
+
canvas.removeEventListener(canvasType, handler);
|
|
1687
|
+
}
|
|
1688
|
+
});
|
|
1476
1689
|
}
|
|
1477
1690
|
/**
|
|
1478
1691
|
* Warns that a graphics option was written too late to have any effect. These options are read
|
|
@@ -1641,126 +1854,6 @@ class AppElement extends AsyncElement {
|
|
|
1641
1854
|
}
|
|
1642
1855
|
customElements.define('pc-app', AppElement);
|
|
1643
1856
|
|
|
1644
|
-
/**
|
|
1645
|
-
* The attribute names of the inline `onpointer*` event handlers, shared by every element that
|
|
1646
|
-
* fronts an engine entity. Spread into `observedAttributes` by subclasses.
|
|
1647
|
-
* @internal
|
|
1648
|
-
*/
|
|
1649
|
-
const POINTER_ATTRIBUTES = [
|
|
1650
|
-
'onpointerenter',
|
|
1651
|
-
'onpointerleave',
|
|
1652
|
-
'onpointerdown',
|
|
1653
|
-
'onpointerup',
|
|
1654
|
-
'onpointermove'
|
|
1655
|
-
];
|
|
1656
|
-
/**
|
|
1657
|
-
* The base class for elements that front an engine {@link Entity}: `<pc-entity>` and
|
|
1658
|
-
* `<pc-model>`, which create one, and `<pc-node>`, which binds to one inside a model's
|
|
1659
|
-
* instantiated hierarchy. It carries what all of them need — the `entity` contract, registration
|
|
1660
|
-
* with the owning application (which joins picked scene nodes back to elements by identity,
|
|
1661
|
-
* never by name), and the pointer listener bookkeeping that lets the application lazily attach
|
|
1662
|
-
* its canvas handlers.
|
|
1663
|
-
*/
|
|
1664
|
-
class EntityBaseElement extends AsyncElement {
|
|
1665
|
-
_entity = null;
|
|
1666
|
-
/**
|
|
1667
|
-
* The application element this entity is registered with, cached at registration time so the
|
|
1668
|
-
* entity can be unregistered even once this element has left the DOM.
|
|
1669
|
-
*/
|
|
1670
|
-
_appElement = null;
|
|
1671
|
-
/**
|
|
1672
|
-
* The pointer event listeners for the entity.
|
|
1673
|
-
*/
|
|
1674
|
-
_listeners = {};
|
|
1675
|
-
/**
|
|
1676
|
-
* The event types for which an inline `onpointer*` attribute is currently present.
|
|
1677
|
-
*/
|
|
1678
|
-
_inlineHandlerTypes = new Set();
|
|
1679
|
-
/**
|
|
1680
|
-
* The PlayCanvas entity instance. `null` until the element is ready, and again once the
|
|
1681
|
-
* entity is gone — await {@link whenReady} or the element's `ready()` promise before
|
|
1682
|
-
* accessing it.
|
|
1683
|
-
* @returns The entity instance, or `null`.
|
|
1684
|
-
*/
|
|
1685
|
-
get entity() {
|
|
1686
|
-
return this._entity;
|
|
1687
|
-
}
|
|
1688
|
-
/**
|
|
1689
|
-
* Registers `entity` as this element's backing entity with the owning application, which
|
|
1690
|
-
* joins engine nodes back to elements by identity (never by name).
|
|
1691
|
-
*
|
|
1692
|
-
* @param entity - The entity to register.
|
|
1693
|
-
*/
|
|
1694
|
-
_registerEntity(entity) {
|
|
1695
|
-
this._appElement = this.closestApp;
|
|
1696
|
-
this._appElement?._registerEntityElement(entity, this);
|
|
1697
|
-
}
|
|
1698
|
-
/**
|
|
1699
|
-
* Removes the registration for `entity`.
|
|
1700
|
-
*
|
|
1701
|
-
* @param entity - The entity to unregister.
|
|
1702
|
-
*/
|
|
1703
|
-
_unregisterEntity(entity) {
|
|
1704
|
-
this._appElement?._unregisterEntityElement(entity);
|
|
1705
|
-
this._appElement = null;
|
|
1706
|
-
}
|
|
1707
|
-
/**
|
|
1708
|
-
* Tracks whether an inline `onpointer*` attribute is present. The browser itself compiles and
|
|
1709
|
-
* runs these attributes — they are standard `GlobalEventHandlers`, so setting one replaces
|
|
1710
|
-
* the previous handler and removing it removes the handler, exactly like `onclick` on any
|
|
1711
|
-
* HTML element. But because they bypass {@link EventTarget.addEventListener}, the connect/disconnect
|
|
1712
|
-
* bookkeeping that lets the application lazily attach its canvas pointer handlers must be
|
|
1713
|
-
* kept in sync here.
|
|
1714
|
-
*
|
|
1715
|
-
* @param name - The attribute name (e.g. 'onpointerdown').
|
|
1716
|
-
* @param value - The attribute value, or `null` when the attribute has been removed.
|
|
1717
|
-
*/
|
|
1718
|
-
_updateInlineHandler(name, value) {
|
|
1719
|
-
const type = name.substring(2);
|
|
1720
|
-
const had = this._inlineHandlerTypes.has(type);
|
|
1721
|
-
const has = value !== null;
|
|
1722
|
-
if (has && !had) {
|
|
1723
|
-
this._inlineHandlerTypes.add(type);
|
|
1724
|
-
this.dispatchEvent(new CustomEvent(`${type}:connect`, { bubbles: true }));
|
|
1725
|
-
}
|
|
1726
|
-
else if (!has && had) {
|
|
1727
|
-
this._inlineHandlerTypes.delete(type);
|
|
1728
|
-
this.dispatchEvent(new CustomEvent(`${type}:disconnect`, { bubbles: true }));
|
|
1729
|
-
}
|
|
1730
|
-
}
|
|
1731
|
-
addEventListener(type, listener, options) {
|
|
1732
|
-
if (!this._listeners[type]) {
|
|
1733
|
-
this._listeners[type] = [];
|
|
1734
|
-
}
|
|
1735
|
-
this._listeners[type].push(listener);
|
|
1736
|
-
super.addEventListener(type, listener, options);
|
|
1737
|
-
if (type.startsWith('pointer')) {
|
|
1738
|
-
this.dispatchEvent(new CustomEvent(`${type}:connect`, { bubbles: true }));
|
|
1739
|
-
}
|
|
1740
|
-
}
|
|
1741
|
-
removeEventListener(type, listener, options) {
|
|
1742
|
-
if (this._listeners[type]) {
|
|
1743
|
-
this._listeners[type] = this._listeners[type].filter((l) => l !== listener);
|
|
1744
|
-
}
|
|
1745
|
-
super.removeEventListener(type, listener, options);
|
|
1746
|
-
if (type.startsWith('pointer')) {
|
|
1747
|
-
this.dispatchEvent(new CustomEvent(`${type}:disconnect`, { bubbles: true }));
|
|
1748
|
-
}
|
|
1749
|
-
}
|
|
1750
|
-
/**
|
|
1751
|
-
* Whether the element has a listener for an event type, registered either with
|
|
1752
|
-
* {@link EventTarget.addEventListener} or with the matching inline `onpointer*` attribute. Read by the
|
|
1753
|
-
* containing `<pc-app>` element to gate pointer event synthesis.
|
|
1754
|
-
*
|
|
1755
|
-
* @param type - The event type.
|
|
1756
|
-
* @returns Whether a listener is registered.
|
|
1757
|
-
* @internal
|
|
1758
|
-
*/
|
|
1759
|
-
_hasListeners(type) {
|
|
1760
|
-
return Boolean(this._listeners[type]?.length) || this._inlineHandlerTypes.has(type);
|
|
1761
|
-
}
|
|
1762
|
-
}
|
|
1763
|
-
|
|
1764
1857
|
/**
|
|
1765
1858
|
* Creates and parents the entities of every descendant entity-owning element of `root`, in two
|
|
1766
1859
|
* passes so that no parent's existence depends on document order. Called wherever a subtree could
|
|
@@ -2028,8 +2121,8 @@ class EntityOwnerElement extends EntityBaseElement {
|
|
|
2028
2121
|
*
|
|
2029
2122
|
* The pointer events below are dispatched by the containing `<pc-app>` element when the pointer
|
|
2030
2123
|
* intersects this entity's geometry. They are only generated while the entity has a listener for
|
|
2031
|
-
* them, registered either with {@link EventTarget.addEventListener} or with the matching inline
|
|
2032
|
-
* attribute.
|
|
2124
|
+
* them, registered either with {@link EventTarget.addEventListener} or with the matching inline
|
|
2125
|
+
* attribute (`onpointerdown`, `onclick`, ...).
|
|
2033
2126
|
*
|
|
2034
2127
|
* @elementSummary The `<pc-entity>` element creates an entity: a named, transformable node of the
|
|
2035
2128
|
* scene hierarchy, and the host for component elements such as `<pc-camera>`, `<pc-light>` and
|
|
@@ -2049,11 +2142,17 @@ class EntityOwnerElement extends EntityBaseElement {
|
|
|
2049
2142
|
* entity.
|
|
2050
2143
|
* @attribute {string} onpointerup - Script to run when a pointer button is released over the
|
|
2051
2144
|
* entity.
|
|
2145
|
+
* @attribute {string} onclick - Script to run when the entity is clicked: a primary pointer
|
|
2146
|
+
* button pressed and then released over it.
|
|
2052
2147
|
* @fires {PointerEvent} pointerenter - Fired when the pointer moves onto the entity.
|
|
2053
2148
|
* @fires {PointerEvent} pointerleave - Fired when the pointer moves off the entity.
|
|
2054
2149
|
* @fires {PointerEvent} pointermove - Fired when the pointer moves over the entity.
|
|
2055
2150
|
* @fires {PointerEvent} pointerdown - Fired when a pointer button is pressed over the entity.
|
|
2056
2151
|
* @fires {PointerEvent} pointerup - Fired when a pointer button is released over the entity.
|
|
2152
|
+
* @fires {PointerEvent} click - Fired when a primary pointer button is pressed and then released
|
|
2153
|
+
* over the entity. A press and release that picked different entities fires on their nearest
|
|
2154
|
+
* common ancestor instead, as in the DOM. `detail` carries the click count, so a double click
|
|
2155
|
+
* arrives as a click whose `detail` is 2.
|
|
2057
2156
|
*/
|
|
2058
2157
|
class EntityElement extends EntityOwnerElement {
|
|
2059
2158
|
connectedCallback() {
|
|
@@ -2088,7 +2187,7 @@ class EntityElement extends EntityOwnerElement {
|
|
|
2088
2187
|
this._entity?.destroy();
|
|
2089
2188
|
}
|
|
2090
2189
|
static get observedAttributes() {
|
|
2091
|
-
return ['enabled', 'name', 'position', 'rotation', 'scale', 'tags', ...
|
|
2190
|
+
return ['enabled', 'name', 'position', 'rotation', 'scale', 'tags', ...EVENT_ATTRIBUTES];
|
|
2092
2191
|
}
|
|
2093
2192
|
attributeChangedCallback(name, _oldValue, newValue) {
|
|
2094
2193
|
switch (name) {
|
|
@@ -2115,6 +2214,7 @@ class EntityElement extends EntityOwnerElement {
|
|
|
2115
2214
|
case 'onpointerdown':
|
|
2116
2215
|
case 'onpointerup':
|
|
2117
2216
|
case 'onpointermove':
|
|
2217
|
+
case 'onclick':
|
|
2118
2218
|
this._updateInlineHandler(name, newValue);
|
|
2119
2219
|
break;
|
|
2120
2220
|
}
|
|
@@ -3064,11 +3164,17 @@ const formatHierarchy = (root, counts) => {
|
|
|
3064
3164
|
* model.
|
|
3065
3165
|
* @attribute {string} onpointerup - Script to run when a pointer button is released over the
|
|
3066
3166
|
* model.
|
|
3167
|
+
* @attribute {string} onclick - Script to run when the model is clicked: a primary pointer
|
|
3168
|
+
* button pressed and then released over it.
|
|
3067
3169
|
* @fires {PointerEvent} pointerenter - Fired when the pointer moves onto the model.
|
|
3068
3170
|
* @fires {PointerEvent} pointerleave - Fired when the pointer moves off the model.
|
|
3069
3171
|
* @fires {PointerEvent} pointermove - Fired when the pointer moves over the model.
|
|
3070
3172
|
* @fires {PointerEvent} pointerdown - Fired when a pointer button is pressed over the model.
|
|
3071
3173
|
* @fires {PointerEvent} pointerup - Fired when a pointer button is released over the model.
|
|
3174
|
+
* @fires {PointerEvent} click - Fired when a primary pointer button is pressed and then released
|
|
3175
|
+
* over the model. A press and release that picked different entities fires on their nearest
|
|
3176
|
+
* common ancestor instead, as in the DOM. `detail` carries the click count, so a double click
|
|
3177
|
+
* arrives as a click whose `detail` is 2.
|
|
3072
3178
|
* @fires {Event} load - Fired each time a container asset finishes instantiating, including
|
|
3073
3179
|
* re-instantiation after `asset` changes. Does not bubble — listen on this element, or use a
|
|
3074
3180
|
* capture-phase listener on an ancestor.
|
|
@@ -3317,7 +3423,7 @@ class ModelElement extends EntityOwnerElement {
|
|
|
3317
3423
|
return this._asset;
|
|
3318
3424
|
}
|
|
3319
3425
|
static get observedAttributes() {
|
|
3320
|
-
return ['asset', 'enabled', 'name', 'position', 'rotation', 'scale', 'tags', ...
|
|
3426
|
+
return ['asset', 'enabled', 'name', 'position', 'rotation', 'scale', 'tags', ...EVENT_ATTRIBUTES];
|
|
3321
3427
|
}
|
|
3322
3428
|
attributeChangedCallback(name, _oldValue, newValue) {
|
|
3323
3429
|
switch (name) {
|
|
@@ -3347,6 +3453,7 @@ class ModelElement extends EntityOwnerElement {
|
|
|
3347
3453
|
case 'onpointerdown':
|
|
3348
3454
|
case 'onpointerup':
|
|
3349
3455
|
case 'onpointermove':
|
|
3456
|
+
case 'onclick':
|
|
3350
3457
|
this._updateInlineHandler(name, newValue);
|
|
3351
3458
|
break;
|
|
3352
3459
|
}
|
|
@@ -4210,8 +4317,6 @@ customElements.define('pc-anim', AnimComponentElement);
|
|
|
4210
4317
|
* @elementSummary The `<pc-anim-clip>` element declares one named animation clip on its parent
|
|
4211
4318
|
* `<pc-anim>`, taken from the `asset` it names or, without one, from the enclosing `<pc-model>`'s
|
|
4212
4319
|
* own animations. Must be a direct child of `<pc-anim>`.
|
|
4213
|
-
*
|
|
4214
|
-
* @category Components
|
|
4215
4320
|
*/
|
|
4216
4321
|
class AnimClipElement extends AsyncElement {
|
|
4217
4322
|
/**
|
|
@@ -13340,11 +13445,17 @@ const levenshtein = (a, b) => {
|
|
|
13340
13445
|
* node.
|
|
13341
13446
|
* @attribute {string} onpointerup - Script to run when a pointer button is released over the
|
|
13342
13447
|
* node.
|
|
13448
|
+
* @attribute {string} onclick - Script to run when the node is clicked: a primary pointer
|
|
13449
|
+
* button pressed and then released over it.
|
|
13343
13450
|
* @fires {PointerEvent} pointerenter - Fired when the pointer moves onto the node.
|
|
13344
13451
|
* @fires {PointerEvent} pointerleave - Fired when the pointer moves off the node.
|
|
13345
13452
|
* @fires {PointerEvent} pointermove - Fired when the pointer moves over the node.
|
|
13346
13453
|
* @fires {PointerEvent} pointerdown - Fired when a pointer button is pressed over the node.
|
|
13347
13454
|
* @fires {PointerEvent} pointerup - Fired when a pointer button is released over the node.
|
|
13455
|
+
* @fires {PointerEvent} click - Fired when a primary pointer button is pressed and then released
|
|
13456
|
+
* over the node. A press and release that picked different entities fires on their nearest
|
|
13457
|
+
* common ancestor instead, as in the DOM. `detail` carries the click count, so a double click
|
|
13458
|
+
* arrives as a click whose `detail` is 2.
|
|
13348
13459
|
*/
|
|
13349
13460
|
class NodeElement extends EntityBaseElement {
|
|
13350
13461
|
_name = '';
|
|
@@ -13994,7 +14105,7 @@ class NodeElement extends EntityBaseElement {
|
|
|
13994
14105
|
'rotation',
|
|
13995
14106
|
'scale',
|
|
13996
14107
|
'tags',
|
|
13997
|
-
...
|
|
14108
|
+
...EVENT_ATTRIBUTES
|
|
13998
14109
|
];
|
|
13999
14110
|
}
|
|
14000
14111
|
attributeChangedCallback(name, _oldValue, newValue) {
|
|
@@ -14044,6 +14155,7 @@ class NodeElement extends EntityBaseElement {
|
|
|
14044
14155
|
case 'onpointerdown':
|
|
14045
14156
|
case 'onpointerup':
|
|
14046
14157
|
case 'onpointermove':
|
|
14158
|
+
case 'onclick':
|
|
14047
14159
|
this._updateInlineHandler(name, newValue);
|
|
14048
14160
|
break;
|
|
14049
14161
|
}
|
|
@@ -14058,9 +14170,14 @@ customElements.define('pc-node', NodeElement);
|
|
|
14058
14170
|
* {@link HTMLElement} interface.
|
|
14059
14171
|
*
|
|
14060
14172
|
* @elementSummary The `<pc-scene>` element holds the entity hierarchy the application renders,
|
|
14061
|
-
* along with the scene-wide fog and gravity settings. Must be a direct child of
|
|
14173
|
+
* along with the scene-wide fog, exposure and gravity settings. Must be a direct child of
|
|
14174
|
+
* `<pc-app>`.
|
|
14062
14175
|
*/
|
|
14063
14176
|
class SceneElement extends AsyncElement {
|
|
14177
|
+
/**
|
|
14178
|
+
* The exposure of the scene.
|
|
14179
|
+
*/
|
|
14180
|
+
_exposure = 1;
|
|
14064
14181
|
/**
|
|
14065
14182
|
* The fog type of the scene.
|
|
14066
14183
|
*/
|
|
@@ -14128,6 +14245,7 @@ class SceneElement extends AsyncElement {
|
|
|
14128
14245
|
}
|
|
14129
14246
|
_updateSceneSettings() {
|
|
14130
14247
|
if (this._scene) {
|
|
14248
|
+
this._scene.exposure = this._exposure;
|
|
14131
14249
|
this._scene.fog.type = this._fog;
|
|
14132
14250
|
this._scene.fog.color = this._fogColor;
|
|
14133
14251
|
this._scene.fog.density = this._fogDensity;
|
|
@@ -14146,6 +14264,24 @@ class SceneElement extends AsyncElement {
|
|
|
14146
14264
|
_applyGravity(value) {
|
|
14147
14265
|
this.closestApp?.app?.systems.rigidbody?.gravity.copy(value);
|
|
14148
14266
|
}
|
|
14267
|
+
/**
|
|
14268
|
+
* Sets the exposure of the scene, which tweaks the overall brightness of the rendered image.
|
|
14269
|
+
* Ignored if the scene is using physical units. Defaults to 1.
|
|
14270
|
+
* @param value - The exposure.
|
|
14271
|
+
*/
|
|
14272
|
+
set exposure(value) {
|
|
14273
|
+
this._exposure = value;
|
|
14274
|
+
if (this.scene) {
|
|
14275
|
+
this.scene.exposure = value;
|
|
14276
|
+
}
|
|
14277
|
+
}
|
|
14278
|
+
/**
|
|
14279
|
+
* Gets the exposure of the scene.
|
|
14280
|
+
* @returns The exposure.
|
|
14281
|
+
*/
|
|
14282
|
+
get exposure() {
|
|
14283
|
+
return this._exposure;
|
|
14284
|
+
}
|
|
14149
14285
|
/**
|
|
14150
14286
|
* Sets the fog type of the scene. Can be `none`, `linear`, `exp` or `exp2`. Defaults to
|
|
14151
14287
|
* `none`.
|
|
@@ -14250,10 +14386,13 @@ class SceneElement extends AsyncElement {
|
|
|
14250
14386
|
return this._gravity;
|
|
14251
14387
|
}
|
|
14252
14388
|
static get observedAttributes() {
|
|
14253
|
-
return ['fog', 'fog-color', 'fog-density', 'fog-start', 'fog-end', 'gravity'];
|
|
14389
|
+
return ['exposure', 'fog', 'fog-color', 'fog-density', 'fog-start', 'fog-end', 'gravity'];
|
|
14254
14390
|
}
|
|
14255
14391
|
attributeChangedCallback(name, _oldValue, newValue) {
|
|
14256
14392
|
switch (name) {
|
|
14393
|
+
case 'exposure':
|
|
14394
|
+
this.exposure = parseNumber(newValue, 1, name);
|
|
14395
|
+
break;
|
|
14257
14396
|
case 'fog':
|
|
14258
14397
|
this.fog = parseEnum(newValue, ['none', 'linear', 'exp', 'exp2'], 'none', name);
|
|
14259
14398
|
break;
|