瀏覽代碼

feat!(events): new event system based on pmdrs/pointer-events

BREAKING CHANGE: Only first element intersected wil trigger the pointer event, no more need to stop propagation on occlusion
alvarosabu 3 月之前
父節點
當前提交
88e31cab82

+ 1 - 0
package.json

@@ -71,6 +71,7 @@
   },
   "dependencies": {
     "@alvarosabu/utils": "^3.2.0",
+    "@pmndrs/pointer-events": "^6.6.17",
     "@vue/devtools-api": "^7.7.2",
     "@vueuse/core": "^12.5.0"
   },

+ 10 - 10
playground/vue/.eslintrc-auto-import.json

@@ -3,13 +3,18 @@
     "Component": true,
     "ComponentPublicInstance": true,
     "ComputedRef": true,
+    "DirectiveBinding": true,
     "EffectScope": true,
     "ExtractDefaultPropTypes": true,
     "ExtractPropTypes": true,
     "ExtractPublicPropTypes": true,
     "InjectionKey": true,
+    "MaybeRef": true,
+    "MaybeRefOrGetter": true,
     "PropType": true,
     "Ref": true,
+    "Slot": true,
+    "Slots": true,
     "VNode": true,
     "WritableComputedRef": true,
     "computed": true,
@@ -41,6 +46,7 @@
     "onServerPrefetch": true,
     "onUnmounted": true,
     "onUpdated": true,
+    "onWatcherCleanup": true,
     "provide": true,
     "reactive": true,
     "readonly": true,
@@ -58,19 +64,13 @@
     "useAttrs": true,
     "useCssModule": true,
     "useCssVars": true,
+    "useId": true,
+    "useModel": true,
     "useSlots": true,
+    "useTemplateRef": true,
     "watch": true,
     "watchEffect": true,
     "watchPostEffect": true,
-    "watchSyncEffect": true,
-    "DirectiveBinding": true,
-    "MaybeRef": true,
-    "MaybeRefOrGetter": true,
-    "onWatcherCleanup": true,
-    "useId": true,
-    "useModel": true,
-    "useTemplateRef": true,
-    "Slot": true,
-    "Slots": true
+    "watchSyncEffect": true
   }
 }

+ 1 - 1
playground/vue/package.json

