Skip to content

Commit

Permalink
fix: raycaster does not work properly when scene is not in full screen (
Browse files Browse the repository at this point in the history
#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 <[email protected]>
Co-authored-by: alvarosabu <[email protected]>
  • Loading branch information
3 people authored Jun 19, 2023
1 parent f3c0276 commit 20a5b9e
Show file tree
Hide file tree
Showing 14 changed files with 254 additions and 147 deletions.
30 changes: 12 additions & 18 deletions docs/api/events.md
Original file line number Diff line number Diff line change
@@ -1,31 +1,25 @@
# 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" />

## Pointer Events

```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 change: 1 addition & 0 deletions playground/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down
45 changes: 9 additions & 36 deletions src/components/TresScene.ts
Original file line number Diff line number Diff line change
@@ -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/'
Expand All @@ -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
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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)
})
}

Expand Down
1 change: 1 addition & 0 deletions src/composables/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export * from './useTres'
export * from './useRaycaster'
export * from './useLogger'
export * from './useSeek'
export * from './usePointerEventHandler'
83 changes: 83 additions & 0 deletions src/composables/usePointerEventHandler/index.ts
Original file line number Diff line number Diff line change
@@ -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,
}
}
141 changes: 95 additions & 46 deletions src/composables/useRaycaster/index.ts
Original file line number Diff line number Diff line change
@@ -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,
}
}
Loading

0 comments on commit 20a5b9e

Please sign in to comment.