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
Conversation
✅ Deploy Preview for tresjs-docs ready!
To edit notification comments on pull requests, go to your Netlify site configuration. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like events are coming along! Great!
I'm not overly familiar with Tres internals, so this review is taking me a while. I'll have another look tomorrow, but I'll go ahead and submit what I've got so you can take a look when it's convenient.
@@ -43,10 +48,10 @@ export const useRaycaster = ( | |||
|
|||
raycaster.value.setFromCamera(new Vector2(x, y), camera.value) | |||
|
|||
return raycaster.value.intersectObjects(objects.value, false) | |||
return raycaster.value.intersectObjects(objects.value, true) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I understand correctly, this will test against all children of the scene and recurse down through all children.
If that's right, is that worth optimizing in this PR? Or ever?
For context, it looks like R3F does some optimization to keep some eventless objects from being tested for intersections.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct, this currently tests against all children of the scene recursively.
I'm not against optimizing the events in this PR, but overall our CPU usage is somehow lower on my system than the main
The reason we need to include the entire scene in the intersectObjects()
is because of the event propagation.
Children that do not have events are still the starting point for parents that do.
There are a few optimizations I've thought of since starting this
- Only include node's with events and their direct descendants in the hit test. Being a custom renderer gives us this flexibility to do this on scene insertion. The original solution used a similar idea to only include node's having events and blocking-objects
- Add the
.self
event modifier so a node will only listen to their own events. That let's us exclude any children of that node without events from the hit test as well - Add a
no-events
attribute that remove that object and possibly it's children from the hit test as well. - I think we're calling intersectObjects() way more than needed via the getIntersects() call. Realistically that should only need to be called on pointer-move, than any other event just grab the intersect results from there. Though I'm still not sure that are actually doing a hit test on every single event. Need to debug it more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good!
It sounds like there's plenty to think about and discuss with the others. I don't want to keep these events out of Tres. So maybe we open another PR when this PR is merged.
Only include node's with events and their direct descendants in the hit test. Being a custom renderer gives us this flexibility to do this on scene insertion.
Yeah, something like that is what I had in mind as well. We'd have to watch event updates as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After talking with @alvarosabu and @Tinoooo it looks like we have enough time before the V4 release for me to take a stab at optimizing this some more
I was able to confirm intersectObjects
is being called multiple times per frame (and even per event).
I've simplified it's usage. I'm seeing about half the cpu % in my test scenes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we could add a flag to the localstate __tres
of the instances marking if we expect it to have events to optimize the traversing of the scene. See recently merged #522
Meshes and groups:
instance.__tres.events = true
Dunno, materials and geometries:
instance.__tres.events = false
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahh good idea. The localstate __tres
will be very helpful
Thank you for calling it out!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added the EventManager onto Tres's Context
Let me know if that was what you had in mind
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
let parent = object.parent | ||
while(parent !== null && !stopPropagating) { | ||
executeEventListeners(parent[eventName], intersection, event) | ||
parent = parent.parent |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One click should probably cause no more than one call to a given click handler.
For a given click, etc., a particular click handler shouldn't be called more than once, it seems to me. I think the way it works currently will be confusing in "real" world scenarios.
Currently
This shows the result of a single click with the current bubbling setup:
The console shows that the box at the top of the pyramid gets its click handler called 4 times.
"Realer" world
The above doesn't seem wrong to me, but I was trying to think about it as a Tres user and I think it breaks down in "realer" world cases.
Here's a simple example: buy a bicycle.
<Bicycle @click="buy">
<Frame>
<Wheel />
<Wheel />
</Frame>
</Bicycle>
Positioning the canvas at a certain angle, you could click both wheels and the frame, and you'll buy 3 bicycles instead of 1.
R3F removes duplicates
I checked out R3F. They remove duplicates.
Here's a StackBlitz with a simple setup like your playground.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great catch!
I will be sure to add duplicate checks somehow
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The latest changes include duplicate checks and it's working pretty well
Thanks for catching that!
977c470
to
580029d
Compare
… to work while multiple TresCanvas' are being used
…moving intersects into a ref that is updated on pointer-move
@garrlker Regarding the store, I would suggest adding it to the |
As an aside, is there any interest in using something like Pinia for the store/ ContextR3F uses their own reactive Pinia-like store (zustand) for similar purposes. |
Meta
@alvarosabu , that answers this question from @garrlker 's source, right?
|
As of right now there's no way to replace it without modifying the core but I think we could support that long term and not need too many changes |
Ohhh @garrlker this already looks great. I just tested the demos and the different options, propagation, and especially I think we can improve the Event object that we are returning. Comparing quickly the event returned by clicking (left R3F vs right Vue): For example, returning and adding type of Event would be important: {
type: "click",
distance: ...
} For example here, I am trying to stop the propagation of the wdyt @garrlker , @andretchen0 do you see any other property that could be important for the end user? |
Here's the R3F approach in a nutshell:
https://docs.pmnd.rs/react-three-fiber/api/events That seems like a good approach to me. |
Great catch! ngl I've never liked how verbose R3F events can be, mostly because I like to log the whole events in my console when debugging. But that's more personal preference 😅 I've started working on adding more properties to the event details object. Also, I've added my personal todo list to the PR so it's easier to see what's been implemented. If you think anythings missing feel free to add more. |
Hey @garrlker I just updated the branch to latest v4, everything smooth,I'm also gonna take over docs
Regarding:
We can iterate in a follow up task after |
@garrlker Doing testing of each event I realise that the I checked it on R3F and you get an event object similar to the other events with the type of event that you missed. Could we have a similar behavior? |
Tested with primitives. Works! Great! 👍 |
@andretchen0 that was my next test, thanks for taking care of it 💚 |
No problem. 😉 Just to be clear, I tested events added on the Fwiw, here's the Vue file I set up if that's handy for anyone for double-checking primitives or my write-up.<script setup lang="ts">
import type { ThreeEvent } from '@tresjs/core'
import { TresCanvas } from '@tresjs/core'
import { BasicShadowMap, SRGBColorSpace, NoToneMapping, BoxGeometry, MeshToonMaterial, Mesh, MathUtils } from 'three'
import { TresLeches, useControls } from '@tresjs/leches'
import { OrbitControls } from '@tresjs/cientos'
import '@tresjs/leches/styles'
const gl = {
clearColor: '#202020',
shadows: true,
alpha: false,
shadowMapType: BasicShadowMap,
outputColorSpace: SRGBColorSpace,
toneMapping: NoToneMapping,
}
const { stopPropagation } = useControls({
stopPropagation: false,
})
function onClick(ev: ThreeEvent<MouseEvent>) {
console.log('click', ev)
if (stopPropagation.value) ev.stopPropagation()
ev.object.material.color.set('#008080')
}
function onDoubleClick(ev: ThreeEvent<MouseEvent>) {
console.log('double-click', ev)
if (stopPropagation.value) ev.stopPropagation()
ev.object.material.color.set('#FFD700')
}
function onPointerEnter(ev: ThreeEvent<MouseEvent>) {
if (stopPropagation.value) ev.stopPropagation()
ev.object.material.color.set('#CCFF03')
}
function onPointerLeave(ev: ThreeEvent<MouseEvent>) {
if (stopPropagation.value) ev.stopPropagation()
/* ev.object.material.color.set('#efefef') */
}
function onPointerMove(ev: ThreeEvent<MouseEvent>) {
if (stopPropagation.value) ev.stopPropagation()
}
function onContextMenu(ev: ThreeEvent<MouseEvent>) {
console.log('context-menu', ev)
if (stopPropagation.value) ev.stopPropagation()
ev.object.material.color.set('#FF4500')
}
function onPointerMissed(ev: ThreeEvent<MouseEvent>) {
console.log('pointer-missed', ev)
if (stopPropagation.value) ev.stopPropagation()
}
const SIDE = 3
const COUNT = SIDE * SIDE * SIDE
const SPREAD = 6
const [LO, HI] = [-SPREAD * 0.5, SPREAD * 0.5]
const lerp = MathUtils.lerp
const geo = new BoxGeometry()
const objs = new Array(COUNT).fill(0).map((_, i) => {
const mesh = new Mesh(geo, new MeshToonMaterial({ color: '#efefef' }))
mesh.position.set(
lerp(LO, HI, (i % SIDE) / (SIDE - 1)),
lerp(LO, HI, (Math.floor(i / SIDE) % SIDE) / (SIDE - 1)),
lerp(LO, HI, Math.floor(i / (SIDE * SIDE)) / (SIDE - 1)),
)
return mesh
})
</script>
<template>
<TresLeches />
<TresCanvas
window-size
v-bind="gl"
>
<TresPerspectiveCamera :position="[10, 10, 10]" />
<OrbitControls />
<primitive
v-for="obj, i in objs"
:key="i"
:object="obj"
@click="onClick"
@double-click="onDoubleClick"
@pointer-enter="onPointerEnter"
@pointer-leave="onPointerLeave"
@pointer-move="onPointerMove"
@context-menu="onContextMenu"
@pointer-missed="onPointerMissed"
/>
<TresDirectionalLight :intensity="1" />
<TresAmbientLight :intensity="1" />
</TresCanvas>
</template>
|
…aterial 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
…es into 515-pointer-eventmanager-state
Hey everyone, I've pushed up my latest. This includes a few fixes from the previous comments and some other features Most notably it includes a much more detailed events object for all events, including the Besides that it also includes the ability to force a raycast(events fire even when mouse isn't moving) and I fixed a major oversight I came across in the pointer-enter/leave events This should be good for more testing |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Amazing work @garrlker 👏🏻 we can finally merge it.
Events
Event Details
Features
TresCanvas
Chores