pmndrs / react-three-fiber

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

Pointer capture does not work in react-native #3315

Open krzys-h opened 3 months ago

krzys-h commented 3 months ago

I'm trying to implement a drag-and-drop like feature using R3F event system. It works perfectly on the web, but in react-native builds, trying to call setPointerCapture / releasePointerCapture as described in the docs (https://docs.pmnd.rs/react-three-fiber/api/events#pointer-capture) fails. Without it, onMouseMove / onMouseUp don't get properly delivered if you move the pointer too fast and it leaves the object.

Specifically, the code fails here: https://github.com/pmndrs/react-three-fiber/blob/v8.16.8/packages/fiber/src/core/events.ts#L297 https://github.com/pmndrs/react-three-fiber/blob/v8.16.8/packages/fiber/src/core/events.ts#L150

on react-native, the event.target field seems to be an integer handle of the target view, not an object, so event.target.setPointerCapture does not exist.

For testing, I patched it out like this:

diff --git a/node_modules/@react-three/fiber/dist/index-99983b2d.esm.js b/node_modules/@react-three/fiber/dist/index-99983b2d.esm.js
index d3aa0a8..606b519 100644
--- a/node_modules/@react-three/fiber/dist/index-99983b2d.esm.js
+++ b/node_modules/@react-three/fiber/dist/index-99983b2d.esm.js
@@ -835,7 +835,7 @@ function releaseInternalPointerCapture(capturedMap, obj, captures, pointerId) {
     // If this was the last capturing object for this pointer
     if (captures.size === 0) {
       capturedMap.delete(pointerId);
-      captureData.target.releasePointerCapture(pointerId);
+      if(captureData.target.releasePointerCapture) captureData.target.releasePointerCapture(pointerId);
     }
   }
 }
@@ -991,7 +991,7 @@ function createEvents(store) {
             // faster access.
             internal.capturedMap.set(id, new Map([[hit.eventObject, captureData]]));
           }
-          event.target.setPointerCapture(id);
+          if(event.target.setPointerCapture) event.target.setPointerCapture(id);
         };
         const releasePointerCapture = id => {
           const captures = internal.capturedMap.get(id);

With that change, pointer capture still didn't work, because on react-native e.pointerId is called e.identifier. This caused it to silently fail here: https://github.com/pmndrs/react-three-fiber/blob/v8.16.8/packages/fiber/src/core/events.ts#L257-L258

so as an additional hack I did this:

if (typeof event.pointerId === 'undefined')
    event.pointerId = event.identifier;

and now the pointer capture seems to work.

I would have expected this to work only partially, and break if you move the pointer out of the canvas while holding the object, but it seems that at least in my setup, react-native defaults to capturing the pointer to the view you initially clicked on. This could be some side-effect of using r3f-native-orbitcontrols though, I didn't fully test it. The "proper" way to implement it in react-native is probably to use PanResponder, which I can see is already partially done in #3252.

CodyJasonBennett commented 3 months ago

With #3252, it seems we only need the last patch with event.identifier. Can you confirm that on your end? Would happily accept a PR also if that's enough.