Skip to content

Commit

Permalink
refactor: events
Browse files Browse the repository at this point in the history
* (fix) BREAKING - remove `event.intersects` – use `event.intersections`
* (fix) BREAKING - remove `event.stopPropagating` – use `event.stopPropagation`
* (fix) BREAKING – from TresCanvas, don't emit pointer/click events bubbled from Tres objects
* (fix) BREAKING – `event.delta` is now reset to `0` following a `click`.
* (fix) BREAKING – `pointerleave` handlers were sent previous intersections at `event.intersections`, now current intersections.
* (fix) BREAKING – `@dblclick` now fires whenever the canvas `dblclick` is emitted. Any TresObject with an `@dblclick` handler will receive the event if it is under the pointer. (Previously objects not under the initial click did not receive the `@dblclick`.)
* (fix) `event.pointer` is now defined – previously typed but was `undefined` in implementation.
* (fix) `event.unprojectedPoint` is now properly calculated. Was previously left as `TODO`.
* (fix) `event.eventObject` holds the object that registered the event handler. Was previously typed but `undefined` in implementation.
* (fix) use Vue-style ("flatcase") event names, e.g. `@pointerdown`. Keep support for old-style ("kebab-case") event names, e.g. `@pointer-down`, and warn. Mixing both on the same object is not supported and may lead to handlers being overwritten.
* (fix) `@pointer{leave,out}` was fired multiple times on a single "leave" – Issue #801
* (feat) BREAKING – `pointermissed` fires when the object that registered the handler is missed – previously only fired when ALL objects were missed
* (feat) add filtering/sorting for intersections, prior to handling events. `:events={filter: (intersections) => ...}`
* (feat) support Vue event modifiers – `stop, prevent, self, once`. (Tres cannot support `passive` and does not currently support `capture`.)
* (feat) via `:events-target` prop, allow users to specify an HTML `addEventListener` target – allows Tres to respond to events, even if there's an overlay on the canvas.
* (feat) via `:events-enabled` prop, allow `eventManager` to be enabled/disabled while Tres is running.
* (feat) via `:events` prop, allow `eventManager` functions to be set/overwritten (non-reactive).
* (feat) via `:events` prop, allow events system to be disabled completely: `<TresCanvas :events="null" />` (non-reactive)
* (feat) `pointer{leave,out}` is triggered (if necessary) just prior to object removal.
* (feat) `:blocking="true"` makes a subtree "solid"; objects behind objects in the subtree will not be "hit".
  • Loading branch information
andretchen0 committed Sep 12, 2024
1 parent 04b001b commit 46545d5
Show file tree
Hide file tree
Showing 39 changed files with 4,594 additions and 888 deletions.
111 changes: 111 additions & 0 deletions playground/vue/src/pages/events/Blocking.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<!-- eslint-disable no-console -->
<script setup lang="ts">
import { OrbitControls } from '@tresjs/cientos'
import { TresCanvas } from '@tresjs/core'
import { Vector3 } from 'three'
import type { ThreeEvent } from '@tresjs/core'

function onClick(ev: ThreeEvent<MouseEvent>) {
ev.eventObject.material.color.set('red')
}

function onPointerOver(ev: ThreeEvent<MouseEvent>) {
ev.eventObject.material.color.set('#FFCC00')
}

function onPointerOut(ev: ThreeEvent<MouseEvent>) {
ev.eventObject.material.color.set(ev.eventObject.color)
}

const blockingIdRef = ref<string>('scene')
function onChangeBlockingId(id: string) {
blockingIdRef.value = id
}

const COUNT = 12
const positions0: Vector3[] = []
const positions1: Vector3[] = []
for (let i = 0; i < COUNT; i++) {
const ang = i / COUNT * 2 * Math.PI
positions0.push(new Vector3(Math.cos(ang) * 20, 0, Math.sin(ang) * 20))
}
for (let i = 0; i < COUNT; i++) {
const ang = i / COUNT * 2 * Math.PI
positions1.push(new Vector3(Math.cos(ang) * 30, 0, Math.sin(ang) * 30))
}
console.log(positions0)
</script>

