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

refactor: events #844

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
Commits
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
Prev Previous commit
Next Next commit
working
andretchen0 committed Sep 18, 2024
commit e1af2e9ff651b8933eb5238c2f29316964c10985
97 changes: 97 additions & 0 deletions playground/vue/src/pages/events/PointerOverOut.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<!-- eslint-disable no-console -->
<script setup lang="ts">
import { OrbitControls } from '@tresjs/cientos'
import { TresCanvas } from '@tresjs/core'
import { shallowRef } from 'vue'

const tresNumOver = ref(0)
const tresNumOut = ref(0)
const tresNumEnter = ref(0)
const tresNumLeave = ref(0)

const domNumOver = ref(0)
const domNumOut = ref(0)
const domNumEnter = ref(0)
const domNumLeave = ref(0)

const config = ref({ priorIntersections: [] })

const ready = (context) => {
config.value = context.eventManager.config
}
</script>

<template>
<div>
{{ config.priorIntersections.map(intr => intr.object.uuid )}} aa
<div class="grid-container">
<div class="col">
<h2>DOM</h2>
pointerover: {{ domNumOver }}<br />
pointerout: {{ domNumOut }}<br />
pointerenter: {{ domNumEnter }}<br />
pointerleave: {{ domNumLeave }}<br />
<div
:style="{ backgroundColor: 'gray' }"
@pointerover="domNumOver++"
@pointerout="domNumOut++"
@pointerenter="domNumEnter++"
@pointerleave="domNumLeave++"
>
<div :style="{ backgroundColor: 'blue' } ">
<div :style="{ backgroundColor: 'purple', color: 'white' }">Mouse over me.</div>
</div>
</div>
</div>

<div class="col">
<h2>Tres</h2>
pointerover: {{ tresNumOver }}<br />
pointerout: {{ tresNumOut }}<br />
pointerenter: {{ tresNumEnter }}<br />
pointerleave: {{ tresNumLeave }}<br />
<div :style="{ width: 'auto', height: '200px' }">
<TresCanvas @ready="ready">
<OrbitControls />
<TresPerspectiveCamera :position="[0, 0, 5]" />
<TresMesh
:blocking="true"
@pointerenter="tresNumEnter++"
@pointerleave="tresNumLeave++"
@pointerover="tresNumOver++"
@pointerout="tresNumOut++"
>
<TresBoxGeometry :args="[7, 3, 4.5]" />
<TresMeshToonMaterial color="gray" />
<TresMesh>
<TresBoxGeometry :args="[5, 2, 5]" />
<TresMeshToonMaterial color="blue" />
<TresMesh :position-z="2.1">
<TresBoxGeometry />
<TresMeshToonMaterial color="purple" />
</TresMesh>
</TresMesh>
</TresMesh>
<TresDirectionalLight :intensity="1" />
<TresAmbientLight :intensity="1" />
</TresCanvas>
</div>
</div>
</div>
</div>
</template>

<style>
div {
padding: 20px;
margin: 20px;
background-color: white;
border: 1px solid #ccc;
}

