-
-
Notifications
You must be signed in to change notification settings - Fork 108
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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 <[email protected]> Co-authored-by: alvarosabu <[email protected]>
- Loading branch information
1 parent
f3c0276
commit 20a5b9e
Showing
14 changed files
with
254 additions
and
147 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} | ||
} |
Oops, something went wrong.