Skip to content

Commit

Permalink
feat(events)!: pointerevents manager and state (#529)
Browse files Browse the repository at this point in the history
* new file:   playground/src/components/Box.vue
	new file:   playground/src/pages/raycaster/Propogation.vue
	  * Started work on interactive Event Propogation playground example
	modified:   src/components/TresCanvas.vue
	  * Import and use `useEventStore`
	  * defineEmits for all expected pointer events so we may emit propogated events off of the canvasa
	modified:   src/composables/index.ts
	new file:   src/composables/useEventStore/index.ts
	  * Started work on an event store. I'm not sure this counts as a store just yet
	  * Wired up majority of pointer events
	  * Added event propogation
	  * Does not require using userData scene props or nodeOps for registering objects to scene
	modified:   src/composables/useRaycaster/index.ts
	  * Added new event listeners to power newly supported pointer events. We now check whole scene/children when calling intersectObjects.
	  * Created new EventHooks for new events
	  * Added `forceUpdate` function that allows for pointer-move events to work without mouth movement (good for when camera is moving but mouse is not)

	modified:   src/core/nodeOps.ts
	  * Added supported events to array so they don't get received as props
	  * (temporarily) unhook current pointer event solution to iterate on useEventStore
	modified:   src/utils/index.ts
	  * Added Camel-to-kebab case util

* Support multiple event listeners, add support for .stop event modifier

* Set stopProgation variable to false by default, whoops

* fix typo

* fix: remove `createGlobalState` from `useEventStore`, allowing events to work while multiple TresCanvas' are being used

* fix(perf): remove extraneous intersectObjects/getIntersects calls by moving intersects into a ref that is updated on pointer-move

* chore(lint): fix lint issues

* feat: enhance events manager to include duplicates checking, pointer-missed support, and forced updating

Per file changelog:
	modified:   playground/src/components/Box.vue
	  * Added a pointer-missed handler for testing
	modified:   playground/src/pages/TheBasic.vue
	  * uses forceUpdate from EventManager to fire events even when the mouse hasn't moved
	modified:   playground/src/pages/raycaster/Propagation.vue
	  * Didn't mean to undo the lint changes, adds a pointer-missed event on the canvas 		for extra testing
	modified:   src/components/TresCanvas.vue
	  * Adds `pointer-missed` as possible event for canvas emits
	modified:   src/composables/index.ts
	  * Update export
	deleted:    src/composables/useEventStore/index.ts
	  * Rename `useEventStore` to `useTresEventManager`
	modified:   src/composables/useRaycaster/index.ts
	  * Check for empty intersects on hit test, wire up pointerMissed events eventHook
	  * Fix forceUpdate to call onPointerMove instead of triggering an EventHook
	modified:   src/composables/useTresContextProvider/index.ts
	  * Add TresEventManager type
	new file:   src/composables/useTresEventManager/index.ts
	  * add onPointerMissed
	  * create (de)registerPointerMissedObj methods so we can track objects in the scene listening to this event
	  * Note: These are passed to nodeOps via TresContext
	  * Implement duplicates checking for eventPropogation
	modified:   src/core/nodeOps.ts
	  * register/deregister pointerMissed objects

* chore: lint

* docs: new event docs

* chore: fix lint

* feat: enhance event object details and use in Box example to change material color. Add ability to force event system updates even when mouse hasn't moved. Enhance pointer-enter/leave events. Update types

  Box.vue
    * Added pointer-missed handler
    * set the materials flash color using the object coming off of the event instead of a ref
  UseRaycaster
    * Flesh out event details to include
      * all mouse event properties
      * intersections
      * tres camera
      * camera raycaster
      * source event
      * mouse position delta
      * stopPropagating stub
      * and unprojectedPoint (this needs work, cant get the math to work)
  UseTresContextProvider
    * Add TresEventManager type to TresContext
  useTresEventManager
    * Add forceUpdate method to allow apps to force an event system update even when the mouse hasnt moved
    * Add pointerMissed event
    * Properly implement pointer-enter/pointer-leave events
      * Before now, pointer-enter | leave were only called on first object in intersection, now we execute the events for all entered/left objects
    * Use stopPropagating property included on event object

* chore: lint

* chore: fix lint issues

---------

Co-authored-by: alvarosabu <alvaro.saburido@gmail.com>
  • Loading branch information
garrlker and alvarosabu committed Apr 22, 2024
1 parent cdf6b6f commit b536ab1
Show file tree
Hide file tree
Showing 14 changed files with 776 additions and 113 deletions.
64 changes: 52 additions & 12 deletions docs/api/events.md
Expand Up @@ -6,22 +6,62 @@

## Pointer Events

The following pointer events are available on `v3` and previous:

- `click`
- `pointer-move`
- `pointer-enter`
- `pointer-leave`

From `v4.x` on, the following pointer events are been added to the list:

- `context-menu` (right click)
- `double-click`
- `pointer-down`
- `pointer-up`
- `wheel`
- `pointer-missed`

```html
<TresMesh
@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)"
@click="(event) => console.log('click')"
@context-menu="(event) => console.log('context-menu (right click)')"
@double-click="(event) => console.log('double-click')"
@pointer-move="(event) => console.log('pointer-move')"
@pointer-enter="(event) => console.log('pointer-enter')"
@pointer-leave="(event) => console.log('pointer-leave')"
@pointer-down="(event) => console.log('pointer-down')"
@pointer-up="(event) => console.log('pointer-up')"
@wheel="(event) => console.log('wheel')"
@pointer-missed="(event) => console.log('pointer-missed')"
/>
```

| 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) |
| <div style="width:160px">Event</div> | 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) |
| contextMenu <Badge type="warning" text="4.0.0" /> | the user triggers a context menu, often by right-clicking | [PointerEvent](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent) |
| double-click <Badge type="warning" text="4.0.0" /> | the user clicks the mouse button twice in quick succession on the same 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) |
| wheel <Badge type="warning" text="4.0.0" /> | the mouse wheel or similar device is rotated | [WheelEvent](https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent) |
| pointer-down <Badge type="warning" text="4.0.0" /> | the pointer is pressed down over 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-up <Badge type="warning" text="4.0.0" /> | the pointer is released over 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) |
| 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-missed <Badge type="warning" text="4.0.0" /> | the pointer interaction is attempted but misses the object | [PointerEvent](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent) |