<template>
<OverlayInfo>
<h1><code>:blocking</code></h1>
<p>By default, from the perspective of the pointer/mouse in Tres, 3D objects are <strong>not</strong> "solid".</p>
<p>Clicks and other pointer events "hit" all objects under the pointer, event if those objects are behind other objects.</p>
<p>But you can make a subtree "solid" by marking it with <code>:blocking="true"</code></p>
<h2>Setup</h2>
<p><strong>Select "blocking" objects in the scene.</strong> Then interact with the scene to see the effect.</p>
<div v-for="id of ['scene', 'cubes', 'spheres', 'none (default Tres behavior)']" :key="id">
<input :id="id" :checked="id === blockingIdRef" type="radio" name="target" @change="() => onChangeBlockingId(id)" />
<label :for="id">{{ id }}</label>
</div>
</OverlayInfo>
<TresCanvas window-size clear-color="gray" :blocking="blockingIdRef === 'scene'">
<OrbitControls />
<TresPerspectiveCamera
:position="[0, 10, 80]"
:look-at="[0, 0, 0]"
/>
<TresGroup :blocking="blockingIdRef === 'scene'">
<TresMesh
v-for="pos, i of positions0"
:key="i"
:position="pos"
:blocking="blockingIdRef === 'cubes'"
:scale="5"
color="green"
@pointer-over="onPointerOver"
@pointer-out="onPointerOut"
@click="onClick"
>
<TresBoxGeometry :args="[]" />
<TresMeshBasicMaterial color="green" />
</TresMesh>

<TresMesh
v-for="pos, i of positions1"
:key="i"
:position="pos"
:blocking="blockingIdRef === 'spheres'"
color="purple"
@pointer-over="onPointerOver"
@pointer-out="onPointerOut"
@click="onClick"
>
<TresSphereGeometry :args="[]" />
<TresMeshBasicMaterial color="purple" />
</TresMesh>
<TresMesh
:scale="10"
color="blue"
@pointer-over="onPointerOver"
@pointer-out="onPointerOut"
@click="onClick"
>
<TresTorusGeometry />
<TresMeshBasicMaterial color="blue" />
</TresMesh>

<TresMesh
:scale="40"
color="gray"
@pointer-over="onPointerOver"
@pointer-out="onPointerOut"
@click="onClick"
>
<TresCylinderGeometry :scale="[-1, 1, 1]" />
<TresMeshBasicMaterial color="gray" />
</TresMesh>
</TresGroup>
<TresDirectionalLight :intensity="1" />
</TresCanvas>
</template>
163 changes: 163 additions & 0 deletions playground/vue/src/pages/events/Connect.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
<!-- eslint-disable no-console -->
<script setup lang="ts">
import { OrbitControls } from '@tresjs/cientos'
import { TresCanvas } from '@tresjs/core'
import { BasicShadowMap, NoToneMapping, SRGBColorSpace } from 'three'
import type { ThreeEvent, TresContext } from '@tresjs/core'

const gl = {
clearColor: '#202020',
shadows: true,
alpha: false,
shadowMapType: BasicShadowMap,
outputColorSpace: SRGBColorSpace,
toneMapping: NoToneMapping,
}

function onClick(ev: ThreeEvent<MouseEvent>) {
ev.eventObject.material?.color.set('#008080')
}

function onDoubleClick(ev: ThreeEvent<MouseEvent>) {
ev.eventObject.material.color.set('#FFAA00')
}

function onPointerEnter(ev: ThreeEvent<MouseEvent>) {
ev.eventObject.material.color.set('#CCFF03')
}

function onPointerLeave(ev: ThreeEvent<MouseEvent>) {
ev.eventObject.material.color.set('#efefef')
}

function onPointerMove(ev: ThreeEvent<MouseEvent>) {
ev.eventObject.material.color.set('#F00')
}

function onContextMenu(ev: ThreeEvent<MouseEvent>) {
ev.eventObject.material.color.set('#FF4500')
}

function onPointerMissed(ev: ThreeEvent<MouseEvent>) {
ev.eventObject.material.color.set('#000000')
}

