|
@@ -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,
|
|
|
}
|
|
|
}
|