Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(events)!: pointerevents manager and state #529

Merged
merged 19 commits into from
Apr 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 52 additions & 12 deletions docs/api/events.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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>
Loading
Loading