@playcanvas/web-components 0.20.0 → 0.21.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 +5 -113
- package/dist/app.d.ts +5 -113
- package/dist/asset-binding.d.cts +1 -0
- package/dist/asset-binding.d.ts +1 -0
- package/dist/components/anim-clip.d.cts +8 -10
- package/dist/components/anim-clip.d.ts +8 -10
- package/dist/components/anim-component.d.cts +5 -4
- package/dist/components/anim-component.d.ts +5 -4
- package/dist/components/audio-listener-component.d.cts +5 -4
- package/dist/components/audio-listener-component.d.ts +5 -4
- package/dist/components/button-component.d.cts +5 -4
- package/dist/components/button-component.d.ts +5 -4
- package/dist/components/camera-component.d.cts +5 -4
- package/dist/components/camera-component.d.ts +5 -4
- package/dist/components/collision-component.d.cts +5 -4
- package/dist/components/collision-component.d.ts +5 -4
- package/dist/components/component.d.cts +2 -2
- package/dist/components/component.d.ts +2 -2
- package/dist/components/element-component.d.cts +5 -4
- package/dist/components/element-component.d.ts +5 -4
- package/dist/components/gsplat-component.d.cts +5 -4
- package/dist/components/gsplat-component.d.ts +5 -4
- package/dist/components/joint-component.d.cts +5 -4
- package/dist/components/joint-component.d.ts +5 -4
- package/dist/components/layout-child-component.d.cts +5 -4
- package/dist/components/layout-child-component.d.ts +5 -4
- package/dist/components/layout-group-component.d.cts +5 -4
- package/dist/components/layout-group-component.d.ts +5 -4
- package/dist/components/light-component.d.cts +5 -4
- package/dist/components/light-component.d.ts +5 -4
- package/dist/components/particle-system-component.d.cts +22 -5
- package/dist/components/particle-system-component.d.ts +22 -5
- package/dist/components/render-component.d.cts +5 -4
- package/dist/components/render-component.d.ts +5 -4
- package/dist/components/rigid-body-component.d.cts +5 -4
- package/dist/components/rigid-body-component.d.ts +5 -4
- package/dist/components/screen-component.d.cts +5 -4
- package/dist/components/screen-component.d.ts +5 -4
- package/dist/components/script-component.d.cts +5 -4
- package/dist/components/script-component.d.ts +5 -4
- package/dist/components/scroll-view-component.d.cts +5 -4
- package/dist/components/scroll-view-component.d.ts +5 -4
- package/dist/components/scrollbar-component.d.cts +5 -4
- package/dist/components/scrollbar-component.d.ts +5 -4
- package/dist/components/sound-component.d.cts +5 -4
- package/dist/components/sound-component.d.ts +5 -4
- package/dist/custom-elements.json +38 -38
- package/dist/entity-reference.d.cts +13 -0
- package/dist/entity-reference.d.ts +13 -0
- package/dist/material.d.cts +8 -6
- package/dist/material.d.ts +8 -6
- package/dist/model.d.cts +8 -9
- package/dist/model.d.ts +8 -9
- package/dist/parse.d.cts +0 -8
- package/dist/parse.d.ts +0 -8
- package/dist/pointer-controller.d.cts +1 -0
- package/dist/pointer-controller.d.ts +1 -0
- package/dist/pwc.cjs +1360 -1186
- package/dist/pwc.cjs.map +1 -1
- package/dist/pwc.js +1360 -1186
- 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 +1361 -1187
- package/dist/pwc.mjs.map +1 -1
- package/dist/sky.d.cts +6 -7
- package/dist/sky.d.ts +6 -7
- package/dist/web-types.json +39 -39
- package/package.json +2 -2
- package/src/app.ts +16 -451
- package/src/asset-binding.ts +141 -0
- package/src/components/anim-clip.ts +22 -45
- package/src/components/anim-component.ts +10 -6
- package/src/components/audio-listener-component.ts +6 -5
- package/src/components/button-component.ts +8 -6
- package/src/components/camera-component.ts +6 -5
- package/src/components/collision-component.ts +6 -5
- package/src/components/component.ts +6 -4
- package/src/components/element-component.ts +6 -5
- package/src/components/gsplat-component.ts +6 -5
- package/src/components/joint-component.ts +8 -6
- package/src/components/layout-child-component.ts +6 -5
- package/src/components/layout-group-component.ts +6 -5
- package/src/components/light-component.ts +6 -5
- package/src/components/particle-system-component.ts +56 -30
- package/src/components/render-component.ts +6 -5
- package/src/components/rigid-body-component.ts +6 -5
- package/src/components/screen-component.ts +6 -5
- package/src/components/script-component.ts +8 -10
- package/src/components/scroll-view-component.ts +8 -6
- package/src/components/scrollbar-component.ts +8 -6
- package/src/components/sound-component.ts +6 -5
- package/src/entity-reference.ts +237 -0
- package/src/material.ts +25 -29
- package/src/model.ts +24 -48
- package/src/parse.ts +0 -232
- package/src/pointer-controller.ts +550 -0
- package/src/sky.ts +19 -30
package/src/app.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { GraphicsDevice, GraphNode, Entity } from 'playcanvas';
|
|
2
2
|
import {
|
|
3
3
|
AppBase,
|
|
4
4
|
AppOptions,
|
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
FILLMODE_NONE,
|
|
8
8
|
Keyboard,
|
|
9
9
|
Mouse,
|
|
10
|
-
Picker,
|
|
11
10
|
RESOLUTION_AUTO,
|
|
12
11
|
AnimComponentSystem,
|
|
13
12
|
AnimationComponentSystem,
|
|
@@ -60,8 +59,7 @@ import {
|
|
|
60
59
|
BatchManager,
|
|
61
60
|
SoundManager,
|
|
62
61
|
Lightmapper,
|
|
63
|
-
XrManager
|
|
64
|
-
MeshInstance
|
|
62
|
+
XrManager
|
|
65
63
|
} from 'playcanvas';
|
|
66
64
|
|
|
67
65
|
import type { AssetElement } from './asset';
|
|
@@ -72,57 +70,9 @@ import type { EntityOwnerElement } from './entity-owner';
|
|
|
72
70
|
import { LoadingBar } from './loading-bar';
|
|
73
71
|
import type { MaterialElement } from './material';
|
|
74
72
|
import { parseBool, parseEnum, parseNumber } from './parse';
|
|
73
|
+
import { PointerController } from './pointer-controller';
|
|
75
74
|
import type { WasmElement } from './wasm';
|
|
76
75
|
|
|
77
|
-
/**
|
|
78
|
-
* The event types whose listeners make an element a hover target. Hover resolution walks past
|
|
79
|
-
* elements listening for none of them, so a silent element never swallows an ancestor's
|
|
80
|
-
* enter/leave pair.
|
|
81
|
-
*/
|
|
82
|
-
const hoverEventTypes = ['pointerenter', 'pointerleave', 'pointermove'] as const;
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* The canvas listeners each synthesized event type is driven by. Enter and leave are derived
|
|
86
|
-
* from move picks. A click is concluded from the down/up pair, with pointercancel discarding a
|
|
87
|
-
* press the browser takes back (for example a touch that becomes a scroll).
|
|
88
|
-
*/
|
|
89
|
-
const canvasEventsFor: Record<(typeof SYNTHESIZED_EVENTS)[number], readonly string[]> = {
|
|
90
|
-
pointermove: ['pointermove'],
|
|
91
|
-
pointerenter: ['pointermove'],
|
|
92
|
-
pointerleave: ['pointermove'],
|
|
93
|
-
pointerdown: ['pointerdown'],
|
|
94
|
-
pointerup: ['pointerup'],
|
|
95
|
-
click: ['pointerdown', 'pointerup', 'pointercancel']
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* How long after a click a further click on the same target still raises the click count that
|
|
100
|
-
* `detail` carries, approximating the platform's double-click time.
|
|
101
|
-
*/
|
|
102
|
-
const CLICK_CHAIN_MS = 500;
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Finds the nearest common inclusive ancestor of two picked nodes - the node a click belongs to
|
|
106
|
-
* when the press and the release picked different geometry, exactly as the DOM assigns a click
|
|
107
|
-
* whose down and up have different targets.
|
|
108
|
-
*
|
|
109
|
-
* @param a - The node the press picked, or `null`.
|
|
110
|
-
* @param b - The node the release picked, or `null`.
|
|
111
|
-
* @returns The nearest common inclusive ancestor, or `null` when there is none.
|
|
112
|
-
*/
|
|
113
|
-
const commonAncestor = (a: GraphNode | null, b: GraphNode | null): GraphNode | null => {
|
|
114
|
-
const ancestors = new Set<GraphNode>();
|
|
115
|
-
for (let node = a; node !== null; node = node.parent) {
|
|
116
|
-
ancestors.add(node);
|
|
117
|
-
}
|
|
118
|
-
for (let node = b; node !== null; node = node.parent) {
|
|
119
|
-
if (ancestors.has(node)) {
|
|
120
|
-
return node;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
return null;
|
|
124
|
-
};
|
|
125
|
-
|
|
126
76
|
/**
|
|
127
77
|
* Gives `pc-app` the sizing contract of a replaced element (`<video>`, `<img>`): a block-level
|
|
128
78
|
* box that the page's CSS sizes, defaulting to the canvas's own 300x150 intrinsic size, with the
|
|
@@ -220,44 +170,16 @@ class AppElement extends AsyncElement {
|
|
|
220
170
|
*/
|
|
221
171
|
private _entityElements = new Map<GraphNode, EntityBaseElement>();
|
|
222
172
|
|
|
223
|
-
private _picker: Picker | null = null;
|
|
224
|
-
|
|
225
|
-
private _hoveredEntity: EntityBaseElement | null = null;
|
|
226
|
-
|
|
227
|
-
// Identifies the newest in-flight hover pick, so out-of-order results can be discarded
|
|
228
|
-
private _pickToken = 0;
|
|
229
|
-
|
|
230
|
-
private _pointerHandlers: Record<string, EventListener | null> = {
|
|
231
|
-
pointermove: null,
|
|
232
|
-
pointerdown: null,
|
|
233
|
-
pointerup: null,
|
|
234
|
-
pointercancel: null
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* The pick of each pointer's primary-button press, keyed by pointerId and kept while a click
|
|
239
|
-
* may still conclude it. The promise is stored rather than its result, so a release can
|
|
240
|
-
* await a press pick that has not resolved yet. Entries are removed by the matching
|
|
241
|
-
* pointerup or pointercancel, and only ever stored while some element listens for click -
|
|
242
|
-
* which is also what keeps those two canvas listeners attached.
|
|
243
|
-
*/
|
|
244
|
-
private _downPicks = new Map<number, Promise<GraphNode | null>>();
|
|
245
|
-
|
|
246
|
-
/** Whether any element in the tree listens for click. Maintained by _syncCanvasListeners. */
|
|
247
|
-
private _clickListened = false;
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* The previous click's target, time and count, for chaining successive clicks into the
|
|
251
|
-
* click count that `detail` carries. `null` until a click has fired.
|
|
252
|
-
*/
|
|
253
|
-
private _lastClick: { element: EntityBaseElement; time: number; count: number } | null = null;
|
|
254
|
-
|
|
255
173
|
/**
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
174
|
+
* The pointer-input subsystem: the picker, the canvas handlers, and the synthesized-event
|
|
175
|
+
* dispatch. The element drives its lifecycle (connect on boot, resize with the drawing
|
|
176
|
+
* buffer, listener syncs, disconnect on teardown) and hands it the two lookups it needs -
|
|
177
|
+
* everything else about pointer input lives in the controller.
|
|
259
178
|
*/
|
|
260
|
-
private
|
|
179
|
+
private _pointer = new PointerController({
|
|
180
|
+
elementFromNode: (node) => this._entityElements.get(node) ?? null,
|
|
181
|
+
pointerTargets: () => Array.from(this.querySelectorAll<EntityBaseElement>('pc-entity, pc-model, pc-node'))
|
|
182
|
+
});
|
|
261
183
|
|
|
262
184
|
private _app: AppBase | null = null;
|
|
263
185
|
|
|
@@ -303,8 +225,8 @@ class AppElement extends AsyncElement {
|
|
|
303
225
|
// entities. Registered once here rather than on every boot - the sync no-ops while there
|
|
304
226
|
// is no canvas, and a re-booted element must not stack a second set.
|
|
305
227
|
SYNTHESIZED_EVENTS.forEach((type) => {
|
|
306
|
-
this.addEventListener(`${type}:connect`, () => this.
|
|
307
|
-
this.addEventListener(`${type}:disconnect`, () => this.
|
|
228
|
+
this.addEventListener(`${type}:connect`, () => this._pointer.syncListeners());
|
|
229
|
+
this.addEventListener(`${type}:disconnect`, () => this._pointer.syncListeners());
|
|
308
230
|
});
|
|
309
231
|
}
|
|
310
232
|
|
|
@@ -497,7 +419,7 @@ class AppElement extends AsyncElement {
|
|
|
497
419
|
app.setCanvasFillMode(FILLMODE_NONE);
|
|
498
420
|
app.setCanvasResolution(RESOLUTION_AUTO);
|
|
499
421
|
|
|
500
|
-
this.
|
|
422
|
+
this._pointer.connect(app, this._canvas!);
|
|
501
423
|
|
|
502
424
|
// Track the element's box rather than the window: containers resize without any window
|
|
503
425
|
// event (splitter drags, flex reflow, animations). Guarded because jsdom has no
|
|
@@ -607,7 +529,7 @@ class AppElement extends AsyncElement {
|
|
|
607
529
|
this._bootGeneration++;
|
|
608
530
|
|
|
609
531
|
this._optionsLocked = false;
|
|
610
|
-
this.
|
|
532
|
+
this._pointer.disconnect();
|
|
611
533
|
|
|
612
534
|
// Clean up the application. Destroying it destroys every entity, whose destroy hooks
|
|
613
535
|
// unregister them - clear() covers any entity the engine no longer reached.
|
|
@@ -648,57 +570,7 @@ class AppElement extends AsyncElement {
|
|
|
648
570
|
}
|
|
649
571
|
this.app.updateCanvasSize();
|
|
650
572
|
const { width, height } = this.app.graphicsDevice;
|
|
651
|
-
this.
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
private _pickerCreate() {
|
|
655
|
-
const { width, height } = this.app!.graphicsDevice;
|
|
656
|
-
this._picker = new Picker(this.app!, width, height);
|
|
657
|
-
|
|
658
|
-
// Create bound handlers but don't attach them yet. The move handler is async, so it is
|
|
659
|
-
// wrapped to discard the promise - a listener must not return one.
|
|
660
|
-
const listener = (handler: (event: PointerEvent) => void | Promise<void>): EventListener => {
|
|
661
|
-
return (event: Event) => {
|
|
662
|
-
handler.call(this, event as PointerEvent);
|
|
663
|
-
};
|
|
664
|
-
};
|
|
665
|
-
|
|
666
|
-
this._pointerHandlers.pointermove = listener(this._onPointerMove);
|
|
667
|
-
this._pointerHandlers.pointerdown = listener(this._onPointerDown);
|
|
668
|
-
this._pointerHandlers.pointerup = listener(this._onPointerUp);
|
|
669
|
-
this._pointerHandlers.pointercancel = (event: Event) => {
|
|
670
|
-
this._downPicks.delete((event as PointerEvent).pointerId);
|
|
671
|
-
};
|
|
672
|
-
|
|
673
|
-
// Attach canvas listeners for element listeners registered before this boot (e.g.
|
|
674
|
-
// handlers created from inline attributes when their elements were first upgraded, or
|
|
675
|
-
// listeners carried over from before a re-boot)
|
|
676
|
-
this._syncCanvasListeners();
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
private _pickerDestroy() {
|
|
680
|
-
if (this._canvas) {
|
|
681
|
-
Object.entries(this._pointerHandlers).forEach(([type, handler]) => {
|
|
682
|
-
if (handler) {
|
|
683
|
-
this._canvas!.removeEventListener(type, handler);
|
|
684
|
-
}
|
|
685
|
-
});
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
this._picker = null;
|
|
689
|
-
this._hoveredEntity = null;
|
|
690
|
-
this._pointerHandlers = {
|
|
691
|
-
pointermove: null,
|
|
692
|
-
pointerdown: null,
|
|
693
|
-
pointerup: null,
|
|
694
|
-
pointercancel: null
|
|
695
|
-
};
|
|
696
|
-
this._downPicks.clear();
|
|
697
|
-
this._clickListened = false;
|
|
698
|
-
this._lastClick = null;
|
|
699
|
-
|
|
700
|
-
// Replace the chain: a pick that never resolves must not stall a later boot's dispatches
|
|
701
|
-
this._dispatchChain = Promise.resolve();
|
|
573
|
+
this._pointer.resize(width, height);
|
|
702
574
|
}
|
|
703
575
|
|
|
704
576
|
/**
|
|
@@ -736,313 +608,6 @@ class AppElement extends AsyncElement {
|
|
|
736
608
|
return this._entityElements.get(entity) ?? null;
|
|
737
609
|
}
|
|
738
610
|
|
|
739
|
-
/**
|
|
740
|
-
* Resolves the element that owns hover for a picked node: the nearest node up the parent
|
|
741
|
-
* chain - starting with the node itself - whose element listens for any of the hover event
|
|
742
|
-
* types. Skipping silent elements matches {@link _elementWithListener}, so a registered
|
|
743
|
-
* element with no hover listeners (a `<pc-model>` host, a plain child entity) is transparent
|
|
744
|
-
* to hover rather than swallowing a listening ancestor's enter/leave pair.
|
|
745
|
-
*
|
|
746
|
-
* @param node - The picked node, or `null`.
|
|
747
|
-
* @returns The hover-owning element, or `null`.
|
|
748
|
-
*/
|
|
749
|
-
private _hoverTarget(node: GraphNode | null): EntityBaseElement | null {
|
|
750
|
-
while (node !== null) {
|
|
751
|
-
const element = this._entityElements.get(node);
|
|
752
|
-
if (element && hoverEventTypes.some((type) => element._hasListeners(type))) {
|
|
753
|
-
return element;
|
|
754
|
-
}
|
|
755
|
-
node = node.parent;
|
|
756
|
-
}
|
|
757
|
-
return null;
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
/**
|
|
761
|
-
* Like {@link _elementFromNode}, but skips elements without a listener for `type`, so a hit
|
|
762
|
-
* on an unlistened child still reaches a listening ancestor.
|
|
763
|
-
*
|
|
764
|
-
* @param node - The picked node, or `null`.
|
|
765
|
-
* @param type - The pointer event type a listener is required for.
|
|
766
|
-
* @returns The nearest listening element, or `null`.
|
|
767
|
-
*/
|
|
768
|
-
private _elementWithListener(node: GraphNode | null, type: string): EntityBaseElement | null {
|
|
769
|
-
while (node !== null) {
|
|
770
|
-
const element = this._entityElements.get(node);
|
|
771
|
-
if (element?._hasListeners(type)) {
|
|
772
|
-
return element;
|
|
773
|
-
}
|
|
774
|
-
node = node.parent;
|
|
775
|
-
}
|
|
776
|
-
return null;
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
/**
|
|
780
|
-
* Converts a pointer event's client coordinates into drawing-buffer coordinates - the space
|
|
781
|
-
* the pick buffer and the camera viewports are laid out in. When the canvas has no CSS box
|
|
782
|
-
* to map through (jsdom; a hidden canvas receives no pointer events in a browser), the
|
|
783
|
-
* client coordinates are passed through unmapped and `mapped` is false, so callers know the
|
|
784
|
-
* coordinates correspond to no real geometry.
|
|
785
|
-
*
|
|
786
|
-
* @param event - The pointer event to convert.
|
|
787
|
-
* @param canvas - The canvas the event was dispatched on.
|
|
788
|
-
* @returns The buffer-space coordinates, and whether they were actually mapped.
|
|
789
|
-
*/
|
|
790
|
-
private _getPickerCoordinates(
|
|
791
|
-
event: PointerEvent,
|
|
792
|
-
canvas: HTMLCanvasElement
|
|
793
|
-
): { x: number; y: number; mapped: boolean } {
|
|
794
|
-
const canvasRect = canvas.getBoundingClientRect();
|
|
795
|
-
if (canvasRect.width === 0 || canvasRect.height === 0) {
|
|
796
|
-
return { x: event.clientX, y: event.clientY, mapped: false };
|
|
797
|
-
}
|
|
798
|
-
const scaleX = canvas.width / canvasRect.width;
|
|
799
|
-
const scaleY = canvas.height / canvasRect.height;
|
|
800
|
-
return {
|
|
801
|
-
x: (event.clientX - canvasRect.left) * scaleX,
|
|
802
|
-
y: (event.clientY - canvasRect.top) * scaleY,
|
|
803
|
-
mapped: true
|
|
804
|
-
};
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
/**
|
|
808
|
-
* Whether a camera's viewport contains the point. A camera renders into its normalized
|
|
809
|
-
* `rect`, whose origin is the bottom-left of the canvas while buffer coordinates run from
|
|
810
|
-
* the top-left - so the vertical test flips, as the engine's ElementInput flips it for UI
|
|
811
|
-
* input. The right and bottom edges are exclusive: a viewport rasterizes the half-open
|
|
812
|
-
* pixel range [left, right) x [top, bottom), so a coordinate on a shared edge belongs to
|
|
813
|
-
* the viewport whose first pixel it is - never to the one it just left, whose pick buffer
|
|
814
|
-
* holds nothing there.
|
|
815
|
-
*
|
|
816
|
-
* @param camera - The camera to test.
|
|
817
|
-
* @param x - The x coordinate, in buffer space.
|
|
818
|
-
* @param y - The y coordinate, in buffer space.
|
|
819
|
-
* @param canvas - The canvas the coordinates are relative to.
|
|
820
|
-
* @returns Whether the camera's viewport contains the point.
|
|
821
|
-
*/
|
|
822
|
-
private _cameraContains(camera: CameraComponent, x: number, y: number, canvas: HTMLCanvasElement): boolean {
|
|
823
|
-
const rect = camera.rect;
|
|
824
|
-
const left = rect.x * canvas.width;
|
|
825
|
-
const bottom = (1 - rect.y) * canvas.height;
|
|
826
|
-
const top = bottom - rect.w * canvas.height;
|
|
827
|
-
return x >= left && x < left + rect.z * canvas.width && y >= top && y < bottom;
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
/**
|
|
831
|
-
* Picks the scene under the pointer and returns the graph node that was hit, or `null`.
|
|
832
|
-
*
|
|
833
|
-
* The camera is resolved the way the engine's ElementInput resolves it for UI input:
|
|
834
|
-
* enabled cameras are tried topmost-first (they render in ascending `priority` order),
|
|
835
|
-
* skipping cameras that render to a texture and cameras whose viewport `rect` does not
|
|
836
|
-
* contain the pointer. A camera that picks nothing ends the search if it clears the color
|
|
837
|
-
* buffer - its background visually owns the pixel - and otherwise cedes to the cameras
|
|
838
|
-
* beneath it, so an overlay camera only intercepts picks where it actually drew something.
|
|
839
|
-
* The pick buffer is prepared per camera, so each camera picks from its own layers.
|
|
840
|
-
*
|
|
841
|
-
* The read back is asynchronous because the synchronous {@link Picker.getSelection} is not
|
|
842
|
-
* supported on WebGPU, where it returns an empty selection rather than failing - which
|
|
843
|
-
* silently disabled every `onpointer*` handler once WebGPU became the resolved backend. The
|
|
844
|
-
* async variant works on both backends and does not block the main thread on a GPU read.
|
|
845
|
-
*
|
|
846
|
-
* @param event - The pointer event to pick under.
|
|
847
|
-
* @returns The graph node under the pointer, or `null` if nothing was hit.
|
|
848
|
-
*/
|
|
849
|
-
private async _pickNode(event: PointerEvent): Promise<GraphNode | null> {
|
|
850
|
-
const app = this.app;
|
|
851
|
-
const picker = this._picker;
|
|
852
|
-
const canvas = this._canvas;
|
|
853
|
-
if (!app || !picker || !canvas) return null;
|
|
854
|
-
|
|
855
|
-
const { x, y, mapped } = this._getPickerCoordinates(event, canvas);
|
|
856
|
-
|
|
857
|
-
// Walked from the end: the array is sorted by ascending priority, so the last camera
|
|
858
|
-
// renders last and sits on top. Read through .at() because a pick handler may remove
|
|
859
|
-
// cameras while an earlier iteration's read back is in flight.
|
|
860
|
-
const cameras = app.systems.camera?.cameras ?? [];
|
|
861
|
-
for (let i = cameras.length - 1; i >= 0; i--) {
|
|
862
|
-
const camera = cameras.at(i);
|
|
863
|
-
|
|
864
|
-
// A camera rendering to a texture is not on the canvas.
|
|
865
|
-
if (!camera || camera.renderTarget) continue;
|
|
866
|
-
|
|
867
|
-
// Coordinates that could not be mapped cannot be tested for containment.
|
|
868
|
-
if (mapped && !this._cameraContains(camera, x, y, canvas)) continue;
|
|
869
|
-
|
|
870
|
-
picker.prepare(camera, app.scene);
|
|
871
|
-
const selection = await picker.getSelectionAsync(x, y);
|
|
872
|
-
|
|
873
|
-
// The element may have disconnected while the read back was in flight.
|
|
874
|
-
if (!this._picker || !this.app) return null;
|
|
875
|
-
|
|
876
|
-
if (selection.length > 0) {
|
|
877
|
-
const item = selection[0];
|
|
878
|
-
return item instanceof MeshInstance ? item.node : (item as GSplatComponent).entity;
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
// Nothing hit. A camera that clears the color buffer paints its background over
|
|
882
|
-
// everything beneath it, so the miss is final; one that does not is an overlay
|
|
883
|
-
// that the cameras beneath show through, so they get their turn.
|
|
884
|
-
if (camera.clearColorBuffer) return null;
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
return null;
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
private async _onPointerMove(event: PointerEvent) {
|
|
891
|
-
if (!this._picker || !this.app) return;
|
|
892
|
-
|
|
893
|
-
// Moves arrive faster than a pick resolves, so results can land out of order. Only the
|
|
894
|
-
// newest pick may update the hover state - an older one describes a pointer position the
|
|
895
|
-
// user has already left.
|
|
896
|
-
const token = ++this._pickToken;
|
|
897
|
-
const node = await this._pickNode(event);
|
|
898
|
-
if (token !== this._pickToken || !this._picker) return;
|
|
899
|
-
|
|
900
|
-
// The hovered element is the nearest one up the node's parent chain with a hover
|
|
901
|
-
// listener - the nearest-listener rule down/up use. Dispatch is still gated per event
|
|
902
|
-
// type below: having any hover listener selects the target, each event needs its own.
|
|
903
|
-
const newHoverEntity = this._hoverTarget(node);
|
|
904
|
-
|
|
905
|
-
// Handle enter/leave events
|
|
906
|
-
if (this._hoveredEntity !== newHoverEntity) {
|
|
907
|
-
if (this._hoveredEntity && this._hoveredEntity._hasListeners('pointerleave')) {
|
|
908
|
-
this._hoveredEntity.dispatchEvent(new PointerEvent('pointerleave', event));
|
|
909
|
-
}
|
|
910
|
-
if (newHoverEntity && newHoverEntity._hasListeners('pointerenter')) {
|
|
911
|
-
newHoverEntity.dispatchEvent(new PointerEvent('pointerenter', event));
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
// Update hover state
|
|
916
|
-
this._hoveredEntity = newHoverEntity;
|
|
917
|
-
|
|
918
|
-
// Handle pointermove event
|
|
919
|
-
if (newHoverEntity && newHoverEntity._hasListeners('pointermove')) {
|
|
920
|
-
newHoverEntity.dispatchEvent(new PointerEvent('pointermove', event));
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
/**
|
|
925
|
-
* Appends a dispatch step to {@link _dispatchChain}. Must be called synchronously from the
|
|
926
|
-
* canvas event handler - the order of appends is what carries canvas-event order. A step
|
|
927
|
-
* that rejects is reported and released, so the steps queued behind it still dispatch.
|
|
928
|
-
*
|
|
929
|
-
* @param step - The dispatch work to run once every earlier step has finished.
|
|
930
|
-
*/
|
|
931
|
-
private _chainDispatch(step: () => Promise<void>) {
|
|
932
|
-
this._dispatchChain = this._dispatchChain.then(step).catch((error) => {
|
|
933
|
-
console.error(error);
|
|
934
|
-
});
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
private _onPointerDown(event: PointerEvent) {
|
|
938
|
-
if (!this._picker || !this.app) return;
|
|
939
|
-
|
|
940
|
-
// Picks stay concurrent - only the dispatch of the results is serialized
|
|
941
|
-
const pick = this._pickNode(event);
|
|
942
|
-
|
|
943
|
-
// A click concludes on the matching pointerup, which needs to know what the press
|
|
944
|
-
// picked. Primary button only - the only button a click can conclude from - and only
|
|
945
|
-
// while click is listened for, since it is the click mapping that keeps the pointerup
|
|
946
|
-
// and pointercancel listeners attached to clean the entry up again.
|
|
947
|
-
if (this._clickListened && event.button === 0) {
|
|
948
|
-
this._downPicks.set(event.pointerId, pick);
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
this._chainDispatch(async () => {
|
|
952
|
-
const node = await pick;
|
|
953
|
-
if (!this._picker) return; // the element disconnected while the pick was in flight
|
|
954
|
-
|
|
955
|
-
const entityElement = this._elementWithListener(node, 'pointerdown');
|
|
956
|
-
if (entityElement) {
|
|
957
|
-
entityElement.dispatchEvent(new PointerEvent('pointerdown', event));
|
|
958
|
-
}
|
|
959
|
-
});
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
private _onPointerUp(event: PointerEvent) {
|
|
963
|
-
if (!this._picker || !this.app) return;
|
|
964
|
-
|
|
965
|
-
// The press pick this release may conclude as a click. Claimed synchronously, so the
|
|
966
|
-
// entry is gone before any other event for this pointer can be handled.
|
|
967
|
-
const downPick = this._downPicks.get(event.pointerId);
|
|
968
|
-
this._downPicks.delete(event.pointerId);
|
|
969
|
-
|
|
970
|
-
const pick = this._pickNode(event);
|
|
971
|
-
|
|
972
|
-
this._chainDispatch(async () => {
|
|
973
|
-
const node = await pick;
|
|
974
|
-
if (!this._picker) return; // the element disconnected while the pick was in flight
|
|
975
|
-
|
|
976
|
-
const entityElement = this._elementWithListener(node, 'pointerup');
|
|
977
|
-
if (entityElement) {
|
|
978
|
-
entityElement.dispatchEvent(new PointerEvent('pointerup', event));
|
|
979
|
-
}
|
|
980
|
-
});
|
|
981
|
-
|
|
982
|
-
// A click fires where the DOM fires it: at the nearest common inclusive ancestor of
|
|
983
|
-
// what the press and the release picked, for the primary button only. Appended after
|
|
984
|
-
// the release's own step, so it dispatches after the pointerup that concludes it.
|
|
985
|
-
if (!downPick || event.button !== 0) return;
|
|
986
|
-
|
|
987
|
-
this._chainDispatch(async () => {
|
|
988
|
-
// A rejected pick was already reported by the press or release step that awaited it;
|
|
989
|
-
// here it just means no click can conclude.
|
|
990
|
-
const picked = await Promise.all([downPick, pick]).catch(() => null);
|
|
991
|
-
if (!picked || !this._picker) return;
|
|
992
|
-
|
|
993
|
-
const [downNode, upNode] = picked;
|
|
994
|
-
const clickElement = this._elementWithListener(commonAncestor(downNode, upNode), 'click');
|
|
995
|
-
if (clickElement) {
|
|
996
|
-
const click = new PointerEvent('click', event);
|
|
997
|
-
|
|
998
|
-
// The init above copied pointerup's `detail`, which the Pointer Events spec fixes
|
|
999
|
-
// at 0 - but click is exempt: its detail is the click count, chained here as the
|
|
1000
|
-
// platform chains it (same target, within the double-click window). Overridden
|
|
1001
|
-
// with defineProperty because an event instance used as an init dict cannot have
|
|
1002
|
-
// single fields replaced.
|
|
1003
|
-
const time = performance.now();
|
|
1004
|
-
const last = this._lastClick;
|
|
1005
|
-
const count =
|
|
1006
|
-
last && last.element === clickElement && time - last.time <= CLICK_CHAIN_MS ? last.count + 1 : 1;
|
|
1007
|
-
this._lastClick = { element: clickElement, time, count };
|
|
1008
|
-
Object.defineProperty(click, 'detail', { value: count });
|
|
1009
|
-
|
|
1010
|
-
clickElement.dispatchEvent(click);
|
|
1011
|
-
}
|
|
1012
|
-
});
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
/**
|
|
1016
|
-
* Attaches exactly the canvas listeners the tree's current element listeners need, and
|
|
1017
|
-
* detaches the rest. Recomputed whenever a listener connects or disconnects anywhere under
|
|
1018
|
-
* this element: several synthesized types can need the same canvas listener (enter, leave
|
|
1019
|
-
* and move all ride the move pick; click rides the down/up pair), so one type's removal
|
|
1020
|
-
* must not detach a listener another type still uses. Re-attaching an attached listener is
|
|
1021
|
-
* a no-op by EventTarget semantics, so no attach state is kept.
|
|
1022
|
-
*/
|
|
1023
|
-
private _syncCanvasListeners() {
|
|
1024
|
-
const canvas = this._canvas;
|
|
1025
|
-
if (!canvas) return; // not booted yet: _pickerCreate syncs once the handlers exist
|
|
1026
|
-
|
|
1027
|
-
const elements = Array.from(this.querySelectorAll<EntityBaseElement>('pc-entity, pc-model, pc-node'));
|
|
1028
|
-
const needed = new Set<string>();
|
|
1029
|
-
for (const type of SYNTHESIZED_EVENTS) {
|
|
1030
|
-
if (elements.some((element) => element._hasListeners(type))) {
|
|
1031
|
-
canvasEventsFor[type].forEach((canvasType) => needed.add(canvasType));
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
this._clickListened = elements.some((element) => element._hasListeners('click'));
|
|
1035
|
-
|
|
1036
|
-
Object.entries(this._pointerHandlers).forEach(([canvasType, handler]) => {
|
|
1037
|
-
if (!handler) return;
|
|
1038
|
-
if (needed.has(canvasType)) {
|
|
1039
|
-
canvas.addEventListener(canvasType, handler);
|
|
1040
|
-
} else {
|
|
1041
|
-
canvas.removeEventListener(canvasType, handler);
|
|
1042
|
-
}
|
|
1043
|
-
});
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
611
|
/**
|
|
1047
612
|
* Warns that a graphics option was written too late to have any effect. These options are read
|
|
1048
613
|
* once, when the element connects and creates its graphics device, so a later write updates
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { Asset, EventHandle } from 'playcanvas';
|
|
2
|
+
|
|
3
|
+
import { useAsset } from './asset';
|
|
4
|
+
|
|
5
|
+
// Keep `export` on these declarations. TypeScript removes the declaration and its inline export
|
|
6
|
+
// when `stripInternal` is enabled. A separate `export { ... }` statement would remain in the
|
|
7
|
+
// generated .d.ts file and refer to a declaration that had been removed.
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Functions called when an {@link AssetBinding} finishes loading an asset or encounters an
|
|
11
|
+
* error.
|
|
12
|
+
*
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
export type AssetBindingCallbacks = {
|
|
16
|
+
/**
|
|
17
|
+
* Called when the asset has a usable resource. If it was already loaded, this can run before
|
|
18
|
+
* {@link AssetBinding.bind} returns. Otherwise it runs when the asset fires `load`.
|
|
19
|
+
*/
|
|
20
|
+
load: (asset: Asset) => void;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Called when the asset fails to load, including when it had already failed before
|
|
24
|
+
* {@link AssetBinding.bind} was called. If this is omitted, the binding keeps waiting so a
|
|
25
|
+
* later successful reload can still call `load`.
|
|
26
|
+
*/
|
|
27
|
+
error?: (err: string | Error) => void;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Watches the asset currently selected by an element.
|
|
32
|
+
*
|
|
33
|
+
* An element can change its asset while the old one is still loading, or disconnect before the
|
|
34
|
+
* load finishes. This class makes sure callbacks from those older loads do nothing. Calling
|
|
35
|
+
* {@link bind} stops watching the previous asset and starts watching the new one. Calling
|
|
36
|
+
* {@link cancel} stops watching altogether.
|
|
37
|
+
*
|
|
38
|
+
* Asset lookup goes through {@link useAsset}, so selecting a lazy asset starts its load. The
|
|
39
|
+
* caller still decides what to do with the result, such as creating scene content, reporting an
|
|
40
|
+
* error, or marking an element ready.
|
|
41
|
+
*
|
|
42
|
+
* Reuse one `AssetBinding` for each asset-valued property throughout the element's lifetime. It
|
|
43
|
+
* is safe for a callback to call `bind` again: the new asset remains active after the callback
|
|
44
|
+
* returns.
|
|
45
|
+
*
|
|
46
|
+
* @internal
|
|
47
|
+
*/
|
|
48
|
+
export class AssetBinding {
|
|
49
|
+
/**
|
|
50
|
+
* Each bind or cancel gets a new number. Event handlers remember the number they were created
|
|
51
|
+
* with and return if it is no longer current. Old listeners are normally removed as well, but
|
|
52
|
+
* this check also protects against an event that was already in progress when removal
|
|
53
|
+
* happened.
|
|
54
|
+
*/
|
|
55
|
+
private _generation = 0;
|
|
56
|
+
|
|
57
|
+
private _loadHandle: EventHandle | null = null;
|
|
58
|
+
|
|
59
|
+
private _errorHandle: EventHandle | null = null;
|
|
60
|
+
|
|
61
|
+
private _detach() {
|
|
62
|
+
this._loadHandle?.off();
|
|
63
|
+
this._loadHandle = null;
|
|
64
|
+
this._errorHandle?.off();
|
|
65
|
+
this._errorHandle = null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Stops watching the current asset and prevents its callbacks from running. The binding can
|
|
70
|
+
* be used again by calling {@link bind}.
|
|
71
|
+
*/
|
|
72
|
+
cancel() {
|
|
73
|
+
this._generation++;
|
|
74
|
+
this._detach();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Starts watching the asset registered under `id` and stops watching the previous one. A
|
|
79
|
+
* missing `id` still clears the previous binding. Looking up a lazy asset starts its load.
|
|
80
|
+
*
|
|
81
|
+
* If the asset has already loaded successfully, `load` runs before this method returns. An
|
|
82
|
+
* earlier failure calls `error` immediately when that callback is provided; without one, the
|
|
83
|
+
* binding waits for a later successful reload. Assets still loading are watched for the same
|
|
84
|
+
* two outcomes. Once an event is handled, both listeners are removed.
|
|
85
|
+
*
|
|
86
|
+
* @param id - The `id` of the `<pc-asset>` element to bind to.
|
|
87
|
+
* @param callbacks - Functions to call when loading succeeds or fails.
|
|
88
|
+
* @returns The selected asset, or `undefined` if no asset has this `id`. The caller decides
|
|
89
|
+
* how to handle a missing asset.
|
|
90
|
+
*/
|
|
91
|
+
bind(id: string, callbacks: AssetBindingCallbacks): Asset | undefined {
|
|
92
|
+
const generation = ++this._generation;
|
|
93
|
+
this._detach();
|
|
94
|
+
|
|
95
|
+
const asset = useAsset(id);
|
|
96
|
+
if (!asset) {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const { error } = callbacks;
|
|
101
|
+
|
|
102
|
+
if (asset.loaded) {
|
|
103
|
+
// PlayCanvas sets `loaded` after both success and failure, so a resource must also be
|
|
104
|
+
// present before this counts as success. Use `!= null` deliberately: `undefined` and
|
|
105
|
+
// `null` both mean there is no resource, while a valid resource can still be falsy
|
|
106
|
+
// (for example, an empty text file produces '').
|
|
107
|
+
if (asset.resource != null) {
|
|
108
|
+
callbacks.load(asset);
|
|
109
|
+
return asset;
|
|
110
|
+
}
|
|
111
|
+
if (error) {
|
|
112
|
+
error(`asset '${id}' failed to load`);
|
|
113
|
+
return asset;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Old listeners are normally removed by bind or cancel. The number check is a second
|
|
118
|
+
// safeguard for a late event. Check it before _detach so an old callback cannot remove
|
|
119
|
+
// the listeners for the current asset. Whichever current event runs first removes both
|
|
120
|
+
// listeners.
|
|
121
|
+
this._loadHandle = asset.once('load', () => {
|
|
122
|
+
if (generation !== this._generation) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
this._detach();
|
|
126
|
+
callbacks.load(asset);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
if (error) {
|
|
130
|
+
this._errorHandle = asset.once('error', (err: string | Error) => {
|
|
131
|
+
if (generation !== this._generation) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
this._detach();
|
|
135
|
+
error(err);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return asset;
|
|
140
|
+
}
|
|
141
|
+
}
|