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

Pointer Events for mesh elements #254

Draft
wants to merge 7 commits into
base: develop
Choose a base branch
from
Draft

Conversation

trusktr
Copy link
Member

@trusktr trusktr commented Dec 1, 2022

Closes #177

Currently only CSS-rendered elements emit events, based on their rectangle shapes.

Now we add click, pointermove, etc, for mesh elements that aren't rectangles (using raycaster and manually emitting the events ourselves to emulate the same event as regular built-in elements).

We will now be able to rely on shapes of meshes for all interactions!

  • 'click'
  • 'pointermove'
  • 'pointerdown'
  • 'pointerup'
  • 'pointerenter'
  • 'pointerleave'
  • 'pointerover'
  • 'pointerout'
  • 'pointercancel' ?
  • <lume-instanced-mesh> per instance events
    • 'click'
    • 'pointermove'
    • 'pointerdown'
    • 'pointerup'
    • 'pointerenter'
    • 'pointerleave'
    • 'pointerover'
    • 'pointerout'
    • 'pointercancel' ?
  • Other events
    • convert existing events (f.e. MODEL_LOAD, GL_LOAD, etc) to DOM events, and remove usage of Eventful. Some events have DOM equivalents.
      • Emit a DOM load event for anything that loads resources, f.e. <lume-gltf-model>
    • Any other events needed?
  • documentation
  • unit tests

@trusktr trusktr marked this pull request as draft December 1, 2022 06:11
@trusktr
Copy link
Member Author

trusktr commented Dec 1, 2022

@keywizzle opened PR to track

if (intersections.length == 0) return

// Create custom event with detail of the exact XYZ position of the click
const newEvent = new CustomEvent('click', {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use MouseEvent to emulate the real event and its properties https://developer.mozilla.org/en-US/docs/Web/API/Element/click_event

detail: {position: intersections[0].point},
})
// Dispatch custom event on the LUME element saved on the intersection Three.js object
intersections[0].object.userData.lumeElement.dispatchEvent(newEvent)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will need to check up the three.js tree because there can be three.js subnodes and we need to find under which lume element those subnodes are

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not super familiar with the three.js tree, but I assume that not every three.js Object3d will have a corresponding Lume element, so should it just check if lumeElement exists on userData, and if not, check the Object3d's parent until one is found?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly, the element's .three property will be a Three.js node, and it might have Three.js children. This upward check could go in getLumeElementFromThree mentioned in the other comment.

@@ -182,6 +182,8 @@ export class ImperativeBase extends Settable(Transformable) {
// Helpful for debugging when looking in devtools.
// @prod-prune
o.name = `${this.tagName}${this.id ? '#' + this.id : ''} (webgl, ${o.type})`
// Save LUME element ref to Three.js object
o.userData.lumeElement = this
Copy link
Member Author

@trusktr trusktr Dec 1, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this we can use a Map so that it is typesafe. We'll need to eventually once we compile to WebAssembly anyway. We also need to unmap the pair on disconnect. If we use a weak map then we don't need to do the manual unmapping but assembly script doesn't support weak map yet, although we can implement it

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think just a weak map would be good

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another idea is we can make a subclass mix in or factory function that returns an instance with the lumeElement property

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would the map be stored on the scene to keep track of each Object3d's corresponding Lume element? Then when it's time to dispatch the event, the scene gets the object's element and dispatches it that way?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can just be in the top level of a module. We can export a function so that anyone can use it, for example:

import {getLumeElementFromThree} from 'lume'

getLumeElementFromThree(someThreejsObject)

and what this might do is traverse up until it finds a node associated with a Lume element using that map.

@trusktr trusktr force-pushed the scene-click-events branch 2 times, most recently from dc8cfa0 to 200cdbf Compare April 3, 2023 04:30
@trusktr trusktr changed the title Click events for Scene mouse/pointer events for mesh elements Apr 20, 2023
keywizzle and others added 6 commits May 14, 2023 21:24
-Dispatches custom event of type click on the first intersected object within a Scene
Co-authored-by: Kyle Bruce <keywizzle@users.noreply.github.com>
…so that we can stop the native events from happening on the CSS content without breaking devtools support, allowing only a single replicated event to be dispatched fot the 3D content.

Also added pointermove, and copied all the needed properties from the native events to our synthetic events.

Thanks helping think through this @keywizzle!

Co-authored-by: Kyle Bruce <keywizzle@users.noreply.github.com>
…en and cyan planes, and on things blocking those planes, and it works as expected.
}

return a
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • move to a utility file

