pmndrs / react-three-fiber

🇨🇭 A React renderer for Three.js
https://docs.pmnd.rs/react-three-fiber
MIT License
27.1k stars 1.55k forks source link

Event priority is ignored after creating a primitive from a glTF scene #3338

Open g-rauhoeft opened 3 weeks ago

g-rauhoeft commented 3 weeks ago

Hello Poimandres! Thank you for this awesome library! :)

I believe I've found a bug within the library concerning the event priority in conjunction with createPortal and glTF scenes. If I create a primitive and assign the gltf.scene as the object and add a click handler with prevent default, then add a portal in front of it containing a different object also with a click handler, the click handler of the object closest to its respective camera will be used instead of the one with the highest priority. This is because the __r3f data is only assigned to the top level of the primitive, not to the children of it's object. If a child is clicked it causes the hits array in the intersect function to be assembled based on the distance instead of the priority, because no priority can be found, because getRootState can't find the __r3f object on the child.

Here's a small reproduction of the issue: https://codesandbox.io/p/sandbox/tender-lederberg-43dr55

If you click on the ViewCube overlaying Suzanne, the click handler of Suzanne is executed, not the one of the ViewCube, which has a higher priority. The ViewCube at the bottom left works as expected, but only because Suzanne isn't behind it.

A user of the library can work around this by defining the filter function on the canvas for events, traversing the intersections upwards until a __r3f object is found and then sorting by priority, but this is inefficient, because intersect also performs a sort.

I can imagine two ways to fix this issue. I'd be happy to provide a pull request if you could let me know which fix is preferred.

  1. Instantiating a primitive traverses the passed object downwards, assigning the __r3f object to each child.
  2. getRootState traverses the object hierarchy upwards if it can't find the __r3f object on its parameter, until it finds an object that has the __r3f object.

Have a great day!

g-rauhoeft commented 3 weeks ago

Here's the user space workaround for the bug:

import { Canvas, events, LocalState, RootState } from '@react-three/fiber';

<Canvas
  events={(state) => {
    return {
      ...events(state),
      filter: (intersections) => {
        const getClosestPriority = (o: Object3D): number => {
          if ((o as any)["__r3f"] !== undefined) {
            return ((o as any)["__r3f"] as LocalState).root.getState().events
              .priority;
          } else {
            return o.parent ? getClosestPriority(o.parent) : 0;
          }
        };
        return intersections
          .sort((a, b) => {
            const aPriority = getClosestPriority(a.object);
            const bPriority = getClosestPriority(b.object);
            return bPriority - aPriority;
          });
      },
    };
  }}
>
  {...}
</Canvas>