.grid-container {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-auto-flow: column;
}
</style>
7 changes: 6 additions & 1 deletion playground/vue/src/router/routes/events.ts
Original file line number Diff line number Diff line change
@@ -21,9 +21,14 @@ export const eventsRoutes = [
},
{
path: '/events/pointer-enter-leave',
name: 'PointerEnter/Leave',
name: 'Pointerenter/leave',
component: () => import('../../pages/events/PointerEnterLeave.vue'),
},
{
path: '/events/pointer-over-out',
name: 'Pointerover/out comparison with Vue DOM',
component: () => import('../../pages/events/PointerOverOut.vue'),
},
{
path: '/events/stop-propagation',
name: 'PointerEnter/StopPropagation',
9 changes: 6 additions & 3 deletions src/types/index.ts
Original file line number Diff line number Diff line change
@@ -102,7 +102,7 @@ export interface IntersectionEvent<TSourceEvent> extends Intersection {
eventObject: TresObject
/** The event source (the object that registered the handler) – equivalent of DOM event.currentTarget */
currentTarget: TresObject
/** The "hit" object, where event bubbling began */
/** The "hit" object, where event bubbling began. */
target: TresObject
/** An array of intersections */
intersections: Intersection[]
@@ -131,9 +131,9 @@ export type DomEvent = PointerEvent | MouseEvent | WheelEvent

export interface TresEvent {
eventObject: TresObject
currentTarget: TresObject
target: TresObject
event: DomEvent
stopPropagation: () => void
stopPropagating: boolean
intersections: Intersection[]
intersects: Intersection[]
}
@@ -145,7 +145,10 @@ export interface Events {
onWheel: EventListener
onPointerdown: EventListener
onPointerup: EventListener
onPointerenter: EventListener
onPointerleave: EventListener
onPointerover: EventListener
onPointerout: EventListener
onPointermove: EventListener
onPointercancel: EventListener
onLostpointercapture: EventListener
505 changes: 403 additions & 102 deletions src/utils/createEventManager/eventsRaycast.test.ts

Large diffs are not rendered by default.

122 changes: 88 additions & 34 deletions src/utils/createEventManager/eventsRaycast.ts
Original file line number Diff line number Diff line change
@@ -23,7 +23,7 @@ import type { CreateEventManagerProps } from './createEventManager'
// future modifications simpler if Event type changes.
type RaycastEvent = MouseEvent | PointerEvent | WheelEvent
type RaycastEventTarget = DomEventTarget
type ThreeEventStub<DomEvent> = Omit<ThreeEvent<DomEvent>, 'eventObject' | 'object' | 'distance' | 'point'> & Partial<IntersectionEvent<DomEvent>>
type ThreeEventStub<DomEvent> = Omit<ThreeEvent<DomEvent>, 'eventObject' | 'object' | 'currentTarget' | 'target' | 'distance' | 'point'> & Partial<IntersectionEvent<DomEvent>>
type Object3DWithEvents = Object3D & EventHandlers

function getInitialEvent() {
@@ -59,8 +59,9 @@ function getInitialConfig(context: TresContext) {

objectsWithEvents: [] as Object3D[],

intersections: [] as ThreeIntersection[],
hits: new Set<Object3D>(),
priorIntersections: [] as ThreeIntersection[],
priorHits: new Set<Object3D>(),
// TODO: Use or remove
initialHits: new Set<Object3D>(),
blockingObjects: new Set<Object3D>(),

@@ -227,7 +228,7 @@ function remove(_instance: TresObject, config: Config) {

config.objectsWithEvents = config.objectsWithEvents.filter(obj => !instanceAndDescendants.has(obj))

const intersections = config.intersections.filter(intersection => !instanceAndDescendants.has(intersection.object))
const intersections = config.priorIntersections.filter(intersection => !instanceAndDescendants.has(intersection.object))

// NOTE: We will call `pointerout` and `pointerleave` if the to-be-removed
// object was under the mouse. That logic is contained within `handleEvent`.
@@ -236,7 +237,7 @@ function remove(_instance: TresObject, config: Config) {
handleIntersections(getLastEvent(config), intersections, config)

// NOTE: Remove the remaining traces of the object and descendants
config.hits = config.hits.difference(instanceAndDescendants)
config.priorHits = config.priorHits.difference(instanceAndDescendants)
config.isEventsDirty = true
}

@@ -268,6 +269,7 @@ function handleIntersections(incomingEvent: RaycastEvent, intersections: ThreeIn
// after we see one containing a "blocking" object.
const hits = new Set<Object3D>()
const initialHits = new Set<Object3D>()
const filteredIntersections = []
const eventIntersections = []
let obj: TresObject | null = null
let hasBlockingObject = false
@@ -277,6 +279,7 @@ function handleIntersections(incomingEvent: RaycastEvent, intersections: ThreeIn
if (hits.has(obj)) {
continue
}
filteredIntersections.push(intersection)
while (obj) {
if (config.blockingObjects.has(obj)) {
hasBlockingObject = true
@@ -298,7 +301,7 @@ function handleIntersections(incomingEvent: RaycastEvent, intersections: ThreeIn

// NOTE:
// 2) Create the outgoing event "stub".
// This includes the fields that all subsequent
// This includes fields that all subsequent
// event handler calls will need.
const distance = (incomingEvent.type === 'click' || incomingEvent.type === 'contextmenu' || incomingEvent.type === 'dblclick' || incomingEvent.type === 'pointerup')
? Math.sqrt(((incomingEvent.offsetX - config.pointerDownPosition.x) ** 2) + ((incomingEvent.offsetY - config.pointerDownPosition.y) ** 2))
@@ -321,13 +324,17 @@ function handleIntersections(incomingEvent: RaycastEvent, intersections: ThreeIn
ray: config.raycaster.ray,
camera: config.raycaster.camera,
nativeEvent: incomingEvent,
// NOTE: If the user has e.g. `@click.prevent`, Vue will
// call `preventDefault` internally without checking whether
// it exists and is a function. This will throw unless we
// add it to the outgoing event.
preventDefault: incomingEvent.preventDefault,
stopped: false,
stopPropagation: () => {},
},
)

outgoingEvent.stopPropagation = () => { outgoingEvent.stopped = true; incomingEvent.stopPropagation() }
outgoingEvent.preventDefault = () => { incomingEvent.preventDefault() }

// NOTE:
// 3) Propagate the events to handlers.
@@ -347,31 +354,77 @@ function handleIntersections(incomingEvent: RaycastEvent, intersections: ThreeIn
}
}

// NOTE: Propagate `pointer{out,leave,over,enter}`
if (incomingEvent.type === 'pointermove') {
/**
* Events mouseenter/mouseleave are like mouseover/mouseout.
* They trigger when the mouse pointer enters/leaves the element.
*
* But there are two important differences:
*
* - Transitions inside the element, to/from descendants, are not counted.
* - Events mouseenter/mouseleave do not bubble.
*
* https://javascript.info/mousemove-mouseover-mouseout-mouseenter-mouseleave#events-mouseenter-and-mouseleave
*/
const hitsLeft = config.hits.difference(hits)
if (hitsLeft.size) {
bubbleIntersectionsIf('pointerleave', outgoingEvent, config.intersections, (obj: Object3D) => { return hitsLeft.has(obj) })
}
const hitsEntered = hits.difference(config.hits)
if (hitsEntered.size) {
bubbleIntersectionsIf('pointerenter', outgoingEvent, intersections, (obj: Object3D) => { return hitsEntered.has(obj) })
const hitsLeft = config.priorHits.difference(hits)
const hitsEntered = hits.difference(config.priorHits)

// NOTE: Call `pointer{leave,enter}`
/**
* Events mouseenter/mouseleave are like mouseover/mouseout.
* They trigger when the mouse pointer enters/leaves the element.
*
* But there are two important differences:
*
* - Transitions inside the element, to/from descendants, are not counted.
* - Events mouseenter/mouseleave do not bubble.
*
* https://javascript.info/mousemove-mouseover-mouseout-mouseenter-mouseleave#events-mouseenter-and-mouseleave
*/
if (hitsLeft.size) {
// NOTE: Propagate `pointerleave`
// TODO: Should use config.initialHits, not config.priorIntersections
callIntersectionObjectsIf('pointerleave', outgoingEvent, config.priorIntersections, (obj: Object3D) => { return hitsLeft.has(obj) })

// NOTE: Bubble `pointerout`
// NOTE: Should use config.initialHits, not config.priorIntersections
const duplicates = new Set()
outgoingEvent.stopped = false
for (const intersection of config.priorIntersections) {
if (outgoingEvent.stopped) { break }

let object: TresObject | null = intersection.object
if (hits.has(object) || duplicates.has(object)) { continue }

// NOTE: An event "is-a" `Intersection`,
// so copy intersection values to the event.
Object.assign(outgoingEvent, intersection)
outgoingEvent.target = object

while (object && !outgoingEvent.stopped && !duplicates.has(object)) {
outgoingEvent.eventObject = object
outgoingEvent.currentTarget = object
object.onPointerout?.(outgoingEvent)
duplicates.add(object)
object = object.parent
}
}
}

// TODO: These need to be bubbled
bubbleIntersectionsIf('pointerout', outgoingEvent, config.intersections, (obj: Object3D) => { return hitsLeft.has(obj) })
bubbleIntersectionsIf('pointerover', outgoingEvent, intersections, (obj: Object3D) => { return hitsEntered.has(obj) })
if (hitsEntered.size) {
// TODO: Should use config.initialHits, not intersections
callIntersectionObjectsIf('pointerenter', outgoingEvent, intersections, (obj: Object3D) => { return hitsEntered.has(obj) })

// NOTE: Bubble pointerover
outgoingEvent.stopped = false
const seenUUIDs: Record<string, boolean> = {}
for (const intersection of filteredIntersections) {
if (outgoingEvent.stopped) { break }

let object: TresObject | null = intersection.object
if (config.priorHits.has(object) || object.uuid in seenUUIDs) { continue }

// NOTE: An event "is-a" `Intersection`,
// so copy intersection values to the event.
Object.assign(outgoingEvent, intersection)
outgoingEvent.target = object

while (object && !outgoingEvent.stopped && !(object.uuid in seenUUIDs)) {
outgoingEvent.eventObject = object
outgoingEvent.currentTarget = object
object.onPointerover?.(outgoingEvent)
seenUUIDs[object.uuid] = true
object = object.parent
}
}
}

// NOTE: Propagate `incomingEvent.type`, e.g.:
@@ -391,18 +444,19 @@ function handleIntersections(incomingEvent: RaycastEvent, intersections: ThreeIn
// so copy intersection values to the event.
Object.assign(outgoingEvent, intersection)

outgoingEvent.currentTarget = intersection.eventObject
const currentTarget = intersection.eventObject
outgoingEvent.currentTarget = currentTarget
outgoingEvent.target = intersection.object

intersection.eventObject[DOM_TO_THREE[incomingEvent.type as DomEventName]]?.(outgoingEvent)
}
}

config.intersections = intersections
config.hits = hits
config.priorIntersections = filteredIntersections
config.priorHits = hits
}

function bubbleIntersectionsIf(domEventName: DomEventName, event: ThreeEventStub<MouseEvent>, intersections: ThreeIntersection[], cond: (a: any) => boolean) {
function callIntersectionObjectsIf(domEventName: DomEventName, event: ThreeEventStub<MouseEvent>, intersections: ThreeIntersection[], cond: (a: any) => boolean) {
const duplicates = new Set()

event.stopped = false
13 changes: 11 additions & 2 deletions vite.config.ts
Original file line number Diff line number Diff line change
@@ -6,9 +6,10 @@ import copy from 'rollup-plugin-copy'
import { defineConfig } from 'vite'
import banner from 'vite-plugin-banner'
import dts from 'vite-plugin-dts'

import Inspect from 'vite-plugin-inspect'

import { coverageConfigDefaults } from 'vitest/config'

/* import analyze from 'rollup-plugin-analyzer'
*/ /* import { visualizer } from 'rollup-plugin-visualizer' */
import { bold, gray, lightGreen, yellow } from 'kolorist'
@@ -46,7 +47,15 @@ export default defineConfig({
test: {
environment: 'jsdom',
globals: true,
threads: false,
coverage: {
provider: 'v8',
exclude: [
...coverageConfigDefaults.exclude,
'playground/**',
'docs/**',
'**/sponsorkit**/**',
],
},
},
build: {
lib: {