|
@@ -1,7 +1,7 @@
|
|
-import { Vector2 } from 'three'
|
|
|
|
|
|
+import { Vector2, Vector3 } from 'three'
|
|
import type { Intersection, Object3D, Object3DEventMap } from 'three'
|
|
import type { Intersection, Object3D, Object3DEventMap } from 'three'
|
|
-import type { Ref } from 'vue'
|
|
|
|
-import { computed, onUnmounted } from 'vue'
|
|
|
|
|
|
+import type { Ref, ShallowRef } from 'vue'
|
|
|
|
+import { computed, onUnmounted, shallowRef } from 'vue'
|
|
import type { EventHook } from '@vueuse/core'
|
|
import type { EventHook } from '@vueuse/core'
|
|
import { createEventHook, useElementBounding, usePointer } from '@vueuse/core'
|
|
import { createEventHook, useElementBounding, usePointer } from '@vueuse/core'
|
|
|
|
|
|
@@ -18,14 +18,20 @@ interface PointerClickEventPayload {
|
|
event: PointerEvent
|
|
event: PointerEvent
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+interface WheelEventPayload {
|
|
|
|
+ intersects: Intersects
|
|
|
|
+ event: WheelEvent
|
|
|
|
+}
|
|
|
|
+
|
|
export const useRaycaster = (
|
|
export const useRaycaster = (
|
|
objects: Ref<Object3D[]>,
|
|
objects: Ref<Object3D[]>,
|
|
ctx: TresContext,
|
|
ctx: TresContext,
|
|
) => {
|
|
) => {
|
|
// having a separate computed makes useElementBounding work
|
|
// having a separate computed makes useElementBounding work
|
|
const canvas = computed(() => ctx.renderer.value.domElement as HTMLCanvasElement)
|
|
const canvas = computed(() => ctx.renderer.value.domElement as HTMLCanvasElement)
|
|
-
|
|
|
|
|
|
+ const intersects: ShallowRef<Intersects[]> = shallowRef([])
|
|
const { x, y } = usePointer({ target: canvas })
|
|
const { x, y } = usePointer({ target: canvas })
|
|
|
|
+ let delta = 0
|
|
|
|
|
|
const { width, height, top, left } = useElementBounding(canvas)
|
|
const { width, height, top, left } = useElementBounding(canvas)
|
|
|
|
|
|
@@ -43,10 +49,11 @@ export const useRaycaster = (
|
|
|
|
|
|
ctx.raycaster.value.setFromCamera(new Vector2(x, y), ctx.camera.value)
|
|
ctx.raycaster.value.setFromCamera(new Vector2(x, y), ctx.camera.value)
|
|
|
|
|
|
- return ctx.raycaster.value.intersectObjects(objects.value, false)
|
|
|
|
|
|
+ intersects.value = ctx.raycaster.value.intersectObjects(objects.value, true)
|
|
|
|
+ return intersects.value
|
|
}
|
|
}
|
|
|
|
|
|
- const getIntersects = (event?: PointerEvent | MouseEvent) => {
|
|
|
|
|
|
+ const getIntersects = (event?: PointerEvent | MouseEvent | WheelEvent) => {
|
|
const pointerPosition = getRelativePointerPosition({
|
|
const pointerPosition = getRelativePointerPosition({
|
|
x: event?.clientX ?? x.value,
|
|
x: event?.clientX ?? x.value,
|
|
y: event?.clientY ?? y.value,
|
|
y: event?.clientY ?? y.value,
|
|
@@ -56,39 +63,144 @@ export const useRaycaster = (
|
|
return getIntersectsByRelativePointerPosition(pointerPosition) || []
|
|
return getIntersectsByRelativePointerPosition(pointerPosition) || []
|
|
}
|
|
}
|
|
|
|
|
|
- const intersects = computed<Intersects>(() => getIntersects())
|
|
|
|
-
|
|
|
|
const eventHookClick = createEventHook<PointerClickEventPayload>()
|
|
const eventHookClick = createEventHook<PointerClickEventPayload>()
|
|
|
|
+ const eventHookDblClick = createEventHook<PointerClickEventPayload>()
|
|
const eventHookPointerMove = createEventHook<PointerMoveEventPayload>()
|
|
const eventHookPointerMove = createEventHook<PointerMoveEventPayload>()
|
|
|
|
+ const eventHookPointerUp = createEventHook<PointerMoveEventPayload>()
|
|
|
|
+ const eventHookPointerDown = createEventHook<PointerMoveEventPayload>()
|
|
|
|
+ const eventHookPointerMissed = createEventHook<PointerClickEventPayload>()
|
|
|
|
+ const eventHookContextMenu = createEventHook<PointerClickEventPayload>()
|
|
|
|
+ const eventHookWheel = createEventHook<WheelEventPayload>()
|
|
|
|
+
|
|
|
|
+ /* ({
|
|
|
|
+ ...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[property] }
|
|
|
|
+ }
|
|
|
|
+ return mouseEventProperties
|
|
|
|
+ }
|
|
|
|
|
|
- const triggerEventHook = (eventHook: EventHook, event: PointerEvent | MouseEvent) => {
|
|
|
|
- eventHook.trigger({ event, intersects: getIntersects(event) })
|
|
|
|
|
|
+ const triggerEventHook = (eventHook: EventHook, event: PointerEvent | MouseEvent | WheelEvent) => {
|
|
|
|
+ const eventProperties = copyMouseEventProperties(event)
|
|
|
|
+
|
|
|
|
+ eventHook.trigger({
|
|
|
|
+ ...eventProperties,
|
|
|
|
+ intersections: intersects.value,
|
|
|
|
+ // The unprojectedPoint is wrong, math needs to be fixed
|
|
|
|
+ unprojectedPoint: new Vector3(event?.clientX, event?.clientY, 0).unproject(ctx.camera?.value),
|
|
|
|
+ ray: ctx.raycaster?.value.ray,
|
|
|
|
+ camera: ctx.camera?.value,
|
|
|
|
+ sourceEvent: event,
|
|
|
|
+ delta,
|
|
|
|
+ stopPropagating: false,
|
|
|
|
+ })
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ let previousPointerMoveEvent: PointerEvent | undefined
|
|
const onPointerMove = (event: PointerEvent) => {
|
|
const onPointerMove = (event: PointerEvent) => {
|
|
|
|
+ // Update the raycast intersects
|
|
|
|
+ getIntersects(event)
|
|
triggerEventHook(eventHookPointerMove, event)
|
|
triggerEventHook(eventHookPointerMove, event)
|
|
|
|
+ previousPointerMoveEvent = event
|
|
}
|
|
}
|
|
|
|
|
|
- // a click event is fired whenever a pointerdown happened after pointerup on the same object
|
|
|
|
|
|
+ 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 mouseDownObject: Object3D | undefined
|
|
|
|
+ let mouseDownPosition
|
|
|
|
+ let mouseUpPosition
|
|
|
|
|
|
const onPointerDown = (event: PointerEvent) => {
|
|
const onPointerDown = (event: PointerEvent) => {
|
|
- mouseDownObject = getIntersects(event)[0]?.object
|
|
|
|
|
|
+ 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) => {
|
|
const onPointerUp = (event: MouseEvent) => {
|
|
if (!(event instanceof PointerEvent)) { return } // prevents triggering twice on mobile devices
|
|
if (!(event instanceof PointerEvent)) { return } // prevents triggering twice on mobile devices
|
|
|
|
|
|
- if (mouseDownObject === getIntersects(event)[0]?.object) { triggerEventHook(eventHookClick, event) }
|
|
|
|
|
|
+ // 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 onPointerLeave = (event: PointerEvent) => eventHookPointerMove.trigger({ event, intersects: [] })
|
|
|
|
|
|
+ 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('pointerup', onPointerUp)
|
|
canvas.value.addEventListener('pointerdown', onPointerDown)
|
|
canvas.value.addEventListener('pointerdown', onPointerDown)
|
|
canvas.value.addEventListener('pointermove', onPointerMove)
|
|
canvas.value.addEventListener('pointermove', onPointerMove)
|
|
canvas.value.addEventListener('pointerleave', onPointerLeave)
|
|
canvas.value.addEventListener('pointerleave', onPointerLeave)
|
|
|
|
+ canvas.value.addEventListener('dblclick', onDoubleClick)
|
|
|
|
+ canvas.value.addEventListener('wheel', onWheel)
|
|
|
|
|
|
onUnmounted(() => {
|
|
onUnmounted(() => {
|
|
if (!canvas?.value) { return }
|
|
if (!canvas?.value) { return }
|
|
@@ -96,11 +208,20 @@ export const useRaycaster = (
|
|
canvas.value.removeEventListener('pointerdown', onPointerDown)
|
|
canvas.value.removeEventListener('pointerdown', onPointerDown)
|
|
canvas.value.removeEventListener('pointermove', onPointerMove)
|
|
canvas.value.removeEventListener('pointermove', onPointerMove)
|
|
canvas.value.removeEventListener('pointerleave', onPointerLeave)
|
|
canvas.value.removeEventListener('pointerleave', onPointerLeave)
|
|
|
|
+ canvas.value.removeEventListener('dblclick', onDoubleClick)
|
|
|
|
+ canvas.value.removeEventListener('wheel', onWheel)
|
|
})
|
|
})
|
|
|
|
|
|
return {
|
|
return {
|
|
intersects,
|
|
intersects,
|
|
onClick: (fn: (value: PointerClickEventPayload) => void) => eventHookClick.on(fn).off,
|
|
onClick: (fn: (value: PointerClickEventPayload) => void) => eventHookClick.on(fn).off,
|
|
|
|
+ onDblClick: (fn: (value: PointerClickEventPayload) => void) => eventHookDblClick.on(fn).off,
|
|
|
|
+ onContextMenu: (fn: (value: PointerClickEventPayload) => void) => eventHookContextMenu.on(fn).off,
|
|
onPointerMove: (fn: (value: PointerMoveEventPayload) => void) => eventHookPointerMove.on(fn).off,
|
|
onPointerMove: (fn: (value: PointerMoveEventPayload) => void) => eventHookPointerMove.on(fn).off,
|
|
|
|
+ onPointerUp: (fn: (value: PointerMoveEventPayload) => void) => eventHookPointerUp.on(fn).off,
|
|
|
|
+ onPointerDown: (fn: (value: PointerMoveEventPayload) => void) => eventHookPointerDown.on(fn).off,
|
|
|
|
+ onPointerMissed: (fn: (value: PointerClickEventPayload) => void) => eventHookPointerMissed.on(fn).off,
|
|
|
|
+ onWheel: (fn: (value: WheelEventPayload) => void) => eventHookWheel.on(fn).off,
|
|
|
|
+ forceUpdate,
|
|
}
|
|
}
|
|
}
|
|
}
|