function onWheel(ev: ThreeEvent<MouseEvent>) {
ev.eventObject.material.color.set('#FFFF00')
}

const defaultConnecter = (target: EventTarget, eventManagerHandler: EventListener) => {
const POINTER_EVENT_NAMES = ['wheel', 'click', 'pointermove', 'pointerup', 'pointerdown', 'contextmenu', 'dblclick']
for (const domEventName of POINTER_EVENT_NAMES) {
target.addEventListener(domEventName, eventManagerHandler)
}

return {
disconnect: () => {
for (const domEventName of POINTER_EVENT_NAMES) {
target.removeEventListener(domEventName, eventManagerHandler)
}
},
}
}

const CONNECTERS: Record<string, (eventTarget: HTMLElement, handler: EventListener) => ({ disconnect: () => void })> = {
'default': defaultConnecter,
'click': (eventTarget, handler) => {
eventTarget.addEventListener('click', handler)
return { disconnect: () => { eventTarget.removeEventListener('click', handler) } }
},
'dblclick, wheel': (eventTarget, handler) => {
eventTarget.addEventListener('dblclick', handler)
eventTarget.addEventListener('wheel', handler)
return { disconnect: () => {
eventTarget.removeEventListener('dblclick', handler)
eventTarget.removeEventListener('wheel', handler)
} }
},
}

const CONNECTER_IDS = Object.keys(CONNECTERS)
const connecterIdRef = ref<keyof typeof CONNECTERS>('default')

const eventsFns = {
connect: CONNECTERS[connecterIdRef.value],
}

type DisconnectFn = () => void
type ConnectFn = (target: HTMLElement) => void

let eventManager: {
connect: ConnectFn
disconnect: DisconnectFn
target: HTMLElement
} | null = null

function onChangeConnecterId(id: keyof typeof CONNECTERS) {
if (!eventManager) { return }
const target = eventManager.target
eventManager.disconnect()
connecterIdRef.value = id
eventsFns.connect = CONNECTERS[connecterIdRef.value]
eventManager.connect(target)
}

function ready(ctx: TresContext) {
eventManager = ctx.eventManager
}
</script>

<template>
<OverlayInfo>
<h1><code>connect</code></h1>
<p>By default, <code>eventManager</code> will listen for DOM events emitted from the canvas. But if you don't need some of those events – particularly <code>pointermove</code>, you can get extra performance by not listening for them.</p>
<p>:events has a settable <code>connect</code> function.</p>
<code>&lt;TresCanvas :events="{connect: ...}" /&gt;</code>
<hr />
<h2>Example <code>connect</code></h2>
<p><strong>Select a connector function below (see function implementation in the Vue playground file) and then interact with the on-screen objects to see its effect.</strong></p>
<div v-for="id of CONNECTER_IDS" :key="id">
<input :id="id" :checked="id === connecterIdRef" type="radio" name="target" @change="() => onChangeConnecterId(id)" />
<label :for="id">{{ id }}</label>
</div>
<hr />
<h2>NOTE</h2>
<p>The <code>:events</code> prop is not reactive. But if you have a reference to it, you can redefine functions, which will then be called at some later point during the lifecycle.</p>
<p><code>connect</code> is not designed to be changed on the fly. To do so, as we have done here, you will have to get a handle on <code>context.eventManager</code> and call <code>disconnect</code>, then <code>connect</code>.</p>
</OverlayInfo>
<TresCanvas
window-size
v-bind="gl"
:events="eventsFns"
@ready="ready"
>
<TresPerspectiveCamera
:position="[15, 15, 15]"
:look-at="[0, 0, 0]"
/>
<OrbitControls />