@@ -9,7 +9,7 @@
     "preview": "vite preview"
   },
   "dependencies": {
-    "@tresjs/cientos": "https://pkg.pr.new/@tresjs/cientos@4fe342a",
+    "@tresjs/cientos": "https://pkg.pr.new/@tresjs/cientos@563",
     "@tresjs/core": "workspace:^",
     "@tresjs/leches": "https://pkg.pr.new/@tresjs/leches@b34e795",
     "vue-router": "^4.5.0"

+ 5 - 16
playground/vue/src/pages/events/index.vue

@@ -1,7 +1,7 @@
 <!-- eslint-disable no-console -->
 <script setup lang="ts">
 import type { ThreeEvent } from '@tresjs/core'
-import { OrbitControls, StatsGl } from '@tresjs/cientos'
+import { OrbitControls } from '@tresjs/cientos'
 import { TresCanvas } from '@tresjs/core'
 import { TresLeches, useControls } from '@tresjs/leches'
 import { BasicShadowMap, NoToneMapping, SRGBColorSpace } from 'three'
@@ -18,45 +18,37 @@ const gl = {
 
 useControls('fpsgraph')
 
-const { stopPropagation } = useControls({
-  stopPropagation: false,
-})
-
 function onClick(ev: ThreeEvent<MouseEvent>) {
   console.log('click', ev)
-  if (stopPropagation.value) { ev.stopPropagation() }
   ev.object.material.color.set('#008080')
 }
 
 function onDoubleClick(ev: ThreeEvent<MouseEvent>) {
   console.log('double-click', ev)
-  if (stopPropagation.value) { ev.stopPropagation() }
   ev.object.material.color.set('#FFD700')
 }
 
 function onPointerEnter(ev: ThreeEvent<MouseEvent>) {
-  if (stopPropagation.value) { ev.stopPropagation() }
+  console.log('pointer-enter', ev)
   ev.object.material.color.set('#CCFF03')
 }
 
 function onPointerLeave(ev: ThreeEvent<MouseEvent>) {
-  if (stopPropagation.value) { ev.stopPropagation() }
-  /*  ev.object.material.color.set('#efefef') */
+  console.log('pointer-leave', ev)
+  ev.object.material.color.set('#efefef')
 }
 
 function onPointerMove(ev: ThreeEvent<MouseEvent>) {
-  if (stopPropagation.value) { ev.stopPropagation() }
+  // console.log('pointer-move', ev)
 }
 
 function onContextMenu(ev: ThreeEvent<MouseEvent>) {
   console.log('context-menu', ev)
-  if (stopPropagation.value) { ev.stopPropagation() }
   ev.object.material.color.set('#FF4500')
 }
 
 function onPointerMissed(ev: ThreeEvent<MouseEvent>) {
   console.log('pointer-missed', ev)
-  if (stopPropagation.value) { ev.stopPropagation() }
 }
 </script>
 
@@ -66,9 +58,6 @@ function onPointerMissed(ev: ThreeEvent<MouseEvent>) {
     window-size
     v-bind="gl"
   >
-    <Suspense>
-      <StatsGl />
-    </Suspense>
     <TresPerspectiveCamera
       :position="[11, 11, 11]"
       :look-at="[0, 0, 0]"

文件差異過大導致無法顯示
+ 212 - 353
pnpm-lock.yaml


+ 2 - 3
src/composables/index.ts

@@ -4,10 +4,9 @@ export * from './useCamera'
 export * from './useGraph'
 export * from './useLoader'
 export * from './useLoop'
-export * from './useRaycaster'
 export * from './useRenderer/useRendererManager'
-export * from './useRenderLoop'
 
+export * from './useRenderLoop'
 export * from './useTresContextProvider'
-export * from './useTresEventManager'
+
 export { UseLoader }

+ 13 - 13
src/composables/useCamera/index.ts

@@ -3,34 +3,34 @@ import type { TresContext } from '../useTresContextProvider'
 import type { ComputedRef, Ref } from 'vue'
 import { computed, ref, watchEffect } from 'vue'
 import { isCamera, isPerspectiveCamera } from '../../utils/is'
-import type { Camera } from 'three'
+import type { TresCamera } from '../../types'
 
 /**
  * Interface for the return value of the useCamera composable
  */
 export interface UseCameraReturn {
 
-  activeCamera: ComputedRef<Camera | undefined>
+  activeCamera: ComputedRef<TresCamera>
   /**
    * The list of cameras
    */
-  cameras: Ref<Camera[]>
+  cameras: Ref<TresCamera[]>
   /**
    * Register a camera
    * @param camera - The camera to register
    * @param active - Whether to set the camera as active
    */
-  registerCamera: (camera: Camera, active?: boolean) => void
+  registerCamera: (camera: TresCamera, active?: boolean) => void
   /**
    * Deregister a camera
    * @param camera - The camera to deregister
    */
-  deregisterCamera: (camera: Camera) => void
+  deregisterCamera: (camera: TresCamera) => void
   /**
    * Set the active camera
    * @param cameraOrUuid - The camera or its UUID to set as active
    */
-  setActiveCamera: (cameraOrUuid: string | Camera) => void
+  setActiveCamera: (cameraOrUuid: string | TresCamera) => void
 }
 
 /**
@@ -47,17 +47,17 @@ interface UseCameraParams {
  * @returns The camera management functions and state
  */
 export const useCameraManager = ({ sizes }: UseCameraParams): UseCameraReturn => {
-  const cameras = ref<Camera[]>([])
-  const activeCamera = computed<Camera | undefined>(() => cameras.value[0]) // the first camera is used to make sure there is always one camera active
+  const cameras = ref<TresCamera[]>([])
+  const activeCamera = computed<TresCamera>(() => cameras.value[0]) // the first camera is used to make sure there is always one camera active
 
   /**
    * Set the active camera
    * @param cameraOrUuid - The camera or its UUID to set as active
    */
-  const setActiveCamera = (cameraOrUuid: string | Camera) => {
+  const setActiveCamera = (cameraOrUuid: string | TresCamera) => {
     const camera = isCamera(cameraOrUuid)
       ? cameraOrUuid
-      : cameras.value.find((camera: Camera) => camera.uuid === cameraOrUuid)
+      : cameras.value.find((camera: TresCamera) => camera.uuid === cameraOrUuid)
 
     if (!camera) { return }
 
@@ -70,7 +70,7 @@ export const useCameraManager = ({ sizes }: UseCameraParams): UseCameraReturn =>
    * @param camera - The camera to register
    * @param active - Whether to set the camera as active
    */
-  const registerCamera = (camera: Camera, active = false): void => {
+  const registerCamera = (camera: TresCamera, active = false): void => {
     if (cameras.value.some(({ uuid }) => uuid === camera.uuid)) { return }
     cameras.value.push(camera)
 
@@ -83,7 +83,7 @@ export const useCameraManager = ({ sizes }: UseCameraParams): UseCameraReturn =>
    * Deregister a camera
    * @param camera - The camera to deregister
    */
-  const deregisterCamera = (camera: Camera): void => {
+  const deregisterCamera = (camera: TresCamera): void => {
     cameras.value = cameras.value.filter(({ uuid }) => uuid !== camera.uuid)
   }
 
@@ -92,7 +92,7 @@ export const useCameraManager = ({ sizes }: UseCameraParams): UseCameraReturn =>
    */
   watchEffect(() => {
     if (sizes.aspectRatio.value) {
-      cameras.value.forEach((camera: Camera) => {
+      cameras.value.forEach((camera: TresCamera) => {
         if (isPerspectiveCamera(camera)) {
           camera.aspect = sizes.aspectRatio.value
           camera.updateProjectionMatrix()

+ 73 - 0
src/composables/useEventManager/index.ts

@@ -0,0 +1,73 @@
+import type { PointerEventsMap } from '@pmndrs/pointer-events'
+import { forwardHtmlEvents, getVoidObject } from '@pmndrs/pointer-events'
+import type { TresCamera, TresObject, TresScene } from '../../types'
+import { toValue } from 'vue'
+import type { ComputedRef, MaybeRef } from 'vue'
+import type { Object3D, Object3DEventMap } from 'three'
+
+export const supportedPointerEvents = [
+  'onClick',
+  'onContextMenu',
+  'onPointerMove',
+  'onPointerEnter',
+  'onPointerLeave',
+  'onPointerOver',
+  'onPointerOut',
+  'onDoubleClick',
+  'onPointerDown',
+  'onPointerUp',
+  'onPointerCancel',
+  'onPointerMissed',
+  'onLostPointerCapture',
+  'onWheel',
+]
+
+export const pointerEventsMap = {
+  onClick: 'click',
+  onContextMenu: 'contextmenu',
+  onPointerMove: 'pointermove',
+  onPointerEnter: 'pointerenter',
+  onPointerLeave: 'pointerleave',
+  onPointerOver: 'pointerover',
+  onPointerOut: 'pointerout',
+  onDoubleClick: 'dblclick',
+  onPointerDown: 'pointerdown',
+  onPointerUp: 'pointerup',
+  onPointerCancel: 'pointercancel',
+  onPointerMissed: 'pointermissed',
+  onLostPointerCapture: 'lostpointercapture',
+  onWheel: 'wheel',
+} as const
+
+export function useEventManager({
+  scene,
+  canvas,
+  camera,
+}: {
+  scene: TresScene
+  canvas: MaybeRef<HTMLCanvasElement>
+  camera: ComputedRef<TresCamera>
+}) {
+  const { update } = forwardHtmlEvents(toValue(canvas), () => toValue(camera), scene)
+  const voidObject = getVoidObject(scene) as Object3D<Object3DEventMap & PointerEventsMap>
+
+  function addEventListener(object: TresObject) {
+    Object.entries(object.__tres?.handlers ?? {}).forEach(([event, handler]) => {
+      object.addEventListener(pointerEventsMap[event as keyof typeof pointerEventsMap], handler)
+      if (event === 'onPointerMissed') {
+        voidObject.addEventListener('click', handler)
+      }
+    })
+  }
+
+  function deregisterObject(object: TresObject) {
+    Object.entries(object.__tres?.handlers ?? {}).forEach(([event, handler]) => {
+      object.removeEventListener(pointerEventsMap[event as keyof typeof pointerEventsMap], handler)
+    })
+  }
+  return {
+    addEventListener,
+    deregisterObject,
+    update,
+  }
+}

+ 0 - 212
src/composables/useRaycaster/index.ts

@@ -1,212 +0,0 @@
-import { createEventHook, useElementBounding, usePointer } from '@vueuse/core'
-import { Vector2, Vector3 } from 'three'
-import { computed, onUnmounted, shallowRef } from 'vue'
-import type { EventHook } from '@vueuse/core'
-import type { Intersection, Object3D, Object3DEventMap } from 'three'
-import type { ShallowRef } from 'vue'
-import type { DomEvent, TresEvent, TresInstance } from '../../types'
-import type { TresContext } from '../useTresContextProvider'
-
-export const useRaycaster = (
-  objectsWithEvents: ShallowRef<TresInstance[]>,
-  ctx: TresContext,
-) => {
-  // having a separate computed makes useElementBounding work
-  const canvas = computed(() => ctx.renderer.instance.value.domElement as HTMLCanvasElement)
-  const intersects: ShallowRef<Intersection[]> = shallowRef([])
-  const { x, y } = usePointer({ target: canvas })
-  let delta = 0
-
-  const { width, height, top, left } = useElementBounding(canvas)
-
-  const getRelativePointerPosition = ({ x, y }: { x: number, y: number }) => {
-    if (!canvas.value) { return }
-
-    return {
-      x: ((x - left.value) / width.value) * 2 - 1,
-      y: -((y - top.value) / height.value) * 2 + 1,
-    }
-  }
-
-  const getIntersectsByRelativePointerPosition = ({ x, y }: { x: number, y: number }) => {
-    if (!ctx.camera.activeCamera.value) { return }
-
-    ctx.raycaster.value.setFromCamera(new Vector2(x, y), ctx.camera.activeCamera.value)
-
-    intersects.value = ctx.raycaster.value.intersectObjects(objectsWithEvents.value as Object3D<Object3DEventMap>[], true)
-    return intersects.value
-  }
-
-  const getIntersects = (event?: DomEvent) => {
-    const pointerPosition = getRelativePointerPosition({
-      x: event?.clientX ?? x.value,
-      y: event?.clientY ?? y.value,
-    })
-    if (!pointerPosition) { return [] }
-
-    return getIntersectsByRelativePointerPosition(pointerPosition) || []
-  }
-
-  const eventHookClick = createEventHook<TresEvent>()
-  const eventHookDblClick = createEventHook<TresEvent>()
-  const eventHookPointerMove = createEventHook<TresEvent>()
-  const eventHookPointerUp = createEventHook<TresEvent>()
-  const eventHookPointerDown = createEventHook<TresEvent>()
-  const eventHookPointerMissed = createEventHook<TresEvent>()
-  const eventHookContextMenu = createEventHook<TresEvent>()
-  const eventHookWheel = createEventHook<TresEvent>()
-
-  /* ({
-    ...DomEvent                   // All the original event data
-    ...Intersection               // All of Three's intersection data - see note 2
-    intersections: Intersection[] // The first intersection of each intersected object
-    object: Object3D              // The object that was actually hit (added to event payload in TresEventManager)
-    eventObject: Object3D         // The object that registered the event (added to event payload in TresEventManager)
-    unprojectedPoint: Vector3     // Camera-unprojected point
-    ray: Ray                      // The ray that was used to strike the object
-    camera: Camera                // The camera that was used in the raycaster
-    sourceEvent: DomEvent         // A reference to the host event
-    delta: number                 // Distance between mouse down and mouse up event in pixels
-  }) => ... */
-
-  // Mouse Event props aren't enumerable, so we can't be simple and use Object.assign or the spread operator
-  // Manually copies the mouse event props into a new object that we can spread in triggerEventHook
-  function copyMouseEventProperties(event: MouseEvent | PointerEvent | WheelEvent) {
-    const mouseEventProperties: any = {}
-
-    for (const property in event) {
-      // Copy all non-function properties
-      if (typeof property !== 'function') { mouseEventProperties[property] = (event as Record<string, any>)[property] }
-    }
-    return mouseEventProperties
-  }
-
-  const triggerEventHook = (eventHook: EventHook, event: PointerEvent | MouseEvent | WheelEvent) => {
-    const eventProperties = copyMouseEventProperties(event)
-    if (!ctx.camera.activeCamera.value) { return }
-    const unprojectedPoint = new Vector3(event?.clientX, event?.clientY, 0).unproject(ctx.camera.activeCamera.value)
-    eventHook.trigger({
-      ...eventProperties,
-      intersections: intersects.value,
-      // The unprojectedPoint is wrong, math needs to be fixed
-      unprojectedPoint,
-      ray: ctx.raycaster?.value.ray,
-      camera: ctx.camera.activeCamera.value,
-      sourceEvent: event,
-      delta,
-      stopPropagating: false,
-    })
-  }
-
-  let previousPointerMoveEvent: PointerEvent | undefined
-  const onPointerMove = (event: PointerEvent) => {
-    // Update the raycast intersects
-    getIntersects(event)
-    triggerEventHook(eventHookPointerMove, event)
-    previousPointerMoveEvent = event
-  }
-
-  const forceUpdate = () => {
-    if (previousPointerMoveEvent) { onPointerMove(previousPointerMoveEvent) }
-  }
-
-  // a click event is fired whenever a pointerdown happened after pointerup on the same object
-  let mouseDownObject: Object3D | undefined
-  let mouseDownPosition: Vector2
-  let mouseUpPosition: Vector2
-
-  const onPointerDown = (event: PointerEvent) => {
-    mouseDownObject = intersects.value[0]?.object
-
-    delta = 0
-    mouseDownPosition = new Vector2(
-      event?.clientX ?? x.value,
-      event?.clientY ?? y.value,
-    )
-
-    triggerEventHook(eventHookPointerDown, event)
-  }
-
-  let previousClickObject: Object3D | undefined
-  let doubleClickConfirmed: boolean = false
-
-  const onPointerUp = (event: MouseEvent) => {
-    if (!(event instanceof PointerEvent)) { return } // prevents triggering twice on mobile devices
-
-    // We missed every object, trigger the pointer missed event
-    if (intersects.value.length === 0) {
-      triggerEventHook(eventHookPointerMissed, event)
-    }
-
-    if (mouseDownObject === intersects.value[0]?.object) {
-      mouseUpPosition = new Vector2(
-        event?.clientX ?? x.value,
-        event?.clientY ?? y.value,
-      )
-
-      // Compute the distance between the mouse down and mouse up events
-      delta = mouseDownPosition?.distanceTo(mouseUpPosition)
-
-      if (event.button === 0) {
-        // Left click
-        triggerEventHook(eventHookClick, event)
-
-        if (previousClickObject === intersects.value[0]?.object) {
-          doubleClickConfirmed = true
-        }
-        else {
-          previousClickObject = intersects.value[0]?.object
-          doubleClickConfirmed = false
-        }
-      }
-      else if (event.button === 2) {
-        // Right click
-        triggerEventHook(eventHookContextMenu, event)
-      }
-    }
-
-    triggerEventHook(eventHookPointerUp, event)
-  }
-
-  const onDoubleClick = (event: MouseEvent) => {
-    if (doubleClickConfirmed) {
-      triggerEventHook(eventHookDblClick, event)
-      previousClickObject = undefined
-      doubleClickConfirmed = false
-    }
-  }
-
-  const onPointerLeave = (event: PointerEvent) => triggerEventHook(eventHookPointerMove, event)
-
-  const onWheel = (event: WheelEvent) => triggerEventHook(eventHookWheel, event)
-
-  canvas.value.addEventListener('pointerup', onPointerUp)
-  canvas.value.addEventListener('pointerdown', onPointerDown)
-  canvas.value.addEventListener('pointermove', onPointerMove)
-  canvas.value.addEventListener('pointerleave', onPointerLeave)
-  canvas.value.addEventListener('dblclick', onDoubleClick)
-  canvas.value.addEventListener('wheel', onWheel)
-
-  onUnmounted(() => {
-    if (!canvas?.value) { return }
-    canvas.value.removeEventListener('pointerup', onPointerUp)
-    canvas.value.removeEventListener('pointerdown', onPointerDown)
-    canvas.value.removeEventListener('pointermove', onPointerMove)
-    canvas.value.removeEventListener('pointerleave', onPointerLeave)
-    canvas.value.removeEventListener('dblclick', onDoubleClick)
-    canvas.value.removeEventListener('wheel', onWheel)
-  })
-
-  return {
-    intersects,
-    onClick: (fn: (value: TresEvent) => void) => eventHookClick.on(fn).off,
-    onDblClick: (fn: (value: TresEvent) => void) => eventHookDblClick.on(fn).off,
-    onContextMenu: (fn: (value: TresEvent) => void) => eventHookContextMenu.on(fn).off,
-    onPointerMove: (fn: (value: TresEvent) => void) => eventHookPointerMove.on(fn).off,
-    onPointerUp: (fn: (value: TresEvent) => void) => eventHookPointerUp.on(fn).off,
-    onPointerDown: (fn: (value: TresEvent) => void) => eventHookPointerDown.on(fn).off,
-    onPointerMissed: (fn: (value: TresEvent) => void) => eventHookPointerMissed.on(fn).off,
-    onWheel: (fn: (value: TresEvent) => void) => eventHookWheel.on(fn).off,
-    forceUpdate,
-  }
-}

+ 4 - 2
src/composables/useRenderer/useRendererManager.ts

@@ -111,13 +111,13 @@ export function useRendererManager(
     scene,
     canvas,
     options,
-    contextParts: { sizes, loop, camera },
+    contextParts: { sizes, loop, camera, events },
   }:
   {
     scene: Scene
     canvas: MaybeRef<HTMLCanvasElement>
     options: UseRendererOptions
-    contextParts: Pick<TresContext, 'sizes' | 'camera' | 'loop'>
+    contextParts: Pick<TresContext, 'sizes' | 'camera' | 'loop' | 'events'>
   },
 ) {
   const webGLRendererConstructorParameters = computed<WebGLRendererParameters>(() => ({
@@ -182,6 +182,8 @@ export function useRendererManager(
       onRender.trigger(instance.value)
     }
 
+    events.update()
+
     amountOfFramesToRender.value = isModeAlways.value
       ? 1
       : Math.max(0, amountOfFramesToRender.value - 1)

+ 10 - 12
src/composables/useTresContextProvider/index.ts

@@ -14,7 +14,7 @@ import type { UseCameraReturn } from '../useCamera/'
 import { useCameraManager } from '../useCamera'
 import { useRendererManager } from '../useRenderer/useRendererManager'
 import useSizes, { type SizesType } from '../useSizes'
-import { type TresEventManager, useTresEventManager } from '../useTresEventManager'
+import { useEventManager } from '../useEventManager'
 
 export interface TresContext {
   uuid: string
@@ -27,14 +27,7 @@ export interface TresContext {
   raycaster: ShallowRef<Raycaster>
   // Loop
   loop: RendererLoop
-  eventManager?: TresEventManager
-  // Events
-  // Temporaly add the methods to the context, this should be handled later by the EventManager state on the context https://github.com/Tresjs/tres/issues/515
-  // When thats done maybe we can short the names of the methods since the parent will give the context.
-  registerObjectAtPointerEventHandler?: (object: TresObject) => void
-  deregisterObjectAtPointerEventHandler?: (object: TresObject) => void
-  registerBlockingObjectAtPointerEventHandler?: (object: TresObject) => void
-  deregisterBlockingObjectAtPointerEventHandler?: (object: TresObject) => void
+  events: ReturnType<typeof useEventManager>
 }
 
 export function useTresContextProvider({
@@ -55,12 +48,18 @@ export function useTresContextProvider({
 
   const loop = createRenderLoop()
 
+  const events = useEventManager({
+    scene,
+    canvas,
+    camera: camera.activeCamera,
+  })
+
   const renderer = useRendererManager(
     {
       scene,
       canvas,
       options: rendererOptions,
-      contextParts: { sizes, camera, loop },
+      contextParts: { sizes, camera, loop, events },
     },
   )
 
@@ -74,6 +73,7 @@ export function useTresContextProvider({
     controls: ref(null),
     extend,
     loop,
+    events,
   }
 
   provide('useTres', ctx)
@@ -93,8 +93,6 @@ export function useTresContextProvider({
     immediate: true,
   })
 
-  useTresEventManager(scene, ctx)
-
   onUnmounted(() => {
     ctx.loop.stop()
   })

+ 0 - 237
src/composables/useTresEventManager/index.ts

@@ -1,237 +0,0 @@
-import type { Intersection, PointerEventType, TresEvent, TresInstance, TresObject, TresPointerEvent } from 'src/types'
-import type { Object3D, Object3DEventMap, Scene } from 'three'
-import type { TresContext } from '../useTresContextProvider'
-import { shallowRef } from 'vue'
-import { hyphenate } from '../../utils'
-import { useRaycaster } from '../useRaycaster'
-import { isObject3D, isTresObject } from '../../utils/is'
-import type { EventHookOff } from '@vueuse/core'
-import { createEventHook } from '@vueuse/core'
-
-export interface TresEventManager {
-  onEvent: EventHookOff<TresPointerEvent>
-  /**
-   * Forces the event system to refire events with the previous mouse event
-   */
-  forceUpdate: () => void
-  /**
-   * pointer-missed events by definition are fired when the pointer missed every object in the scene
-   * So we need to track them separately
-   * Note: These are used in nodeOps
-   */
-  registerObject: (object: unknown) => void
-  deregisterObject: (object: unknown) => void
-  registerPointerMissedObject: (object: unknown) => void
-  deregisterPointerMissedObject: (object: unknown) => void
-}
-function executeEventListeners(
-  listeners: (event: TresEvent) => void | ((event: TresEvent) => void)[],
-  event: TresEvent,
-) {
-  // Components with multiple event listeners will have an array of functions
-  if (Array.isArray(listeners)) {
-    for (const listener of listeners) {
-      listener(event)
-    }
-  }
-
-  // Single listener will be a function
-  if (typeof listeners === 'function') {
-    listeners(event)
-  }
-}
-
-export function useTresEventManager(
-  scene: Scene,
-  context: TresContext,
-) {
-  const _scene = shallowRef<Scene>()
-  const _context = shallowRef<TresContext>()
-
-  if (scene) { _scene.value = scene }
-  if (context) { _context.value = context }
-
-  const hasEvents = (object: TresInstance) => object.__tres?.eventCount > 0
-  const hasChildrenWithEvents = (object: TresInstance) => object.children?.some((child: TresInstance) => hasChildrenWithEvents(child)) || hasEvents(object)
-  // TODO: Optimize to not hit test on the whole scene
-  const objectsWithEvents = shallowRef((_scene.value?.children as TresInstance[]).filter(hasChildrenWithEvents) || [])
-
-  const eventHook = createEventHook<TresPointerEvent>()
-
-  /**
-   * propogateEvent
-   *
-   * Propogates an event to all intersected objects and their parents
-   * @param eventName - The name of the event to propogate
-   * @param event - The event object to propogate
-   */
-  function propogateEvent(eventName: string, event: TresEvent) {
-    // Array of objects we've already propogated to
-    const duplicates = []
-
-    // Flag that is set to true when the stopProgatingFn is called
-    const stopPropagatingFn = () => (event.stopPropagating = true)
-    event.stopPropagation = stopPropagatingFn
-
-    // Loop through all intersected objects and call their event handler
-    for (const intersection of event?.intersections) {
-      if (event.stopPropagating) { return }
-
-      // Add intersection data to event object
-      event = { ...event, ...intersection }
-
-      const { object } = intersection
-      event.eventObject = object as TresObject
-      executeEventListeners((object as Record<string, any>)[eventName], event)
-      duplicates.push(object)
-
-      // Propogate the event up the parent chain before moving on to the next intersected object
-      let parentObj = object.parent
-      while (parentObj !== null && !event.stopPropagating) {
-        // We've already been here, break the loop
-        if (duplicates.includes(parentObj)) {
-          break
-        }
-
-        // Sets eventObject to object that registered the event listener
-        event.eventObject = parentObj as TresObject
-        executeEventListeners((parentObj as Record<string, any>)[eventName], event)
-        duplicates.push(parentObj)
-        parentObj = parentObj.parent
-      }
-
-      // Convert eventName to kebab case and emit event from TresCanvas
-      const kebabEventName = hyphenate(eventName.slice(2)) as PointerEventType
-
-      eventHook.trigger({ type: kebabEventName, event, intersection })
-    }
-  }
-
-  const {
-    onClick,
-    onDblClick,
-    onContextMenu,
-    onPointerMove,
-    onPointerDown,
-    onPointerUp,
-    onPointerMissed,
-    onWheel,
-    forceUpdate,
-  } = useRaycaster(objectsWithEvents, context)
-
-  onPointerUp(event => propogateEvent('onPointerUp', event))
-  onPointerDown(event => propogateEvent('onPointerDown', event))
-  onClick(event => propogateEvent('onClick', event))
-  onDblClick(event => propogateEvent('onDoubleClick', event))
-  onContextMenu(event => propogateEvent('onContextMenu', event))
-  onWheel(event => propogateEvent('onWheel', event))
-
-  let prevIntersections: Intersection[] = []
-
-  onPointerMove((event) => {
-    // Current intersections mapped as meshes
-    const hits = event.intersections.map(({ object }) => object)
-
-    // Keep Backup of new intersections incase we overwrite due to a pointer out or leave event
-    const newIntersections = event.intersections as unknown as Intersection[]
-
-    // Previously intersected mesh is no longer intersected, fire onPointerLeave
-    prevIntersections.forEach(({ object: hit }) => {
-      if (
-        !hits.includes(hit as unknown as Object3D<Object3DEventMap>)
-      ) {
-        event.intersections = prevIntersections
-        propogateEvent('onPointerLeave', event)
-        propogateEvent('onPointerOut', event)
-      }
-    })
-
-    // Reset intersections to newIntersections
-    event.intersections = newIntersections
-
-    // Newly intersected mesh is not in the previous intersections, fire onPointerEnter
-    event.intersections.forEach(({ object: hit }) => {
-      if (!prevIntersections.includes(hit as unknown as Intersection)) {
-        propogateEvent('onPointerEnter', event)
-        propogateEvent('onPointerOver', event)
-      }
-    })
-
-    // Fire onPointerMove for all intersected objects
-    propogateEvent('onPointerMove', event)
-
-    // Update previous intersections
-    prevIntersections = event.intersections as unknown as Intersection[]
-  })
-
-  /**
-   * We need to track pointer missed objects separately
-   * since they will not be a part of the raycaster intersection
-   */
-  const pointerMissedObjects: TresObject[] = []
-  onPointerMissed((event: TresEvent) => {
-    // Flag that is set to true when the stopProgatingFn is called
-    const stopPropagatingFn = () => (event.stopPropagating = true)
-    event.stopPropagation = stopPropagatingFn
-
-    pointerMissedObjects.forEach((object: TresObject) => {
-      if (event.stopPropagating) { return }
-
-      // Set eventObject to object that registered the event
-      event.eventObject = object
-
-      executeEventListeners(object.onPointerMissed, event)
-    })
-
-    eventHook.trigger({ type: 'pointer-missed', event })
-  })
-
-  function registerObject(maybeTresObject: unknown) {
-    if (isTresObject(maybeTresObject) && isObject3D(maybeTresObject)) {
-      objectsWithEvents.value.push(maybeTresObject as TresInstance)
-    }
-  }
-
-  function deregisterObject(maybeTresObject: unknown) {
-    if (isTresObject(maybeTresObject) && isObject3D(maybeTresObject)) {
-      const index = objectsWithEvents.value.indexOf(maybeTresObject as TresInstance)
-      if (index > -1) {
-        objectsWithEvents.value.splice(index, 1)
-      }
-    }
-  }
-
-  function registerPointerMissedObject(maybeTresObject: unknown) {
-    if (isTresObject(maybeTresObject) && isObject3D(maybeTresObject) && maybeTresObject.onPointerMissed) {
-      pointerMissedObjects.push(maybeTresObject)
-    }
-  }
-
-  function deregisterPointerMissedObject(maybeTresObject: unknown) {
-    if (isTresObject(maybeTresObject) && isObject3D(maybeTresObject)) {
-      const index = pointerMissedObjects.indexOf(maybeTresObject)
-      if (index > -1) {
-        pointerMissedObjects.splice(index, 1)
-      }
-    }
-  }
-
-  // Attach methods to tres context
-  context.eventManager = {
-    onEvent: eventHook.on,
-    forceUpdate,
-    registerObject,
-    deregisterObject,
-    registerPointerMissedObject,
-    deregisterPointerMissedObject,
-  }
-
-  return {
-    onEvent: eventHook.on,
-    forceUpdate,
-    registerObject,
-    deregisterObject,
-    registerPointerMissedObject,
-    deregisterPointerMissedObject,
-  }
-}

+ 5 - 20
src/core/nodeOps.ts

@@ -7,23 +7,7 @@ import { logError } from '../utils/logger'
 import { isArray, isCamera, isClassInstance, isColor, isColorRepresentation, isCopyable, isFunction, isLayers, isObject, isObject3D, isScene, isTresInstance, isUndefined, isVectorLike } from '../utils/is'
 import { createRetargetingProxy } from '../utils/primitive/createRetargetingProxy'
 import { catalogue } from './catalogue'
-
-const supportedPointerEvents = [
-  'onClick',
-  'onContextMenu',
-  'onPointerMove',
-  'onPointerEnter',
-  'onPointerLeave',
-  'onPointerOver',
-  'onPointerOut',
-  'onDoubleClick',
-  'onPointerDown',
-  'onPointerUp',
-  'onPointerCancel',
-  'onPointerMissed',
-  'onLostPointerCapture',
-  'onWheel',
-]
+import { supportedPointerEvents } from '../composables/useEventManager'
 
 export const nodeOps: (context: TresContext) => RendererOptions<TresObject, TresObject | null> = (context) => {
   const scene = context.scene.value
@@ -112,14 +96,14 @@ export const nodeOps: (context: TresContext) => RendererOptions<TresObject, Tres
     parent = unboxTresPrimitive(parentInstance)
 
     if (child.__tres && child.__tres?.eventCount > 0) {
-      context.eventManager?.registerObject(child)
+      context.events?.addEventListener(child)
     }
 
     if (isCamera(child)) {
       context.camera?.registerCamera(child)
     }
     // NOTE: Track onPointerMissed objects separate from the scene
-    context.eventManager?.registerPointerMissedObject(child)
+    // context.events?.registerPointerMissedObject(child)
 
     if (childInstance.__tres.attach) {
       attach(parentInstance, childInstance, childInstance.__tres.attach)
@@ -151,7 +135,7 @@ export const nodeOps: (context: TresContext) => RendererOptions<TresObject, Tres
 
     // Remove from event manager if necessary
     if (node?.__tres && node.__tres?.eventCount > 0) {
-      context.eventManager?.deregisterObject(node)
+      context.events?.deregisterObject(node)
     }
 
     // NOTE: Derive `dispose` value for this `remove` call and
@@ -276,6 +260,7 @@ export const nodeOps: (context: TresContext) => RendererOptions<TresObject, Tres
     // Has events
     if (supportedPointerEvents.includes(prop) && node.__tres) {
       node.__tres.eventCount += 1
+      node.__tres.handlers[prop] = nextValue
     }
     let finalKey = kebabToCamel(key)
     let target = root?.[finalKey] as Record<string, unknown>

+ 1 - 1
src/types/index.ts

@@ -53,7 +53,7 @@ export interface LocalState {
   type: string
   eventCount: number
   root: TresContext
-  handlers: Partial<EventHandlers>
+  handlers: Record<PointerEventType, EventListener>
   memoizedProps: { [key: string]: any }
   // NOTE:
   // LocalState holds information about the parent/child relationship

部分文件因文件數量過多而無法顯示