1- import { Application , FILLMODE_FILL_WINDOW , Keyboard , Mouse , RESOLUTION_AUTO } from 'playcanvas' ;
1+ import { Application , CameraComponent , FILLMODE_FILL_WINDOW , Keyboard , Mouse , Picker , RESOLUTION_AUTO } from 'playcanvas' ;
22
33import { AssetElement } from './asset' ;
44import { AsyncElement } from './async-element' ;
@@ -27,6 +27,24 @@ class AppElement extends AsyncElement {
2727
2828 private _hierarchyReady = false ;
2929
30+ private _picker : Picker | null = null ;
31+
32+ private _hasPointerListeners : { [ key : string ] : boolean } = {
33+ pointerenter : false ,
34+ pointerleave : false ,
35+ pointerdown : false ,
36+ pointerup : false ,
37+ pointermove : false
38+ } ;
39+
40+ private _hoveredEntity : EntityElement | null = null ;
41+
42+ private _pointerHandlers : { [ key : string ] : EventListener | null } = {
43+ pointermove : null ,
44+ pointerdown : null ,
45+ pointerup : null
46+ } ;
47+
3048 /**
3149 * The PlayCanvas application instance.
3250 */
@@ -71,6 +89,8 @@ class AppElement extends AsyncElement {
7189 this . app . setCanvasFillMode ( FILLMODE_FILL_WINDOW ) ;
7290 this . app . setCanvasResolution ( RESOLUTION_AUTO ) ;
7391
92+ this . _pickerCreate ( ) ;
93+
7494 // Get all pc-asset elements that are direct children of the pc-app element
7595 const assetElements = this . querySelectorAll < AssetElement > ( ':scope > pc-asset' ) ;
7696 Array . from ( assetElements ) . forEach ( ( assetElement ) => {
@@ -113,6 +133,8 @@ class AppElement extends AsyncElement {
113133 }
114134
115135 disconnectedCallback ( ) {
136+ this . _pickerDestroy ( ) ;
137+
116138 // Clean up the application
117139 if ( this . app ) {
118140 this . app . destroy ( ) ;
@@ -135,6 +157,150 @@ class AppElement extends AsyncElement {
135157 }
136158 }
137159
160+ _pickerCreate ( ) {
161+ const { width, height } = this . app ! . graphicsDevice ;
162+ this . _picker = new Picker ( this . app ! , width , height ) ;
163+
164+ // Create bound handlers but don't attach them yet
165+ this . _pointerHandlers . pointermove = this . _onPointerMove . bind ( this ) as EventListener ;
166+ this . _pointerHandlers . pointerdown = this . _onPointerDown . bind ( this ) as EventListener ;
167+ this . _pointerHandlers . pointerup = this . _onPointerUp . bind ( this ) as EventListener ;
168+
169+ // Listen for pointer listeners being added/removed
170+ [ 'pointermove' , 'pointerdown' , 'pointerup' , 'pointerenter' , 'pointerleave' ] . forEach ( ( type ) => {
171+ this . addEventListener ( `${ type } :connect` , ( ) => this . _onPointerListenerAdded ( type ) ) ;
172+ this . addEventListener ( `${ type } :disconnect` , ( ) => this . _onPointerListenerRemoved ( type ) ) ;
173+ } ) ;
174+ }
175+
176+ _pickerDestroy ( ) {
177+ if ( this . _canvas ) {
178+ Object . entries ( this . _pointerHandlers ) . forEach ( ( [ type , handler ] ) => {
179+ if ( handler ) {
180+ this . _canvas ! . removeEventListener ( type , handler ) ;
181+ }
182+ } ) ;
183+ }
184+
185+ this . _picker = null ;
186+ this . _pointerHandlers = {
187+ pointermove : null ,
188+ pointerdown : null ,
189+ pointerup : null
190+ } ;
191+ }
192+
193+ _onPointerMove ( event : PointerEvent ) {
194+ if ( ! this . _picker || ! this . app ) return ;
195+
196+ const camera = this . app ! . root . findComponent ( 'camera' ) as CameraComponent ;
197+ if ( ! camera ) return ;
198+
199+ const canvasRect = this . _canvas ! . getBoundingClientRect ( ) ;
200+ const x = event . clientX - canvasRect . left ;
201+ const y = event . clientY - canvasRect . top ;
202+
203+ this . _picker . prepare ( camera , this . app ! . scene ) ;
204+ const selection = this . _picker . getSelection ( x , y ) ;
205+
206+ // Get the currently hovered entity (if any)
207+ const newHoverEntity = selection . length > 0 ?
208+ this . querySelector ( `pc-entity[name="${ selection [ 0 ] . node . name } "]` ) as EntityElement :
209+ null ;
210+
211+ // Handle enter/leave events
212+ if ( this . _hoveredEntity !== newHoverEntity ) {
213+ if ( this . _hoveredEntity && this . _hoveredEntity . hasListeners ( 'pointerleave' ) ) {
214+ this . _hoveredEntity . dispatchEvent ( new PointerEvent ( 'pointerleave' , event ) ) ;
215+ }
216+ if ( newHoverEntity && newHoverEntity . hasListeners ( 'pointerenter' ) ) {
217+ newHoverEntity . dispatchEvent ( new PointerEvent ( 'pointerenter' , event ) ) ;
218+ }
219+ }
220+
221+ // Update hover state
222+ this . _hoveredEntity = newHoverEntity ;
223+
224+ // Handle pointermove event
225+ if ( newHoverEntity && newHoverEntity . hasListeners ( 'pointermove' ) ) {
226+ newHoverEntity . dispatchEvent ( new PointerEvent ( 'pointermove' , event ) ) ;
227+ }
228+ }
229+
230+ _onPointerDown ( event : PointerEvent ) {
231+ if ( ! this . _picker || ! this . app ) return ;
232+
233+ const camera = this . app ! . root . findComponent ( 'camera' ) as CameraComponent ;
234+ if ( ! camera ) return ;
235+
236+ const canvasRect = this . _canvas ! . getBoundingClientRect ( ) ;
237+ const x = event . clientX - canvasRect . left ;
238+ const y = event . clientY - canvasRect . top ;
239+
240+ this . _picker . prepare ( camera , this . app ! . scene ) ;
241+ const selection = this . _picker . getSelection ( x , y ) ;
242+
243+ if ( selection . length > 0 ) {
244+ const entityElement = this . querySelector ( `pc-entity[name="${ selection [ 0 ] . node . name } "]` ) as EntityElement ;
245+ if ( entityElement && entityElement . hasListeners ( 'pointerdown' ) ) {
246+ entityElement . dispatchEvent ( new PointerEvent ( 'pointerdown' , event ) ) ;
247+ }
248+ }
249+ }
250+
251+ _onPointerUp ( event : PointerEvent ) {
252+ if ( ! this . _picker || ! this . app ) return ;
253+
254+ const camera = this . app ! . root . findComponent ( 'camera' ) as CameraComponent ;
255+ if ( ! camera ) return ;
256+
257+ const canvasRect = this . _canvas ! . getBoundingClientRect ( ) ;
258+ const x = event . clientX - canvasRect . left ;
259+ const y = event . clientY - canvasRect . top ;
260+
261+ this . _picker . prepare ( camera , this . app ! . scene ) ;
262+ const selection = this . _picker . getSelection ( x , y ) ;
263+
264+ if ( selection . length > 0 ) {
265+ const entityElement = this . querySelector ( `pc-entity[name="${ selection [ 0 ] . node . name } "]` ) as EntityElement ;
266+ if ( entityElement && entityElement . hasListeners ( 'pointerup' ) ) {
267+ entityElement . dispatchEvent ( new PointerEvent ( 'pointerup' , event ) ) ;
268+ }
269+ }
270+ }
271+
272+ _onPointerListenerAdded ( type : string ) {
273+ if ( ! this . _hasPointerListeners [ type ] && this . _canvas ) {
274+ this . _hasPointerListeners [ type ] = true ;
275+
276+ // For enter/leave events, we need the move handler
277+ const handler = ( type === 'pointerenter' || type === 'pointerleave' ) ?
278+ this . _pointerHandlers . pointermove :
279+ this . _pointerHandlers [ type ] ;
280+
281+ if ( handler ) {
282+ this . _canvas . addEventListener ( type === 'pointerenter' || type === 'pointerleave' ? 'pointermove' : type , handler ) ;
283+ }
284+ }
285+ }
286+
287+ _onPointerListenerRemoved ( type : string ) {
288+ const hasListeners = Array . from ( this . querySelectorAll < EntityElement > ( 'pc-entity' ) )
289+ . some ( entity => entity . hasListeners ( type ) ) ;
290+
291+ if ( ! hasListeners && this . _canvas ) {
292+ this . _hasPointerListeners [ type ] = false ;
293+
294+ const handler = ( type === 'pointerenter' || type === 'pointerleave' ) ?
295+ this . _pointerHandlers . pointermove :
296+ this . _pointerHandlers [ type ] ;
297+
298+ if ( handler ) {
299+ this . _canvas . removeEventListener ( type === 'pointerenter' || type === 'pointerleave' ? 'pointermove' : type , handler ) ;
300+ }
301+ }
302+ }
303+
138304 /**
139305 * Sets the alpha flag.
140306 * @param value - The alpha flag.
0 commit comments