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
squash
andretchen0 committed Sep 27, 2024
commit 359c6ef8135486f1326f94766952d9b75d1ce0f0
13 changes: 8 additions & 5 deletions playground/vue/src/pages/events/PointerCapture.vue
Original file line number Diff line number Diff line change
@@ -7,15 +7,19 @@ import type { ThreeEvent } from '@tresjs/core'

const r = (radius: number) => radius * 2 * (Math.random() - 0.5)

const RADIUS = 50
const COUNT = 100
const RADIUS = 20
const COUNT = 250
const MATERIALS = ['red', 'orange', 'yellow', 'green', 'blue', 'purple'].map(s => new MeshPhongMaterial({ color: s }))
const DRAG_MATERIAL = new MeshBasicMaterial({ color: 'white' })

const positions: Vector3[] = []

for (let i = 0; i < COUNT; i++) {
positions.push(new Vector3(r(RADIUS), r(RADIUS * 0.5), r(RADIUS)))
positions.push(new Vector3(
Math.cos(i * 0.03) * RADIUS + r(10),
Math.sin(i * 0.17) * RADIUS,
Math.sin(i * 0.07 + 1.2) * RADIUS + r(5),
))
}

const isDragging = ref(false)
@@ -60,7 +64,7 @@ function onDragStop(e: ThreeEvent<any>) {
<TresCanvas window-size clear-color="gray">
<OrbitControls :enabled="!isDragging" />
<TresPerspectiveCamera
:position="[0, 75, 75]"
:position="[0, 5, 55]"
:look-at="[0, 0, 0]"
/>
<TresGroup>
@@ -69,7 +73,6 @@ function onDragStop(e: ThreeEvent<any>) {
:key="i"
:position="pos"
:material="MATERIALS[i % 6]"
:scale="2"
@pointerdown="onDragStart"
@pointermove="onDrag"
@lostpointercapture="onDragStop"
107 changes: 80 additions & 27 deletions src/utils/createEventManager/eventsRaycast.test.ts
Original file line number Diff line number Diff line change
@@ -1641,7 +1641,7 @@ describe('eventsRaycast', () => {
})
})
describe('when a pointer is captured', () => {
it('adds captured object intersections to the front of `event.intersections`', () => {
it('adds the captured object intersection to the end of `event.intersections` if it was not otherwise hit', () => {
const mock = mockTresUsingEventManagerProps()
const { m, n, o, p } = mock.add.DAG('m; n; o; p')
mock.add.eventsTo(m)
@@ -1657,37 +1657,20 @@ describe('eventsRaycast', () => {

mock.apply('pointerdown').to([m])
mock.apply('pointermove').to([n])
expect(getLast('pointermove').on(n).intersections.length).toBe(2)
expect(getLast('pointermove').on(n).intersections[0].eventObject).toBe(m)
expect(getLast('pointermove').on(n).intersections.map(intr => intr.eventObject.name).join('')).toBe('nm')

mock.apply('pointermove').to([n, o])
expect(getLast('pointermove').on(n).intersections.length).toBe(3)
expect(getLast('pointermove').on(n).intersections[0].eventObject).toBe(m)
expect(getLast('pointermove').on(n).intersections.map(intr => intr.eventObject.name).join('')).toBe('nom')

// NOTE: This adds p to pointer capture
mock.apply('pointermove').to([n, p])
expect(getLast('pointermove').on(n).intersections[0].eventObject.name).toBe('m')
expect(getLast('pointermove').on(n).intersections[1].eventObject.name).toBe('n')
expect(getLast('pointermove').on(n).intersections[2].eventObject.name).toBe('p')
expect(getLast('pointermove').on(n).intersections.map(intr => intr.eventObject.name).join('')).toBe('npm')

mock.apply('pointermove').to([n, o, m, p])
expect(getLast('pointermove').on(n).intersections.length).toBe(4)
expect(getLast('pointermove').on(n).intersections[0].eventObject.name).toBe('p')
expect(getLast('pointermove').on(n).intersections[1].eventObject.name).toBe('m')
expect(getLast('pointermove').on(n).intersections[2].eventObject.name).toBe('n')
expect(getLast('pointermove').on(n).intersections[3].eventObject.name).toBe('o')
expect(getLast('pointermove').on(n).intersections.map(intr => intr.eventObject.name).join('')).toBe('nomp')

mock.apply('pointermove').to([n, o])
expect(getLast('pointermove').on(n).intersections.length).toBe(4)
expect(getLast('pointermove').on(n).intersections[0].eventObject.name).toBe('p')
expect(getLast('pointermove').on(n).intersections[1].eventObject.name).toBe('m')
expect(getLast('pointermove').on(n).intersections[2].eventObject.name).toBe('n')
expect(getLast('pointermove').on(n).intersections[3].eventObject.name).toBe('o')

mock.apply('pointermove').to([])
expect(getLast('pointermove').on(m).intersections.length).toBe(2)
expect(getLast('pointermove').on(m).intersections[0].eventObject.name).toBe('p')
expect(getLast('pointermove').on(m).intersections[1].eventObject.name).toBe('m')
mock.apply('pointermove').to([o])
expect(getLast('pointermove').on(o).intersections.map(intr => intr.eventObject.name).join('')).toBe('omp')
})
it('calls object\'s event handlers, if they exist, even if the object is not hit', () => {
const mock = mockTresUsingEventManagerProps()
@@ -1706,6 +1689,72 @@ describe('eventsRaycast', () => {
expect(getLast('pointermove').on(g)).not.toBeNull()
expect(getLast('pointermove').on(m)).not.toBeNull()
})
it('calls object\'s event handlers, if they exist, even if `stopPropagation` is called', () => {
const mock = mockTresUsingEventManagerProps()
const { m, n, o } = mock.add.DAG('m; n; o')
mock.add.eventsTo(m)
mock.add.eventsTo(n)
mock.nodeOps.patchProp(m, 'onPointerdown', undefined, (e) => {
e.eventObject.setPointerCapture(e.pointerId)
})
mock.nodeOps.patchProp(n, 'onPointerdown', undefined, (e) => {
e.eventObject.setPointerCapture(e.pointerId)
})
mock.nodeOps.patchProp(o, 'onPointermove', undefined, (e) => {
e.stopPropagation()
})

mock.apply('pointerdown').to([m, n])

expect(getLast('pointermove').on(m)).toBeNull()
expect(getLast('pointermove').on(n)).toBeNull()
mock.apply('pointermove').to([o])
expect(getLast('pointermove').on(m)).not.toBeNull()
expect(getLast('pointermove').on(n)).not.toBeNull()
})
it('calls object\'s event handlers, if they exist, even if objects are `:blocking`', () => {
const mock = mockTresUsingEventManagerProps()
const { g, m, n, o } = mock.add.DAG('g -> m; g -> n; g -> o')
mock.add.eventsTo(m)
mock.add.eventsTo(n)
mock.nodeOps.patchProp(g, 'blocking', undefined, true)

const capture = e => e.eventObject.setPointerCapture(e.pointerId)
mock.nodeOps.patchProp(m, 'onPointerdown', undefined, capture)
mock.nodeOps.patchProp(n, 'onPointerdown', undefined, capture)

mock.apply('pointerdown').to([m, n])

expect(getLast('pointermove').on(m)).toBeNull()
expect(getLast('pointermove').on(n)).toBeNull()
mock.apply('pointermove').to([o])
expect(getLast('pointermove').on(m)).not.toBeNull()
expect(getLast('pointermove').on(n)).not.toBeNull()
})
it('bubbles events from captured objects', () => {
const mock = mockTresUsingEventManagerProps()
const { m, n, o } = mock.add.DAG('m -> n; n -> o')
mock.add.eventsTo(m)
mock.add.eventsTo(n)
mock.add.eventsTo(o)
mock.nodeOps.patchProp(o, 'onPointerdown', undefined, (e) => {
e.eventObject.setPointerCapture(e.pointerId)
})

mock.apply('pointerdown').to([o])

expect(getLast('pointermove').on(m)).toBeNull()
expect(getLast('pointermove').on(n)).toBeNull()
mock.apply('pointermove').to([])
expect(getLast('pointermove').on(m)).not.toBeNull()
expect(getLast('pointermove').on(n)).not.toBeNull()

expect(getLast('contextmenu').on(m)).toBeNull()
expect(getLast('contextmenu').on(n)).toBeNull()
mock.apply('contextmenu').to([])
expect(getLast('contextmenu').on(m)).not.toBeNull()
expect(getLast('contextmenu').on(n)).not.toBeNull()
})
it('calls pointer{over,out,enter,leave}', () => {
const mock = mockTresUsingEventManagerProps()
const { g, m, n, o } = mock.add.DAG('g -> m; n; o')
@@ -1959,7 +2008,7 @@ describe('eventsRaycast', () => {
mock.apply('pointercancel').to([])
expect(getLast('lostpointercapture').on(m)).not.toBeNull()
})
it('is called with expected event object fields', () => {
it('is called with an object having the expected fields', () => {
const mock = mockTresUsingEventManagerProps()
const { m } = mock.add.DAG('m')
mock.add.eventsTo(m)
@@ -1975,12 +2024,16 @@ describe('eventsRaycast', () => {
mock.apply('pointercancel').to([])

expect(event.type).toBe('lostpointercapture')
expect(event.target.uuid).toBe(m.uuid)
expect(event.eventObject).toBe(m)
expect(event.object).toBe(m)
expect(event.currentTarget).toBe(m)
expect(event.target).toBe(m)
})
it('is not called on objects that don\'t have the pointer capture', () => {
const mock = mockTresUsingEventManagerProps()
const { m, n } = mock.add.DAG('m; n')
mock.add.eventsTo(m, n)
mock.add.eventsTo(m)
mock.add.eventsTo(n)
mock.nodeOps.patchProp(m, 'onPointerdown', undefined, (e) => {
e.eventObject.setPointerCapture(e.pointerId)
})
23 changes: 16 additions & 7 deletions src/utils/createEventManager/eventsRaycast.ts
Original file line number Diff line number Diff line change
@@ -359,16 +359,17 @@ function handleIntersections(incomingEvent: RaycastEvent, intersections: ThreeIn
// The `hits` Set typically includes all `intersections`
// objects and their ancestors.
// 2) Create an outgoing event "stub" with the fields
// common to all events handlers will receive.
// 3) Call event handlers
// 4) Release the pointer, if necessary
// 5) `null` the event
// common to all events handlers.
// 3) Call event handlers.
// 4) Release the pointer, if necessary.
// 5) `null` the event.

// NOTE:
// 0) Add captured intersections if pointer is captured.
if ('pointerId' in incomingEvent && config.pointerToCapturedEvents.has(incomingEvent.pointerId)) {
const HAS_CAPTURED_POINTER = ('pointerId' in incomingEvent && config.pointerToCapturedEvents.has(incomingEvent.pointerId))
if (HAS_CAPTURED_POINTER) {
for (const intersection of config.pointerToCapturedEvents.get(incomingEvent.pointerId)!) {
intersections.unshift({
intersections.push({
// TODO: add rest of intersection
eventObject: intersection.currentTarget,
object: intersection.currentTarget,
@@ -495,8 +496,16 @@ function handleIntersections(incomingEvent: RaycastEvent, intersections: ThreeIn
},
)

outgoingEvent.stopPropagation = () => { outgoingEvent.stopped = true; incomingEvent.stopPropagation() }
// NOTE: If the pointer is captured, we must deliver
// pointer events to capturing objects. So we will
// turn off `stopPropagation` for Tres events.
outgoingEvent.stopPropagation = HAS_CAPTURED_POINTER
? () => { incomingEvent.stopPropagation() }
: () => { outgoingEvent.stopped = true; incomingEvent.stopPropagation() }

// NOTE: Set the `capturableEvent`. If this is left
// as falsy, it does not allow capturing within
// event handlers.
if (incomingEvent.type.startsWith('pointer') && !(incomingEvent.type === 'pointerup' || incomingEvent.type === 'pointercancel')) {
config.capturableEvent = outgoingEvent as ThreeEvent<PointerEvent>
}