<template v-for="x in [-2.5, 0, 2.5]">
<template v-for="y in [-2.5, 0, 2.5]">
<TresMesh
v-for="z in [-2.5, 0, 2.5]"
:key="`${[x, y, z]}`"
:position="[x, y, z]"
@click="onClick"
@dblclick="onDoubleClick"
@pointerenter="onPointerEnter"
@pointerleave="onPointerLeave"
@pointermove="onPointerMove"
@contextmenu="onContextMenu"
@pointermissed="onPointerMissed"
@wheel="onWheel"
>
<TresBoxGeometry :args="[1, 1, 1]" />
<TresMeshToonMaterial color="#efefef" />
</TresMesh>
</template>
</template>
<TresDirectionalLight :intensity="1" />
<TresAmbientLight :intensity="1" />
</TresCanvas>
</template>
87 changes: 87 additions & 0 deletions playground/vue/src/pages/events/DeprecatedEventNames.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<!-- eslint-disable no-console -->
<script setup lang="ts">
import { OrbitControls } from '@tresjs/cientos'
import { TresCanvas } from '@tresjs/core'
import type { ThreeEvent } from '@tresjs/core'
import '@tresjs/leches/styles'

function onClick(ev: ThreeEvent<MouseEvent>) {
console.log('click', ev)
ev.eventObject.material.color.set('#008080')
}

function onDoubleClick(ev: ThreeEvent<MouseEvent>) {
console.log('double-click', ev)
ev.eventObject.material.color.set('#FFD700')
}

function onPointerEnter(ev: ThreeEvent<MouseEvent>) {
ev.eventObject.material.color.set('#CCFF03')
}

function onPointerLeave(ev: ThreeEvent<MouseEvent>) {
console.log('pointer-leave', ev)
}

function onPointerMove(ev: ThreeEvent<MouseEvent>) {
console.log('pointer-move', ev)
}

function onContextMenu(ev: ThreeEvent<MouseEvent>) {
console.log('context-menu', ev)
ev.eventObject.material.color.set('#FF4500')
}

function onPointerMissed(ev: ThreeEvent<MouseEvent>) {
console.log('pointer-missed', ev)
ev.eventObject.material.color.set('#000')
}
</script>

<template>
<OverlayInfo>
<h1>Deprecated event names</h1>
<h2>Setup</h2>
<p>This scene contains TresObjects with deprecated event names.</p>
<p>Regardless, interacting with the objects should trigger their handlers. (They will change colors and log to the console.)</p>
<h2>Background</h2>
<p>Vue event names are written like:</p>
<p><strong><code>@</code> + DOM event name</strong>, e.g. <code>@pointerdown</code></p>
<p>DOM events names are typically, but not always, written in <a href="https://en.wikipedia.org/wiki/Naming_convention_(programming)#Examples_of_multiple-word_identifier_formats">flatcase</a>.</p>
<p>Through Tres v4.x, Tres' multiple-word event names were written in <a href="https://en.wikipedia.org/wiki/Naming_convention_(programming)#Examples_of_multiple-word_identifier_formats">kebab-case</a>, e.g. <code>@pointer-down</code>.</p>
<p>Now Tres has adopted the Vue naming convention for supported events.</p>
<p>Old-style hyphenated events will still respond, though they will generate a console warning in dev mode the first time they're encountered in a scene.</p>
<p>The developper should take care not to mix hyphenated and non-hyphenated event names.</p>
</OverlayInfo>
<TresCanvas
window-size
clear-color="gray"
>
<TresPerspectiveCamera
:position="[11, 11, 11]"
:look-at="[0, 0, 0]"
/>
<OrbitControls />
<template v-for="x in [-2.5, 0, 2.5]">
<template v-for="y in [-2.5, 0, 2.5]">
<TresMesh
v-for="z in [-2.5, 0, 2.5]"
:key="`${[x, y, z]}`"
:position="[x, y, z]"
@click="onClick"
@double-click="onDoubleClick"
@pointer-enter="onPointerEnter"
@pointer-leave="onPointerLeave"
@pointer-move="onPointerMove"
@context-menu="onContextMenu"
@pointer-missed="onPointerMissed"
>
<TresBoxGeometry :args="[1, 1, 1]" />
<TresMeshToonMaterial color="#efefef" />
</TresMesh>
</template>
</template>
<TresDirectionalLight :intensity="1" />
<TresAmbientLight :intensity="1" />
</TresCanvas>
</template>
Loading

0 comments on commit 46545d5

Please sign in to comment.