## Event Propagation (Bubbling 🫧) <Badge type="warning" text="^4.0.0" />

Propagation of events on 3D scenes works differently than in the DOM because objects can **occlude each other in 3D**. The `intersections` array contains all the objects that the raycaster intersects with, sorted by distance from the camera. The first object in the array is the closest one to the camera.

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`.
When an event is triggered, the event is propagated to the closest object in the `intersections` array. If the event is not handled by the object, it will be propagated to the next object in the array.

By default, objects positioned in front of others with event handlers do not prevent those events from being triggered. This behavior can be achieved by using the prop `blocks-pointer-events`.
`event.stopPropagation()` can be used to stop the event from propagating to the next object in the array, stoping the event from bubbling up and reaching to farther objects (the oens behind the first one).

```html
<TresMesh
@pointer-down="(event) => {
console.log('pointer-down')
event.stopPropagation()
}"
/>
```
2 changes: 2 additions & 0 deletions playground/components.d.ts
Expand Up @@ -10,13 +10,15 @@ declare module 'vue' {
AkuAku: typeof import('./src/components/AkuAku.vue')['default']
AnimatedModel: typeof import('./src/components/AnimatedModel.vue')['default']
BlenderCube: typeof import('./src/components/BlenderCube.vue')['default']
Box: typeof import('./src/components/Box.vue')['default']
CameraOperator: typeof import('./src/components/CameraOperator.vue')['default']
Cameras: typeof import('./src/components/Cameras.vue')['default']
copy: typeof import('./src/components/TheBasic copy.vue')['default']
DanielTest: typeof import('./src/components/DanielTest.vue')['default']
DebugUI: typeof import('./src/components/DebugUI.vue')['default']
DeleteMe: typeof import('./src/components/DeleteMe.vue')['default']
DynamicModel: typeof import('./src/components/DynamicModel.vue')['default']
EventsPropogation: typeof import('./src/components/EventsPropogation.vue')['default']
FBXModels: typeof import('./src/components/FBXModels.vue')['default']
Gltf: typeof import('./src/components/gltf/index.vue')['default']
GraphPane: typeof import('./src/components/GraphPane.vue')['default']
Expand Down
44 changes: 44 additions & 0 deletions playground/src/components/Box.vue
@@ -0,0 +1,44 @@
<script setup lang="ts">
import { ref, shallowRef } from 'vue'
import { useRenderLoop } from '@tresjs/core'
import { Color } from 'three'
const props = defineProps(['position', 'name'])
// TODO: Once we have troika text in cientos, display the count over each box
const count = ref(0)
const boxRef = shallowRef()
// Event Testing Colors
const black = new Color('black')
const green = new Color('green')
const blue = new Color('blue')
// Once the box has flashed green, lerp it back to black
const { onLoop } = useRenderLoop()
onLoop(() => {
boxRef.value?.material.color.lerp(black, 0.1)
})
// onClick flash the box a color and update the counter
function handleClick(color: Color, ev) {
count.value++
ev?.eventObject?.material.color.set(color)
// eslint-disable-next-line no-console
console.log(`Box ${boxRef.value.name} count=${count.value}`)
}
</script>

<template>
<TresMesh
ref="boxRef"
v-bind="props"
@click.self="ev => handleClick(green, ev)"
@pointer-missed="ev => handleClick(blue, ev)"
>
<TresBoxGeometry />
<TresMeshStandardMaterial />
<slot></slot>
</TresMesh>
</template>
14 changes: 13 additions & 1 deletion playground/src/pages/basic/index.vue
Expand Up @@ -14,13 +14,17 @@ const state = reactive({
toneMapping: NoToneMapping,
})
const canvasRef = ref()
const sphereRef = ref()
const { onLoop } = useRenderLoop()
onLoop(({ elapsed }) => {
if (!sphereRef.value) { return }
sphereRef.value.position.y += Math.sin(elapsed) * 0.01
// Update events without needing the mouse to move
canvasRef.value?.context?.eventManager.forceUpdate()
})
function onPointerEnter(ev) {
Expand All @@ -29,6 +33,10 @@ function onPointerEnter(ev) {
}
}
function onPointerOut(ev) {
ev.object.material.color.set('teal')
}
const sphereExists = ref(true)
</script>

Expand All @@ -37,7 +45,10 @@ const sphereExists = ref(true)
v-model="sphereExists"
type="checkbox"
/>
<TresCanvas v-bind="state">
<TresCanvas
ref="canvasRef"
v-bind="state"
>
<TresPerspectiveCamera
:position="[5, 5, 5]"
:fov="45"
Expand All @@ -56,6 +67,7 @@ const sphereExists = ref(true)
:position="[0, 4, 0]"
cast-shadow
@pointer-enter="onPointerEnter"
@pointer-out="onPointerOut"
>
<TresSphereGeometry :args="[2, 32, 32]" />
<TresMeshToonMaterial color="teal" />
Expand Down
188 changes: 188 additions & 0 deletions playground/src/pages/events/Propagation.vue
@@ -0,0 +1,188 @@
<script setup lang="ts">
import { onUnmounted, ref } from 'vue'
import {
TresCanvas,
} from '@tresjs/core'
import { BasicShadowMap, NoToneMapping, SRGBColorSpace } from 'three'
import { OrbitControls } from '@tresjs/cientos'
import '@tresjs/leches/styles'
import Box from '../../components/Box.vue'
const gl = {
clearColor: '#202020',
shadows: true,
alpha: false,
shadowMapType: BasicShadowMap,
outputColorSpace: SRGBColorSpace,
toneMapping: NoToneMapping,
}
const showBox = ref(true)
const intervalRef = setInterval(() => {
// showBox.value = !showBox.value;
}, 1000)
onUnmounted(() => {
clearInterval(intervalRef)
})
</script>

<template>
<TresCanvas
window-size
v-bind="gl"
@pointer-missed="event => console.log('pointer-missed', event)"
>
<TresPerspectiveCamera
:position="[0, 0, 6]"
:look-at="[0, 0, 0]"
/>
<OrbitControls />

<TresDirectionalLight
:intensity="1"
:position="[1, 1, 1]"
/>
<TresAmbientLight :intensity="1" />
<Box
:position="[0, 1.5, 0]"
name="A0"
>
<Box
:position="[-0.66, -1, 0]"
name="B0"
>
<Box
:position="[-0.66, -1, 0]"
name="C0"
>
<Box
:position="[-0.66, -1, 0]"
name="D0"
/>
<Box
:position="[0.66, -1, 0]"
name="D1"
/>
</Box>
<Box
:position="[0.66, -1, 0]"
name="C1"
>
<Box
:position="[0.66, -1, 0]"
name="D2"
/>
</Box>
</Box>
<Box
:position="[0.66, -1, 0]"
name="B1"
>
<Box
:position="[0.66, -1, 0]"
name="C2"
>
<Box
v-if="showBox"
:position="[0.66, -1, 0]"
name="D3"
/>
</Box>
</Box>
</Box>
<Box
:position="[0, 1.5, -3]"
name="A0"
>
<Box
:position="[-0.66, -1, 0]"
name="B0"
>
<Box
:position="[-0.66, -1, 0]"
name="C0"
>
<Box
:position="[-0.66, -1, 0]"
name="D0"
/>
<Box
:position="[0.66, -1, 0]"
name="D1"
/>
</Box>
<Box
:position="[0.66, -1, 0]"
name="C1"
>
<Box
:position="[0.66, -1, 0]"
name="D2"
/>
</Box>
</Box>
<Box
:position="[0.66, -1, 0]"
name="B1"
>
<Box
:position="[0.66, -1, 0]"
name="C2"
>
<Box
:position="[0.66, -1, 0]"
name="D3"
/>
</Box>
</Box>
</Box>
<Box
:position="[0, 1.5, -6]"
name="A0"
>
<Box
:position="[-0.66, -1, 0]"
name="B0"
>
<Box
:position="[-0.66, -1, 0]"
name="C0"
>
<Box
:position="[-0.66, -1, 0]"
name="D0"
/>
<Box
:position="[0.66, -1, 0]"
name="D1"
/>
</Box>
<Box
:position="[0.66, -1, 0]"
name="C1"
>
<Box
:position="[0.66, -1, 0]"
name="D2"
/>
</Box>
</Box>
<Box
:position="[0.66, -1, 0]"
name="B1"
>
<Box
:position="[0.66, -1, 0]"
name="C2"
>
<Box
:position="[0.66, -1, 0]"
name="D3"
/>
</Box>
</Box>
</Box>
</TresCanvas>
</template>

0 comments on commit b536ab1

Please sign in to comment.