@@ -206,6 +206,8 @@ export class SharedAPI extends DefaultBehaviors(ChildTracker(Settable(Transforma
// Helpful for debugging when looking in devtools.
// @prod-prune
o.name = `${this.tagName}${this.id ? '#' + this.id : ''} (webgl, ${o.type})`
// Save LUME element ref to Three.js object
o.userData.lumeElement = this
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • avoid using o.userData which has no types, we can use a Map or WeakMap instead.

@@ -1189,6 +1380,8 @@ export class Scene extends SharedAPI {
${super.css}

:host {
pointer-events: auto;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • I don't remember why we need to add this (or I forgot to remove it when experimenting). Let's add a comment, or remove if not needed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I saw this a while ago and don't know why. I know we messed with disabling pointer-events for CSS click events and maybe this was an artifact of that

@@ -19,161 +19,231 @@

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Move this out of test.html into its own demo

@@ -685,6 +694,8 @@ export class Scene extends SharedAPI {
override disconnectedCallback() {
super.disconnectedCallback()
this.#stopParentSizeObservation()
this.removeEventListener('click', this.#handleClick)
this.removeEventListener('pointermove', this.#handlePointermove)
Copy link
Member Author

@trusktr trusktr May 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • we don't need to explicitly remove the handlers, as event an element with its own event listeners will be GC'd fine.

@keywizzle
Copy link
Contributor

InstancedMesh pointer events are a good addition. We know the raycaster has the ability to intersect with an InstancedMesh element already, and thankfully the intersection(s) that the raycaster returns has an instanceId property, which is the index of the instance in the InstancedMesh.

Question is, what's the most user-friendly way of defining specific event listeners of an instance? An easy method might be to add a method to InstancedMesh like addInstanceEventListener or something, which takes in the index of the instance + other event stuff passed when creating an event listener. Something like:

instancedMesh.addInstanceEventListener(0, 'pointer-move', () => {
    // Do whatever
}

Maybe long term, it would be better to have an MeshInstance class of some sort to be able to control individual instances that way, but that's another can of worms on its own (although more declarative and user-friendly IMO). Would look something like:

instancedMesh.instances[0].addEventListener('pointer-move', () => {
    // Do whatever
}

At that point, we might as well add instances to the DOM as a <lume-mesh-instance> and add things like position and rotation to the MeshInstance (but then how to deal with reactivity? oh goodness). I'd imagine some interpretation of the first option is better for now.

@trusktr
Copy link
Member Author

trusktr commented Sep 3, 2023

@keywizzle

instancedMesh.addInstanceEventListener(0, 'pointer-move', () => {
    // Do whatever
}

I think initially we can start with this:

instancedMesh.addEventListener('pointermove', evt => {
    evt.instanceId
}

Where evt will be a subclass of the PointerEvent specifically for instanced meshes.

We could then provide simpler abstraction built on top of that.

That's a really good idea about the mesh instance elements. Will greatly simplify the user experience. Example:

<lume-instanced-mesh ...>
  <lume-mesh-instance number="100" position=".." color=".." ...></lume-mesh-instance>
</lume-instanced-mesh>

Where the child controls instance 100 specifically. And then:

const instance = document.querySelector('lume-mesh-instance')
instance.addEventListener('pointermove', (evt) => {
  instance.number === evt.instanceId // true

  // ...handle pointer events only for this instance...
})

Naming TBD

@trusktr trusktr changed the title mouse/pointer events for mesh elements Pointer Events for mesh elements Dec 16, 2023
@keywizzle
Copy link
Contributor

Some thoughts:

For animations, the finished event fired from Three's AnimationMixer when an animation clip is finished. Not sure what element specifically it should be fired on though.
Some of the other events that seem possible require a concept of having focus of a specific 3D element (copy/paste, keypress, drag/drop, etc.) which I could see being useful for the editor (or if anyone wanted to make their own 3D editor).

What other 3D details should be included the event? Currently it's only the world position, but three.js gives more details which we might want to include:

  • Instance index, in the case of the element being an InstancedMesh
  • Face/face index, although it would just be the three.js face so maybe we should look into defining a face within Lume
  • Distance from the ray origin and the intersection
  • Normal vector

@trusktr
Copy link
Member Author

trusktr commented May 22, 2024

For animations, the finished event fired from Three's AnimationMixer when an animation clip is finished. Not sure what element specifically it should be fired on though.
Some of the other events that seem possible require a concept of having focus of a specific 3D element (copy/paste, keypress, drag/drop, etc.) which I could see being useful for the editor (or if anyone wanted to make their own 3D editor).

These are good thoughts, although we can save this for a separate change and focus this one on the pointer events. But yeah, we'd need to choose between emitting on the animation controller element, or on the model itself, or maybe even both. Anywho let's chat about that in a new issue.

What other 3D details should be included the event? Currently it's only the world position, but three.js gives more details which we might want to include:

  • Instance index, in the case of the element being an InstancedMesh
  • Face/face index, although it would just be the three.js face so maybe we should look into defining a face within Lume
  • Distance from the ray origin and the intersection
  • Normal vector

That's true. I think first let's get basics working, then we can add more official event properties as needed in new PRs. We can dump any other properties in something like event.threeIntersections and let people do anything custom with that, and then we can determine when/if it would be nice to officially support more event properties.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Implement interaction events as standard events like pointer events (pointerdown, pointermove, click, etc)
2 participants