浏览代码

fix: raycaster does not work properly when scene is not in full screen (#304)

* chore: tinkering on possible solutions concerning pointer event handling

* chore: made click listeners work with changed architectures concerning raycaster

* chore: changed callback structure

* chore: made pointer move work

* chore: made other pointer events work

* chore: code cleanup

* chore: added deregistration of pointer event handlers for when an Oject3D is removed

* chore: handled the case when the pointer leaves an Object3D but also the canvas

* chore: replaced useRaycaster

* fix: raycaster works properly when scene does not take up the whole viewport
fix: onPointerMove does not fire too often anymore

* chore: made types in nodeOps a little more specific

* chore: improved click event handling

* docs: adjusted events page

* chore: fixed typo

* chore: cleanup

* chore: adjusted code so tests pass

* chore: merge latest main

---------

Co-authored-by: Tino Koch <tinoooo@users.noreply.github.com>
Co-authored-by: alvarosabu <alvaro.saburido@gmail.com>
Tino Koch 1 年之前
父节点
当前提交
20a5b9eee9

+ 12 - 18
docs/api/events.md

@@ -1,6 +1,6 @@
 # Events
 
-**TresJS** Mesh objects emit pointer events when they are interacted with using `raycaster` and `pointer` objects under the hood.
+**TresJS** components emit pointer events when they are interacted with. This is the case for the components that represent Three.js classes that derive from [THREE.Object3D](https://threejs.org/docs/index.html?q=object#api/en/core/Object3D) (like meshes, groups,...).
 
 <StackBlitzEmbed project-id="tresjs-events" />
 
@@ -8,24 +8,18 @@
 
 ```html
 <TresMesh
-  @click="(ev) => console.log('click', ev)"
-  @pointer-move="(ev) => console.log('click', ev)"
-  @pointer-enter="(ev) => console.log('click', ev)"
-  @pointer-leave="(ev) => console.log('click', ev)"
+  @click="(intersection, pointerEvent) => console.log('click', intersection, pointerEvent)"
+  @pointer-move="(intersection, pointerEvent) => console.log('pointer-move', intersection, pointerEvent)"
+  @pointer-enter="(intersection, pointerEvent) => console.log('pointer-enter', intersection, pointerEvent)"
+  @pointer-leave="(intersection, pointerEvent) => console.log('pointer-leave', pointerEvent)"
 />
 ```
 
-## Event Data
+| Event         | fires when ...                                                                        | Event Handler Parameter Type(s)                                                                                                                                                                       |
+| ------------- | ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| click         | ... the events pointerdown and pointerup fired on the same object one after the other | [Intersection](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/three/src/core/Raycaster.d.ts#L16), [PointerEvent](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent) |
+| pointer-move  | ... the pointer is moving above the object                                            | [Intersection](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/three/src/core/Raycaster.d.ts#L16), [PointerEvent](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent) |
+| pointer-enter | ... the pointer is entering the object                                                | [Intersection](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/three/src/core/Raycaster.d.ts#L16), [PointerEvent](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent) |
+| pointer-leave | ... the pointer is leaves the object                                                  | [PointerEvent](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent)                                                                                                                         |
 
-The event data is a `TresEvent` object that contains the following properties:
-
-```ts
-;({
-  object: Object3D, // The mesh object that emitted the event
-  distance: number, // The distance between the camera and the mesh
-  point: Vector3, // The intersection point between the ray and the mesh
-  uv: Vector2, // The uv coordinates of the intersection point
-  face: Face3, // The face of the mesh that was intersected
-  faceIndex: number, // The index of the face that was intersected
-})
-```
+The returned [Intersection](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/three/src/core/Raycaster.d.ts#L16) includes the [Object3D](https://threejs.org/docs/index.html?q=object#api/en/core/Object3D) that triggered the event. You can access it via `intersection.object`.

+ 1 - 0
playground/components.d.ts

@@ -10,6 +10,7 @@ declare module 'vue' {
     AnimatedModel: typeof import('./src/components/AnimatedModel.vue')['default']
     Cameras: typeof import('./src/components/Cameras.vue')['default']
     DanielTest: typeof import('./src/components/DanielTest.vue')['default']
+    DeleteMe: typeof import('./src/components/DeleteMe.vue')['default']
     FBXModels: typeof import('./src/components/FBXModels.vue')['default']
     Gltf: typeof import('./src/components/gltf/index.vue')['default']
     MeshWobbleMaterial: typeof import('./src/components/meshWobbleMaterial/index.vue')['default']

+ 9 - 36
src/components/TresScene.ts

@@ -1,7 +1,6 @@
-import { App, defineComponent, h, onMounted, onUnmounted, ref, watch, watchEffect, VNode } from 'vue'
+import { App, defineComponent, h, onMounted, onUnmounted, ref, watch, VNode } from 'vue'
 import * as THREE from 'three'
 import { ColorSpace, ShadowMapType, ToneMapping } from 'three'
-import { useEventListener } from '@vueuse/core'
 import { isString } from '@alvarosabu/utils'
 import { createTres } from '../core/renderer'
 import { TresCamera } from '../types/'
@@ -12,11 +11,12 @@ import {
   useCamera,
   useRenderer,
   useRenderLoop,
-  useRaycaster,
   useTres,
+  usePointerEventHandler,
 } from '../composables'
 import { extend } from '../core/catalogue'
 import { type RendererPresetsType } from '../composables/useRenderer/const'
+import { OBJECT_3D_USER_DATA_KEYS } from '../keys'
 
 export interface TresSceneProps {
   shadows?: boolean
@@ -67,12 +67,18 @@ export const TresScene = defineComponent<TresSceneProps>({
 
     const container = ref<HTMLElement>()
     const canvas = ref<HTMLElement>()
+
     const scene = new THREE.Scene()
+
+    const pointerEventHandler = usePointerEventHandler()
     const { setState } = useTres()
 
+    scene.userData[OBJECT_3D_USER_DATA_KEYS.REGISTER_AT_POINTER_EVENT_HANDLER] = pointerEventHandler.registerObject
+
     setState('scene', scene)
     setState('canvas', canvas)
     setState('container', container)
+    setState('pointerEventHandler', pointerEventHandler)
 
     const isCameraAvailable = ref()
 
@@ -105,41 +111,8 @@ export const TresScene = defineComponent<TresSceneProps>({
         pushCamera(props.camera as any)
       }
 
-      const { raycaster, pointer } = useRaycaster()
-
-      // TODO: Type raycasting events correctly
-      let prevInstance: any = null
-      let currentInstance: any = null
-
-      watchEffect(() => {
-        if (activeCamera.value) raycaster.value.setFromCamera(pointer.value, activeCamera.value)
-      })
-
       onLoop(() => {
         if (activeCamera.value && props.disableRender !== true) renderer.value?.render(scene, activeCamera.value)
-
-        if (raycaster.value) {
-          const intersects = raycaster.value.intersectObjects(scene.children)
-
-          if (intersects.length > 0) {
-            currentInstance = intersects[0]
-            if (prevInstance === null) {
-              currentInstance.object?.events?.onPointerEnter?.(currentInstance)
-            }
-            currentInstance.object?.events?.onPointerMove?.(currentInstance)
-          } else {
-            if (prevInstance !== null) {
-              currentInstance?.object?.events?.onPointerLeave?.(prevInstance)
-              currentInstance = null
-            }
-          }
-          prevInstance = currentInstance
-        }
-      })
-
-      useEventListener(canvas.value, 'click', () => {
-        if (currentInstance === null) return
-        currentInstance.object?.events?.onClick?.(currentInstance)
       })
     }
 

+ 1 - 0
src/composables/index.ts

@@ -7,3 +7,4 @@ export * from './useTres'
 export * from './useRaycaster'
 export * from './useLogger'
 export * from './useSeek'
+export * from './usePointerEventHandler'

+ 83 - 0
src/composables/usePointerEventHandler/index.ts

@@ -0,0 +1,83 @@
+import { uniqueBy } from '../../utils'
+import { useRaycaster } from '../useRaycaster'
+import { computed, reactive } from 'vue'
+import type { Intersection, Event, Object3D } from 'three'
+
+type CallbackFn = (intersection: Intersection<Object3D<Event>>, event: PointerEvent) => void //TODO document
+type CallbackFnPointerLeave = (object: Object3D<Event>, event: PointerEvent) => void
+
+type EventProps = {
+  onClick?: CallbackFn
+  onPointerEnter?: CallbackFn
+  onPointerMove?: CallbackFn
+  onPointerLeave?: CallbackFnPointerLeave
+}
+
+export const usePointerEventHandler = () => {
+  const objectsWithEventListeners = reactive({
+    click: new Map<Object3D, CallbackFn>(),
+    pointerMove: new Map<Object3D, CallbackFn>(),
+    pointerEnter: new Map<Object3D, CallbackFn>(),
+    pointerLeave: new Map<Object3D, CallbackFnPointerLeave>(),
+  })
+
+  const deregisterObject = (object: Object3D) => {
+    Object.values(objectsWithEventListeners).forEach(map => map.delete(object))
+  }
+
+  const registerObject = (object: Object3D & EventProps) => {
+    const { onClick, onPointerMove, onPointerEnter, onPointerLeave } = object
+
+    if (onClick) objectsWithEventListeners.click.set(object, onClick)
+    if (onPointerMove) objectsWithEventListeners.pointerMove.set(object, onPointerMove)
+    if (onPointerEnter) objectsWithEventListeners.pointerEnter.set(object, onPointerEnter)
+    if (onPointerLeave) objectsWithEventListeners.pointerLeave.set(object, onPointerLeave)
+
+    object.addEventListener('removed', () => {
+      object.traverse((child: Object3D) => {
+        deregisterObject(child)
+      })
+
+      deregisterObject(object)
+    })
+  }
+
+  const objectsToWatch = computed(() =>
+    uniqueBy(
+      Object.values(objectsWithEventListeners)
+        .map(map => Array.from(map.keys()))
+        .flat(),
+      ({ uuid }) => uuid,
+    ),
+  )
+
+  const { onClick, onPointerMove } = useRaycaster(objectsToWatch)
+
+  onClick(({ intersects, event }) => {
+    if (intersects.length) objectsWithEventListeners.click.get(intersects[0].object)?.(intersects[0], event)
+  })
+
+  let previouslyIntersectedObject: Object3D<Event> | null
+
+  onPointerMove(({ intersects, event }) => {
+    const firstObject = intersects?.[0]?.object
+
+    const { pointerLeave, pointerEnter, pointerMove } = objectsWithEventListeners
+
+    if (previouslyIntersectedObject && previouslyIntersectedObject !== firstObject)
+      pointerLeave.get(previouslyIntersectedObject)?.(previouslyIntersectedObject, event)
+
+    if (firstObject) {
+      if (previouslyIntersectedObject !== firstObject) pointerEnter.get(firstObject)?.(intersects[0], event)
+
+      pointerMove.get(firstObject)?.(intersects[0], event)
+    }
+
+    previouslyIntersectedObject = firstObject || null
+  })
+
+  return {
+    registerObject,
+    deregisterObject,
+  }
+}

+ 95 - 46
src/composables/useRaycaster/index.ts

@@ -1,59 +1,108 @@
 import { useTres } from '../useTres'
-import { Raycaster, Vector2 } from 'three'
-import { onUnmounted, Ref, ref, ShallowRef, shallowRef } from 'vue'
-
-/**
- * Raycaster composable return type
- *
- * @export
- * @interface UseRaycasterReturn
- */
-export interface UseRaycasterReturn {
-  /**
-   * Raycaster instance
-   *
-   * @type {ShallowRef<Raycaster>}
-   * @memberof UseRaycasterReturn
-   */
-  raycaster: ShallowRef<Raycaster>
-  /**
-   * Pointer position
-   *
-   * @type {Ref<Vector2>}
-   * @memberof UseRaycasterReturn
-   */
-  pointer: Ref<Vector2>
+import { Object3D, Raycaster, Vector2 } from 'three'
+import { Ref, computed, onUnmounted, watchEffect } from 'vue'
+import { EventHook, createEventHook, useElementBounding, usePointer } from '@vueuse/core'
+
+export type Intersects = THREE.Intersection<THREE.Object3D<THREE.Event>>[]
+interface PointerMoveEventPayload {
+  intersects?: Intersects
+  event: PointerEvent
+}
+
+interface PointerClickEventPayload {
+  intersects: Intersects
+  event: PointerEvent
 }
 
-/**
- * Composable to provide raycaster support and pointer information
- *
- * @see https://threejs.org/docs/index.html?q=raycas#api/en/core/Raycaster
- * @export
- * @return {*} {UseRaycasterReturn}
- */
-export function useRaycaster(): UseRaycasterReturn {
-  const raycaster = shallowRef(new Raycaster())
-  const pointer = ref(new Vector2())
-  const currentInstance = ref(null)
-  const { setState, state } = useTres()
+export const useRaycaster = (objects: Ref<THREE.Object3D[]>) => {
+  const { state } = useTres()
+
+  const canvas = computed(() => state.canvas?.value) // having a seperate computed makes useElementBounding work
+
+  const { x, y } = usePointer({ target: canvas })
+
+  const { width, height, top, left } = useElementBounding(canvas)
+
+  const raycaster = new Raycaster()
+
+  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 (!state.camera) return
+
+    raycaster.setFromCamera(new Vector2(x, y), state.camera)
+
+    return raycaster.intersectObjects(objects.value, false)
+  }
+
+  const getIntersects = (event?: PointerEvent | MouseEvent) => {
+    const pointerPosition = getRelativePointerPosition({
+      x: event?.clientX ?? x.value,
+      y: event?.clientY ?? y.value,
+    })
+    if (!pointerPosition) return []
+
+    return getIntersectsByRelativePointerPosition(pointerPosition) || []
+  }
+
+  const intersects = computed<Intersects>(() => getIntersects())
+
+  const eventHookClick = createEventHook<PointerClickEventPayload>()
+  const eventHookPointerMove = createEventHook<PointerMoveEventPayload>()
+
+  const triggerEventHook = (eventHook: EventHook, event: PointerEvent | MouseEvent) => {
+    eventHook.trigger({ event, intersects: getIntersects(event) })
+  }
+
+  const onPointerMove = (event: PointerEvent) => {
+    triggerEventHook(eventHookPointerMove, event)
+  }
+
+  // a click event is fired whenever a pointerdown happened after pointerup on the same object
 
-  setState('raycaster', raycaster.value)
-  setState('pointer', pointer)
-  setState('currentInstance', currentInstance)
+  let mouseDownObject: Object3D | undefined = undefined
 
-  function onPointerMove(event: MouseEvent) {
-    pointer.value.x = (event.clientX / window.innerWidth) * 2 - 1
-    pointer.value.y = -(event.clientY / window.innerHeight) * 2 + 1
+  const onPointerDown = (event: PointerEvent) => {
+    mouseDownObject = getIntersects(event)[0]?.object
   }
 
-  state?.renderer?.domElement.addEventListener('pointermove', onPointerMove)
+  const onPointerUp = (event: MouseEvent) => {
+    if (!(event instanceof PointerEvent)) return // prevents triggering twice on mobile devices
+
+    if (mouseDownObject === getIntersects(event)[0]?.object) triggerEventHook(eventHookClick, event)
+  }
+
+  const onPointerLeave = (event: PointerEvent) => eventHookPointerMove.trigger({ event, intersects: [] })
+
+  const unwatch = watchEffect(() => {
+    if (!canvas?.value) return
+
+    canvas.value.addEventListener('pointerup', onPointerUp)
+    canvas.value.addEventListener('pointerdown', onPointerDown)
+    canvas.value.addEventListener('pointermove', onPointerMove)
+    canvas.value.addEventListener('pointerleave', onPointerLeave)
+
+    unwatch()
+  })
 
   onUnmounted(() => {
-    state?.renderer?.domElement.removeEventListener('pointermove', onPointerMove)
+    if (!canvas?.value) return
+    canvas.value.removeEventListener('pointerup', onPointerUp)
+    canvas.value.removeEventListener('pointerdown', onPointerDown)
+    canvas.value.removeEventListener('pointermove', onPointerMove)
+    canvas.value.removeEventListener('pointerleave', onPointerLeave)
   })
+
   return {
-    raycaster,
-    pointer,
+    intersects,
+    onClick: (fn: (value: PointerClickEventPayload) => void) => eventHookClick.on(fn).off,
+    onPointerMove: (fn: (value: PointerMoveEventPayload) => void) => eventHookPointerMove.on(fn).off,
   }
 }

+ 0 - 23
src/composables/useRaycaster/useRaycaster.test.ts

@@ -1,23 +0,0 @@
-import { Raycaster, Vector2 } from 'three'
-/* import { useRaycaster } from '.' */
-import { withSetup } from '../../utils/test-utils'
-
-/* const [composable, app] = withSetup(() => useRaycaster()) */
-
-describe.skip('useRaycaster', () => {
-  /*   afterEach(() => {
-    app.unmount()
-  }) */
-  /*   test.skip('provides raycaster', () => {
-    const { raycaster } = composable
-    expect(raycaster).toBeDefined()
-    expect(raycaster.value).toBeInstanceOf(Raycaster)
-  })
-  test.skip('provides pointer', () => {
-    const { pointer } = composable
-    expect(pointer).toBeDefined()
-    expect(pointer.value).toBeInstanceOf(Vector2)
-  }) */
-})
-
-// TODO: find a way to test this with useTresProvider approach

+ 1 - 1
src/composables/useRenderer/index.ts

@@ -159,7 +159,7 @@ export function useRenderer(options: UseRendererOptions) {
   const aspectRatio = computed(() => width.value / height.value)
 
   setTimeout(() => {
-    if (!toValue(windowSize) && !state.canvas.value.offsetHeight) {
+    if (!toValue(windowSize) && !state.canvas?.value.offsetHeight) {
       logWarning(`Oops... Seems like your canvas height is currently 0px, it's posible that you couldn't watch your scene.
   You could set windowSize=true to force the canvas to be the size of the window.`)
     }

+ 1 - 1
src/composables/useSeek/useSeek.test.ts

@@ -4,7 +4,7 @@ import { withSetup } from '../../utils/test-utils'
 
 const [composable, app] = withSetup(() => useSeek())
 
-describe('useRaycaster', () => {
+describe('useSeek', () => {
   afterEach(() => {
     app.unmount()
   })

+ 16 - 2
src/composables/useTres/index.ts

@@ -1,7 +1,8 @@
 import { Clock, EventDispatcher, Raycaster, Scene, Vector2, WebGLRenderer } from 'three'
 import { generateUUID } from 'three/src/math/MathUtils'
-import { computed, ComputedRef, inject, provide, shallowReactive, toRefs } from 'vue'
+import { ComputedRef, inject, provide, Ref, shallowReactive, toRefs } from 'vue'
 import { Camera } from '../useCamera'
+import type { usePointerEventHandler } from '../usePointerEventHandler'
 
 export interface TresState {
   /**
@@ -88,6 +89,15 @@ export interface TresState {
    * @memberof TresState
    */
   controls?: (EventDispatcher & { enabled: boolean }) | null
+
+  canvas?: Ref<HTMLElement>
+
+  /**
+   * The entity that handles pointer events
+   * @type {ReturnType<typeof usePointerEventHandler>}
+   * @memberof TresState
+   */
+  pointerEventHandler?: ReturnType<typeof usePointerEventHandler>
   [key: string]: any
 }
 
@@ -113,9 +123,11 @@ export function useTresProvider() {
     uuid: generateUUID(),
     camera: undefined,
     cameras: [],
+    canvas: undefined,
     scene: undefined,
     renderer: undefined,
-    aspectRatio: computed(() => window.innerWidth / window.innerHeight),
+    aspectRatio: undefined,
+    pointerEventHandler: undefined,
   })
   /**
    * Get a state value.
@@ -155,8 +167,10 @@ export const useTres = () => {
     state: shallowReactive({
       camera: undefined,
       cameras: [],
+      canvas: undefined,
       scene: undefined,
       renderer: undefined,
+      pointerEventHandler: undefined,
     }),
   })
 

+ 14 - 19
src/core/nodeOps.ts

@@ -1,10 +1,12 @@
 import { RendererOptions } from 'vue'
-import { BufferAttribute, BufferGeometry, Material, Scene } from 'three'
+import { BufferAttribute, Scene } from 'three'
 import { useCamera, useLogger } from '../composables'
 import { isFunction } from '@alvarosabu/utils'
 import { catalogue } from './catalogue'
-import { EventHandlers, TresObject } from '../types'
+import { TresObject } from '../types'
 import { isHTMLTag, kebabToCamel } from '../utils'
+import { OBJECT_3D_USER_DATA_KEYS } from '../keys'
+import type { Material, BufferGeometry, Object3D } from 'three'
 
 const onRE = /^on[^a-z]/
 export const isOn = (key: string) => onRE.test(key)
@@ -15,10 +17,6 @@ function noop(fn: string): any {
 
 let fallback: TresObject | null = null
 let scene: Scene | null = null
-const OBJECT_3D_USER_DATA_KEYS = {
-  GEOMETRY_VIA_PROP: 'tres__geometryViaProp',
-  MATERIAL_VIA_PROP: 'tres__materialViaProp',
-}
 
 const { logError } = useLogger()
 
@@ -70,12 +68,10 @@ export const nodeOps: RendererOptions<TresObject, TresObject> = {
     const { GEOMETRY_VIA_PROP, MATERIAL_VIA_PROP } = OBJECT_3D_USER_DATA_KEYS
 
     if (instance.isObject3D) {
-      if (props?.material?.isMaterial) (instance as TresObject).userData[MATERIAL_VIA_PROP] = true
-      if (props?.geometry?.isBufferGeometry) (instance as TresObject).userData[GEOMETRY_VIA_PROP] = true
+      if (props?.material?.isMaterial) (instance as Object3D).userData[MATERIAL_VIA_PROP] = true
+      if (props?.geometry?.isBufferGeometry) (instance as Object3D).userData[GEOMETRY_VIA_PROP] = true
     }
 
-    instance.events = {}
-
     return instance
   },
   insert(child, parent) {
@@ -96,6 +92,7 @@ export const nodeOps: RendererOptions<TresObject, TresObject> = {
     if (child?.isObject3D && parent?.isObject3D) {
       parent.add(child)
       child.dispatchEvent({ type: 'added' })
+      scene?.userData?.[OBJECT_3D_USER_DATA_KEYS.REGISTER_AT_POINTER_EVENT_HANDLER]?.(child)
     } else if (child?.isFog) {
       parent.fog = child
     } else if (typeof child?.attach === 'string') {
@@ -110,17 +107,19 @@ export const nodeOps: RendererOptions<TresObject, TresObject> = {
     // remove is only called on the node being removed and not on child nodes.
 
     if (node.isObject3D) {
-      const object3D = node as unknown as TresObject
+      const object3D = node as unknown as Object3D
 
-      const disposeMaterialsAndGeometries = (object3D: TresObject) => {
+      const disposeMaterialsAndGeometries = (object3D: Object3D) => {
         const { GEOMETRY_VIA_PROP, MATERIAL_VIA_PROP } = OBJECT_3D_USER_DATA_KEYS
 
-        if (!object3D.userData[MATERIAL_VIA_PROP]) (object3D as TresObject & { material: Material }).material?.dispose()
+        if (!object3D.userData[MATERIAL_VIA_PROP]) (object3D as Object3D & { material: Material }).material?.dispose()
         if (!object3D.userData[GEOMETRY_VIA_PROP])
-          (object3D as TresObject & { geometry: BufferGeometry }).geometry?.dispose()
+          (object3D as Object3D & { geometry: BufferGeometry }).geometry?.dispose()
       }
 
-      object3D.traverse((child: TresObject) => disposeMaterialsAndGeometries(child))
+      object3D.traverse((child: Object3D) => {
+        disposeMaterialsAndGeometries(child)
+      })
 
       disposeMaterialsAndGeometries(object3D)
     }
@@ -153,10 +152,6 @@ export const nodeOps: RendererOptions<TresObject, TresObject> = {
         finalKey = key.toLowerCase()
         if (!target?.set) root = chain.reduce((acc, key) => acc[kebabToCamel(key)], root)
       }
-      if (isOn(key)) {
-        const eventHandlerKey: keyof EventHandlers = key as keyof EventHandlers // This is fine
-        node.events[eventHandlerKey] = nextValue
-      }
       let value = nextValue
       if (value === '') value = true
       // Set prop, prefer atomic methods if applicable

+ 6 - 0
src/keys.ts

@@ -1 +1,7 @@
 export const UseTresStateSymbol = Symbol('UseTresState')
+
+export const OBJECT_3D_USER_DATA_KEYS = {
+  GEOMETRY_VIA_PROP: 'tres__geometryViaProp',
+  MATERIAL_VIA_PROP: 'tres__materialViaProp',
+  REGISTER_AT_POINTER_EVENT_HANDLER: 'tres__registerAtPointerEventHandler',
+}

+ 0 - 1
src/types/index.ts

@@ -30,7 +30,6 @@ export interface InstanceProps<T = any, P = any> {
 
 interface TresBaseObject {
   attach?: string
-  events: EventHandlers
   removeFromParent?: () => void
   dispose?: () => void
   [prop: string]: any // for arbitrary properties

+ 15 - 0
src/utils/index.ts

@@ -38,3 +38,18 @@ export function makeMap(str: string, expectsLowerCase?: boolean): (key: string)
   }
   return expectsLowerCase ? val => !!map[val.toLowerCase()] : val => !!map[val]
 }
+
+export const uniqueBy = <T, K>(array: T[], iteratee: (value: T) => K): T[] => {
+  const seen = new Set<K>()
+  const result: T[] = []
+
+  for (const item of array) {
+    const identifier = iteratee(item)
+    if (!seen.has(identifier)) {
+      seen.add(identifier)
+      result.push(item)
+    }
+  }
+
+  return result
+}