From b09cb679e3e5a6360972f0dc4615f1ef1c5fe98c Mon Sep 17 00:00:00 2001 From: Cody Bennett Date: Sun, 5 May 2024 17:53:48 -0500 Subject: [PATCH 1/7] feat(native): implement native EventTarget methods --- packages/fiber/src/native/Canvas.tsx | 102 ++++++++++++++++++++++----- 1 file changed, 86 insertions(+), 16 deletions(-) diff --git a/packages/fiber/src/native/Canvas.tsx b/packages/fiber/src/native/Canvas.tsx index db14e5034b..0d2f564114 100644 --- a/packages/fiber/src/native/Canvas.tsx +++ b/packages/fiber/src/native/Canvas.tsx @@ -1,6 +1,16 @@ import * as React from 'react' import * as THREE from 'three' -import { View, ViewProps, ViewStyle, LayoutChangeEvent, StyleSheet, PixelRatio } from 'react-native' +import { + View, + type ViewProps, + type ViewStyle, + type GestureResponderHandlers, + type GestureResponderEvent, + PanResponder, + type LayoutChangeEvent, + StyleSheet, + PixelRatio, +} from 'react-native' import { ExpoWebGLRenderingContext, GLView } from 'expo-gl' import { useContextBridge, FiberProvider } from 'its-fine' import { SetBlock, Block, ErrorBoundary, useMutableCallback } from '../core/utils' @@ -51,7 +61,7 @@ const CanvasImpl = /*#__PURE__*/ React.forwardRef( const [{ width, height, top, left }, setSize] = React.useState({ width: 0, height: 0, top: 0, left: 0 }) const [canvas, setCanvas] = React.useState(null) - const [bind, setBind] = React.useState() + const [bind, setBind] = React.useState() React.useImperativeHandle(forwardedRef, () => viewRef.current) const handlePointerMissed = useMutableCallback(onPointerMissed) @@ -76,21 +86,79 @@ const CanvasImpl = /*#__PURE__*/ React.forwardRef( // Called on context create or swap // https://github.com/pmndrs/react-three-fiber/pull/2297 const onContextCreate = React.useCallback((context: ExpoWebGLRenderingContext) => { - const canvasShim = { + const listeners = new Map() + + const canvas = { width: context.drawingBufferWidth, height: context.drawingBufferHeight, - style: {}, - addEventListener: (() => {}) as any, - removeEventListener: (() => {}) as any, + clientWidth: context.drawingBufferWidth, clientHeight: context.drawingBufferHeight, - getContext: ((_: any, { antialias = false }) => { + getContext: (_: any, { antialias = false }) => { setAntialias(antialias) return context - }) as any, - } as HTMLCanvasElement + }, + addEventListener(type: string, listener: EventListener) { + let callbacks = listeners.get(type) + if (!callbacks) { + callbacks = [] + listeners.set(type, callbacks) + } + + callbacks.push(listener) + }, + removeEventListener(type: string, listener: EventListener) { + const callbacks = listeners.get(type) + if (callbacks) { + const index = callbacks.indexOf(listener) + if (index !== -1) callbacks.splice(index, 1) + } + }, + dispatchEvent(event: Event) { + Object.assign(event, { target: this }) + + const callbacks = listeners.get(event.type) + if (callbacks) { + for (const callback of callbacks) { + callback(event) + } + } + }, + } as unknown as HTMLCanvasElement - root.current = createRoot(canvasShim) - setCanvas(canvasShim) + root.current = createRoot(canvas) + setCanvas(canvas) + + function handleTouch(gestureEvent: GestureResponderEvent, type: string): true { + gestureEvent.persist() + + canvas.dispatchEvent( + Object.assign(gestureEvent.nativeEvent, { + type, + offsetX: gestureEvent.nativeEvent.locationX, + offsetY: gestureEvent.nativeEvent.locationY, + }) as unknown as Event, + ) + + return true + } + + const responder = PanResponder.create({ + onStartShouldSetPanResponder: () => true, + onMoveShouldSetPanResponder: () => true, + onMoveShouldSetPanResponderCapture: () => true, + onPanResponderTerminationRequest: () => true, + onStartShouldSetPanResponderCapture: (e) => handleTouch(e, 'onPointerCapture'), + onPanResponderStart: (e) => handleTouch(e, 'onPointerDown'), + onPanResponderMove: (e) => handleTouch(e, 'onPointerMove'), + onPanResponderEnd: (e, state) => { + handleTouch(e, 'onPointerUp') + if (Math.hypot(state.dx, state.dy) < 20) handleTouch(e, 'onClick') + }, + onPanResponderRelease: (e) => handleTouch(e, 'onPointerLeave'), + onPanResponderTerminate: (e) => handleTouch(e, 'onLostPointerCapture'), + onPanResponderReject: (e) => handleTouch(e, 'onLostPointerCapture'), + }) + setBind(responder.panHandlers) }, []) if (root.current && width > 0 && height > 0) { @@ -115,9 +183,6 @@ const CanvasImpl = /*#__PURE__*/ React.forwardRef( onPointerMissed: (...args) => handlePointerMissed.current?.(...args), // Overwrite onCreated to apply RN bindings onCreated: (state: RootState) => { - // Bind events after creation - setBind(state.events.handlers) - // Bind render to RN bridge const context = state.gl.getContext() as ExpoWebGLRenderingContext const renderFrame = state.gl.render.bind(state.gl) @@ -145,9 +210,14 @@ const CanvasImpl = /*#__PURE__*/ React.forwardRef( }, [canvas]) return ( - + {width > 0 && ( - + )} ) From 55f2989011dcbf10d4337afd9314ba3b9db5088f Mon Sep 17 00:00:00 2001 From: Cody Bennett Date: Fri, 9 Aug 2024 23:58:31 -0500 Subject: [PATCH 2/7] refactor: use web events everywhere --- packages/fiber/src/core/events.ts | 60 +++++++++++++++++++++++ packages/fiber/src/index.tsx | 3 +- packages/fiber/src/native.tsx | 3 +- packages/fiber/src/native/Canvas.tsx | 20 ++++---- packages/fiber/src/native/events.ts | 71 ---------------------------- packages/fiber/src/web/Canvas.tsx | 3 +- packages/fiber/src/web/events.ts | 63 ------------------------ 7 files changed, 73 insertions(+), 150 deletions(-) delete mode 100644 packages/fiber/src/native/events.ts delete mode 100644 packages/fiber/src/web/events.ts diff --git a/packages/fiber/src/core/events.ts b/packages/fiber/src/core/events.ts index 68a8511dbd..0694892799 100644 --- a/packages/fiber/src/core/events.ts +++ b/packages/fiber/src/core/events.ts @@ -531,3 +531,63 @@ export function createEvents(store: UseBoundStore) { return { handlePointer } } + +const DOM_EVENTS = { + onClick: ['click', false], + onContextMenu: ['contextmenu', false], + onDoubleClick: ['dblclick', false], + onWheel: ['wheel', true], + onPointerDown: ['pointerdown', true], + onPointerUp: ['pointerup', true], + onPointerLeave: ['pointerleave', true], + onPointerMove: ['pointermove', true], + onPointerCancel: ['pointercancel', true], + onLostPointerCapture: ['lostpointercapture', true], +} as const + +/** Default R3F event manager for web */ +export function createPointerEvents(store: UseBoundStore): EventManager { + const { handlePointer } = createEvents(store) + + return { + priority: 1, + enabled: true, + compute(event: DomEvent, state: RootState, previous?: RootState) { + // https://github.com/pmndrs/react-three-fiber/pull/782 + // Events trigger outside of canvas when moved, use offsetX/Y by default and allow overrides + state.pointer.set((event.offsetX / state.size.width) * 2 - 1, -(event.offsetY / state.size.height) * 2 + 1) + state.raycaster.setFromCamera(state.pointer, state.camera) + }, + + connected: undefined, + handlers: Object.keys(DOM_EVENTS).reduce( + (acc, key) => ({ ...acc, [key]: handlePointer(key) }), + {}, + ) as unknown as Events, + update: () => { + const { events, internal } = store.getState() + if (internal.lastEvent?.current && events.handlers) events.handlers.onPointerMove(internal.lastEvent.current) + }, + connect: (target: HTMLElement) => { + const { set, events } = store.getState() + events.disconnect?.() + set((state) => ({ events: { ...state.events, connected: target } })) + Object.entries(events.handlers ?? []).forEach(([name, event]) => { + const [eventName, passive] = DOM_EVENTS[name as keyof typeof DOM_EVENTS] + target.addEventListener(eventName, event, { passive }) + }) + }, + disconnect: () => { + const { set, events } = store.getState() + if (events.connected) { + Object.entries(events.handlers ?? []).forEach(([name, event]) => { + if (events && events.connected instanceof HTMLElement) { + const [eventName] = DOM_EVENTS[name as keyof typeof DOM_EVENTS] + events.connected.removeEventListener(eventName, event) + } + }) + set((state) => ({ events: { ...state.events, connected: undefined } })) + } + }, + } +} diff --git a/packages/fiber/src/index.tsx b/packages/fiber/src/index.tsx index 72efc6bd33..5dece10d63 100644 --- a/packages/fiber/src/index.tsx +++ b/packages/fiber/src/index.tsx @@ -13,9 +13,8 @@ export type { RootState, } from './core/store' export type { ThreeEvent, Events, EventManager, ComputeFunction } from './core/events' -export { createEvents } from './core/events' +export { createEvents, createPointerEvents as events } from './core/events' export type { ObjectMap, Camera } from './core/utils' export * from './web/Canvas' -export { createPointerEvents as events } from './web/events' export type { GlobalRenderCallback, GlobalEffectType } from './core/loop' export * from './core' diff --git a/packages/fiber/src/native.tsx b/packages/fiber/src/native.tsx index c37809041f..9de959620f 100644 --- a/packages/fiber/src/native.tsx +++ b/packages/fiber/src/native.tsx @@ -13,10 +13,9 @@ export type { RootState, } from './core/store' export type { ThreeEvent, Events, EventManager, ComputeFunction } from './core/events' -export { createEvents } from './core/events' +export { createEvents, createPointerEvents as events } from './core/events' export type { ObjectMap, Camera } from './core/utils' export * from './native/Canvas' -export { createTouchEvents as events } from './native/events' export type { GlobalRenderCallback, GlobalEffectType } from './core/loop' export * from './core' diff --git a/packages/fiber/src/native/Canvas.tsx b/packages/fiber/src/native/Canvas.tsx index 0d2f564114..7c53e301c5 100644 --- a/packages/fiber/src/native/Canvas.tsx +++ b/packages/fiber/src/native/Canvas.tsx @@ -15,7 +15,7 @@ import { ExpoWebGLRenderingContext, GLView } from 'expo-gl' import { useContextBridge, FiberProvider } from 'its-fine' import { SetBlock, Block, ErrorBoundary, useMutableCallback } from '../core/utils' import { extend, createRoot, unmountComponentAtNode, RenderProps, ReconcilerRoot } from '../core' -import { createTouchEvents } from './events' +import { createPointerEvents } from '../core/events' import { RootState, Size } from '../core/store' export interface CanvasProps extends Omit, 'size' | 'dpr'>, ViewProps { @@ -35,7 +35,7 @@ const CanvasImpl = /*#__PURE__*/ React.forwardRef( children, style, gl, - events = createTouchEvents, + events = createPointerEvents, shadows, linear, flat, @@ -147,16 +147,16 @@ const CanvasImpl = /*#__PURE__*/ React.forwardRef( onMoveShouldSetPanResponder: () => true, onMoveShouldSetPanResponderCapture: () => true, onPanResponderTerminationRequest: () => true, - onStartShouldSetPanResponderCapture: (e) => handleTouch(e, 'onPointerCapture'), - onPanResponderStart: (e) => handleTouch(e, 'onPointerDown'), - onPanResponderMove: (e) => handleTouch(e, 'onPointerMove'), + onStartShouldSetPanResponderCapture: (e) => handleTouch(e, 'pointercapture'), + onPanResponderStart: (e) => handleTouch(e, 'pointerdown'), + onPanResponderMove: (e) => handleTouch(e, 'pointermove'), onPanResponderEnd: (e, state) => { - handleTouch(e, 'onPointerUp') - if (Math.hypot(state.dx, state.dy) < 20) handleTouch(e, 'onClick') + handleTouch(e, 'pointerup') + if (Math.hypot(state.dx, state.dy) < 20) handleTouch(e, 'click') }, - onPanResponderRelease: (e) => handleTouch(e, 'onPointerLeave'), - onPanResponderTerminate: (e) => handleTouch(e, 'onLostPointerCapture'), - onPanResponderReject: (e) => handleTouch(e, 'onLostPointerCapture'), + onPanResponderRelease: (e) => handleTouch(e, 'pointerleave'), + onPanResponderTerminate: (e) => handleTouch(e, 'lostpointercapture'), + onPanResponderReject: (e) => handleTouch(e, 'lostpointercapture'), }) setBind(responder.panHandlers) }, []) diff --git a/packages/fiber/src/native/events.ts b/packages/fiber/src/native/events.ts deleted file mode 100644 index 0e4ef1bb36..0000000000 --- a/packages/fiber/src/native/events.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { UseBoundStore } from 'zustand' -import { RootState } from '../core/store' -import { createEvents, DomEvent, EventManager, Events } from '../core/events' -import { type GestureResponderEvent, PanResponder } from 'react-native' - -/** Default R3F event manager for react-native */ -export function createTouchEvents(store: UseBoundStore): EventManager { - const { handlePointer } = createEvents(store) - - const handleTouch = (event: GestureResponderEvent, name: string): true => { - event.persist() - - // Apply offset - ;(event as any).nativeEvent.offsetX = event.nativeEvent.locationX - ;(event as any).nativeEvent.offsetY = event.nativeEvent.locationY - - // Emulate DOM event - const callback = handlePointer(name) - callback(event.nativeEvent as any) - - return true - } - - const responder = PanResponder.create({ - onStartShouldSetPanResponder: () => true, - onMoveShouldSetPanResponder: () => true, - onMoveShouldSetPanResponderCapture: () => true, - onPanResponderTerminationRequest: () => true, - onStartShouldSetPanResponderCapture: (e) => handleTouch(e, 'onPointerCapture'), - onPanResponderStart: (e) => handleTouch(e, 'onPointerDown'), - onPanResponderMove: (e) => handleTouch(e, 'onPointerMove'), - onPanResponderEnd: (e, state) => { - handleTouch(e, 'onPointerUp') - if (Math.hypot(state.dx, state.dy) < 20) handleTouch(e, 'onClick') - }, - onPanResponderRelease: (e) => handleTouch(e, 'onPointerLeave'), - onPanResponderTerminate: (e) => handleTouch(e, 'onLostPointerCapture'), - onPanResponderReject: (e) => handleTouch(e, 'onLostPointerCapture'), - }) - - return { - priority: 1, - enabled: true, - compute(event: DomEvent, state: RootState, previous?: RootState) { - // https://github.com/pmndrs/react-three-fiber/pull/782 - // Events trigger outside of canvas when moved, use offsetX/Y by default and allow overrides - state.pointer.set((event.offsetX / state.size.width) * 2 - 1, -(event.offsetY / state.size.height) * 2 + 1) - state.raycaster.setFromCamera(state.pointer, state.camera) - }, - - connected: undefined, - handlers: responder.panHandlers as unknown as Events, - update: () => { - const { events, internal } = store.getState() - if (internal.lastEvent?.current && events.handlers) { - handlePointer('onPointerMove')(internal.lastEvent.current) - } - }, - connect: () => { - const { set, events } = store.getState() - events.disconnect?.() - - set((state) => ({ events: { ...state.events, connected: true } })) - }, - disconnect: () => { - const { set } = store.getState() - - set((state) => ({ events: { ...state.events, connected: false } })) - }, - } -} diff --git a/packages/fiber/src/web/Canvas.tsx b/packages/fiber/src/web/Canvas.tsx index a25584e357..24ae8d1e6a 100644 --- a/packages/fiber/src/web/Canvas.tsx +++ b/packages/fiber/src/web/Canvas.tsx @@ -5,8 +5,7 @@ import type { Options as ResizeOptions } from 'react-use-measure' import { useContextBridge, FiberProvider } from 'its-fine' import { isRef, SetBlock, Block, ErrorBoundary, useMutableCallback, useIsomorphicLayoutEffect } from '../core/utils' import { ReconcilerRoot, extend, createRoot, unmountComponentAtNode, RenderProps } from '../core' -import { createPointerEvents } from './events' -import { DomEvent } from '../core/events' +import { createPointerEvents, DomEvent } from '../core/events' export interface CanvasProps extends Omit, 'size'>, diff --git a/packages/fiber/src/web/events.ts b/packages/fiber/src/web/events.ts deleted file mode 100644 index 0bb836c4fc..0000000000 --- a/packages/fiber/src/web/events.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { UseBoundStore } from 'zustand' -import { RootState } from '../core/store' -import { EventManager, Events, createEvents, DomEvent } from '../core/events' - -const DOM_EVENTS = { - onClick: ['click', false], - onContextMenu: ['contextmenu', false], - onDoubleClick: ['dblclick', false], - onWheel: ['wheel', true], - onPointerDown: ['pointerdown', true], - onPointerUp: ['pointerup', true], - onPointerLeave: ['pointerleave', true], - onPointerMove: ['pointermove', true], - onPointerCancel: ['pointercancel', true], - onLostPointerCapture: ['lostpointercapture', true], -} as const - -/** Default R3F event manager for web */ -export function createPointerEvents(store: UseBoundStore): EventManager { - const { handlePointer } = createEvents(store) - - return { - priority: 1, - enabled: true, - compute(event: DomEvent, state: RootState, previous?: RootState) { - // https://github.com/pmndrs/react-three-fiber/pull/782 - // Events trigger outside of canvas when moved, use offsetX/Y by default and allow overrides - state.pointer.set((event.offsetX / state.size.width) * 2 - 1, -(event.offsetY / state.size.height) * 2 + 1) - state.raycaster.setFromCamera(state.pointer, state.camera) - }, - - connected: undefined, - handlers: Object.keys(DOM_EVENTS).reduce( - (acc, key) => ({ ...acc, [key]: handlePointer(key) }), - {}, - ) as unknown as Events, - update: () => { - const { events, internal } = store.getState() - if (internal.lastEvent?.current && events.handlers) events.handlers.onPointerMove(internal.lastEvent.current) - }, - connect: (target: HTMLElement) => { - const { set, events } = store.getState() - events.disconnect?.() - set((state) => ({ events: { ...state.events, connected: target } })) - Object.entries(events.handlers ?? []).forEach(([name, event]) => { - const [eventName, passive] = DOM_EVENTS[name as keyof typeof DOM_EVENTS] - target.addEventListener(eventName, event, { passive }) - }) - }, - disconnect: () => { - const { set, events } = store.getState() - if (events.connected) { - Object.entries(events.handlers ?? []).forEach(([name, event]) => { - if (events && events.connected instanceof HTMLElement) { - const [eventName] = DOM_EVENTS[name as keyof typeof DOM_EVENTS] - events.connected.removeEventListener(eventName, event) - } - }) - set((state) => ({ events: { ...state.events, connected: undefined } })) - } - }, - } -} From fb93a1e6487007a2c50167d4d38ff6d3c8faa877 Mon Sep 17 00:00:00 2001 From: Cody Bennett Date: Sat, 10 Aug 2024 00:05:44 -0500 Subject: [PATCH 3/7] fix: restore DOM fields --- packages/fiber/src/native/Canvas.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/fiber/src/native/Canvas.tsx b/packages/fiber/src/native/Canvas.tsx index 7c53e301c5..33084f1145 100644 --- a/packages/fiber/src/native/Canvas.tsx +++ b/packages/fiber/src/native/Canvas.tsx @@ -89,6 +89,7 @@ const CanvasImpl = /*#__PURE__*/ React.forwardRef( const listeners = new Map() const canvas = { + style: {}, width: context.drawingBufferWidth, height: context.drawingBufferHeight, clientWidth: context.drawingBufferWidth, @@ -123,8 +124,15 @@ const CanvasImpl = /*#__PURE__*/ React.forwardRef( } } }, + releasePointerCapture() { + // TODO + }, } as unknown as HTMLCanvasElement + // TODO: this is wrong but necessary to trick controls + // @ts-ignore + canvas.ownerDocument = canvas + root.current = createRoot(canvas) setCanvas(canvas) From c4a662ac4f5e4d50a2f4611b9c61567aa7119f89 Mon Sep 17 00:00:00 2001 From: Cody Bennett Date: Sat, 10 Aug 2024 00:13:47 -0500 Subject: [PATCH 4/7] fix: catch event props in secondary view --- packages/fiber/src/native/Canvas.tsx | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/fiber/src/native/Canvas.tsx b/packages/fiber/src/native/Canvas.tsx index 33084f1145..3ff565fe65 100644 --- a/packages/fiber/src/native/Canvas.tsx +++ b/packages/fiber/src/native/Canvas.tsx @@ -218,15 +218,17 @@ const CanvasImpl = /*#__PURE__*/ React.forwardRef( }, [canvas]) return ( - - {width > 0 && ( - - )} + + + {width > 0 && ( + + )} + ) }, From 6d1aab54ca75d0d2499ad4cb62e78fc5136d0475 Mon Sep 17 00:00:00 2001 From: Cody Bennett Date: Sat, 10 Aug 2024 04:35:54 -0500 Subject: [PATCH 5/7] fix: missing methods for controls --- packages/fiber/src/native/Canvas.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/fiber/src/native/Canvas.tsx b/packages/fiber/src/native/Canvas.tsx index 3ff565fe65..d50c71ba85 100644 --- a/packages/fiber/src/native/Canvas.tsx +++ b/packages/fiber/src/native/Canvas.tsx @@ -124,6 +124,9 @@ const CanvasImpl = /*#__PURE__*/ React.forwardRef( } } }, + setPointerCapture() { + // TODO + }, releasePointerCapture() { // TODO }, @@ -132,6 +135,7 @@ const CanvasImpl = /*#__PURE__*/ React.forwardRef( // TODO: this is wrong but necessary to trick controls // @ts-ignore canvas.ownerDocument = canvas + canvas.getRootNode = () => canvas root.current = createRoot(canvas) setCanvas(canvas) @@ -144,6 +148,7 @@ const CanvasImpl = /*#__PURE__*/ React.forwardRef( type, offsetX: gestureEvent.nativeEvent.locationX, offsetY: gestureEvent.nativeEvent.locationY, + pointerType: 'touch', }) as unknown as Event, ) From 971ffd52f96e0c2e5c33da69c59d450043affd71 Mon Sep 17 00:00:00 2001 From: Cody Bennett Date: Sat, 10 Aug 2024 04:40:53 -0500 Subject: [PATCH 6/7] chore: restore native events export --- packages/fiber/src/core/events.ts | 60 ----------------------- packages/fiber/src/index.tsx | 3 +- packages/fiber/src/native.tsx | 4 +- packages/fiber/src/native/Canvas.tsx | 2 +- packages/fiber/src/native/events.ts | 71 ++++++++++++++++++++++++++++ packages/fiber/src/web/events.ts | 63 ++++++++++++++++++++++++ 6 files changed, 140 insertions(+), 63 deletions(-) create mode 100644 packages/fiber/src/native/events.ts create mode 100644 packages/fiber/src/web/events.ts diff --git a/packages/fiber/src/core/events.ts b/packages/fiber/src/core/events.ts index 0694892799..68a8511dbd 100644 --- a/packages/fiber/src/core/events.ts +++ b/packages/fiber/src/core/events.ts @@ -531,63 +531,3 @@ export function createEvents(store: UseBoundStore) { return { handlePointer } } - -const DOM_EVENTS = { - onClick: ['click', false], - onContextMenu: ['contextmenu', false], - onDoubleClick: ['dblclick', false], - onWheel: ['wheel', true], - onPointerDown: ['pointerdown', true], - onPointerUp: ['pointerup', true], - onPointerLeave: ['pointerleave', true], - onPointerMove: ['pointermove', true], - onPointerCancel: ['pointercancel', true], - onLostPointerCapture: ['lostpointercapture', true], -} as const - -/** Default R3F event manager for web */ -export function createPointerEvents(store: UseBoundStore): EventManager { - const { handlePointer } = createEvents(store) - - return { - priority: 1, - enabled: true, - compute(event: DomEvent, state: RootState, previous?: RootState) { - // https://github.com/pmndrs/react-three-fiber/pull/782 - // Events trigger outside of canvas when moved, use offsetX/Y by default and allow overrides - state.pointer.set((event.offsetX / state.size.width) * 2 - 1, -(event.offsetY / state.size.height) * 2 + 1) - state.raycaster.setFromCamera(state.pointer, state.camera) - }, - - connected: undefined, - handlers: Object.keys(DOM_EVENTS).reduce( - (acc, key) => ({ ...acc, [key]: handlePointer(key) }), - {}, - ) as unknown as Events, - update: () => { - const { events, internal } = store.getState() - if (internal.lastEvent?.current && events.handlers) events.handlers.onPointerMove(internal.lastEvent.current) - }, - connect: (target: HTMLElement) => { - const { set, events } = store.getState() - events.disconnect?.() - set((state) => ({ events: { ...state.events, connected: target } })) - Object.entries(events.handlers ?? []).forEach(([name, event]) => { - const [eventName, passive] = DOM_EVENTS[name as keyof typeof DOM_EVENTS] - target.addEventListener(eventName, event, { passive }) - }) - }, - disconnect: () => { - const { set, events } = store.getState() - if (events.connected) { - Object.entries(events.handlers ?? []).forEach(([name, event]) => { - if (events && events.connected instanceof HTMLElement) { - const [eventName] = DOM_EVENTS[name as keyof typeof DOM_EVENTS] - events.connected.removeEventListener(eventName, event) - } - }) - set((state) => ({ events: { ...state.events, connected: undefined } })) - } - }, - } -} diff --git a/packages/fiber/src/index.tsx b/packages/fiber/src/index.tsx index 5dece10d63..4b8af9106f 100644 --- a/packages/fiber/src/index.tsx +++ b/packages/fiber/src/index.tsx @@ -13,8 +13,9 @@ export type { RootState, } from './core/store' export type { ThreeEvent, Events, EventManager, ComputeFunction } from './core/events' -export { createEvents, createPointerEvents as events } from './core/events' +export { createEvents } from './core/events' export type { ObjectMap, Camera } from './core/utils' export * from './web/Canvas' +export { createPointerEvents as events, createPointerEvents } from './web/events' export type { GlobalRenderCallback, GlobalEffectType } from './core/loop' export * from './core' diff --git a/packages/fiber/src/native.tsx b/packages/fiber/src/native.tsx index 9de959620f..97a3a1ddc1 100644 --- a/packages/fiber/src/native.tsx +++ b/packages/fiber/src/native.tsx @@ -13,9 +13,11 @@ export type { RootState, } from './core/store' export type { ThreeEvent, Events, EventManager, ComputeFunction } from './core/events' -export { createEvents, createPointerEvents as events } from './core/events' +export { createEvents } from './core/events' export type { ObjectMap, Camera } from './core/utils' export * from './native/Canvas' +export { createTouchEvents as events } from './native/events' +export { createPointerEvents } from './web/events' export type { GlobalRenderCallback, GlobalEffectType } from './core/loop' export * from './core' diff --git a/packages/fiber/src/native/Canvas.tsx b/packages/fiber/src/native/Canvas.tsx index d50c71ba85..b39f5437a3 100644 --- a/packages/fiber/src/native/Canvas.tsx +++ b/packages/fiber/src/native/Canvas.tsx @@ -15,7 +15,7 @@ import { ExpoWebGLRenderingContext, GLView } from 'expo-gl' import { useContextBridge, FiberProvider } from 'its-fine' import { SetBlock, Block, ErrorBoundary, useMutableCallback } from '../core/utils' import { extend, createRoot, unmountComponentAtNode, RenderProps, ReconcilerRoot } from '../core' -import { createPointerEvents } from '../core/events' +import { createPointerEvents } from '../web/events' import { RootState, Size } from '../core/store' export interface CanvasProps extends Omit, 'size' | 'dpr'>, ViewProps { diff --git a/packages/fiber/src/native/events.ts b/packages/fiber/src/native/events.ts new file mode 100644 index 0000000000..0e4ef1bb36 --- /dev/null +++ b/packages/fiber/src/native/events.ts @@ -0,0 +1,71 @@ +import { UseBoundStore } from 'zustand' +import { RootState } from '../core/store' +import { createEvents, DomEvent, EventManager, Events } from '../core/events' +import { type GestureResponderEvent, PanResponder } from 'react-native' + +/** Default R3F event manager for react-native */ +export function createTouchEvents(store: UseBoundStore): EventManager { + const { handlePointer } = createEvents(store) + + const handleTouch = (event: GestureResponderEvent, name: string): true => { + event.persist() + + // Apply offset + ;(event as any).nativeEvent.offsetX = event.nativeEvent.locationX + ;(event as any).nativeEvent.offsetY = event.nativeEvent.locationY + + // Emulate DOM event + const callback = handlePointer(name) + callback(event.nativeEvent as any) + + return true + } + + const responder = PanResponder.create({ + onStartShouldSetPanResponder: () => true, + onMoveShouldSetPanResponder: () => true, + onMoveShouldSetPanResponderCapture: () => true, + onPanResponderTerminationRequest: () => true, + onStartShouldSetPanResponderCapture: (e) => handleTouch(e, 'onPointerCapture'), + onPanResponderStart: (e) => handleTouch(e, 'onPointerDown'), + onPanResponderMove: (e) => handleTouch(e, 'onPointerMove'), + onPanResponderEnd: (e, state) => { + handleTouch(e, 'onPointerUp') + if (Math.hypot(state.dx, state.dy) < 20) handleTouch(e, 'onClick') + }, + onPanResponderRelease: (e) => handleTouch(e, 'onPointerLeave'), + onPanResponderTerminate: (e) => handleTouch(e, 'onLostPointerCapture'), + onPanResponderReject: (e) => handleTouch(e, 'onLostPointerCapture'), + }) + + return { + priority: 1, + enabled: true, + compute(event: DomEvent, state: RootState, previous?: RootState) { + // https://github.com/pmndrs/react-three-fiber/pull/782 + // Events trigger outside of canvas when moved, use offsetX/Y by default and allow overrides + state.pointer.set((event.offsetX / state.size.width) * 2 - 1, -(event.offsetY / state.size.height) * 2 + 1) + state.raycaster.setFromCamera(state.pointer, state.camera) + }, + + connected: undefined, + handlers: responder.panHandlers as unknown as Events, + update: () => { + const { events, internal } = store.getState() + if (internal.lastEvent?.current && events.handlers) { + handlePointer('onPointerMove')(internal.lastEvent.current) + } + }, + connect: () => { + const { set, events } = store.getState() + events.disconnect?.() + + set((state) => ({ events: { ...state.events, connected: true } })) + }, + disconnect: () => { + const { set } = store.getState() + + set((state) => ({ events: { ...state.events, connected: false } })) + }, + } +} diff --git a/packages/fiber/src/web/events.ts b/packages/fiber/src/web/events.ts new file mode 100644 index 0000000000..0bb836c4fc --- /dev/null +++ b/packages/fiber/src/web/events.ts @@ -0,0 +1,63 @@ +import { UseBoundStore } from 'zustand' +import { RootState } from '../core/store' +import { EventManager, Events, createEvents, DomEvent } from '../core/events' + +const DOM_EVENTS = { + onClick: ['click', false], + onContextMenu: ['contextmenu', false], + onDoubleClick: ['dblclick', false], + onWheel: ['wheel', true], + onPointerDown: ['pointerdown', true], + onPointerUp: ['pointerup', true], + onPointerLeave: ['pointerleave', true], + onPointerMove: ['pointermove', true], + onPointerCancel: ['pointercancel', true], + onLostPointerCapture: ['lostpointercapture', true], +} as const + +/** Default R3F event manager for web */ +export function createPointerEvents(store: UseBoundStore): EventManager { + const { handlePointer } = createEvents(store) + + return { + priority: 1, + enabled: true, + compute(event: DomEvent, state: RootState, previous?: RootState) { + // https://github.com/pmndrs/react-three-fiber/pull/782 + // Events trigger outside of canvas when moved, use offsetX/Y by default and allow overrides + state.pointer.set((event.offsetX / state.size.width) * 2 - 1, -(event.offsetY / state.size.height) * 2 + 1) + state.raycaster.setFromCamera(state.pointer, state.camera) + }, + + connected: undefined, + handlers: Object.keys(DOM_EVENTS).reduce( + (acc, key) => ({ ...acc, [key]: handlePointer(key) }), + {}, + ) as unknown as Events, + update: () => { + const { events, internal } = store.getState() + if (internal.lastEvent?.current && events.handlers) events.handlers.onPointerMove(internal.lastEvent.current) + }, + connect: (target: HTMLElement) => { + const { set, events } = store.getState() + events.disconnect?.() + set((state) => ({ events: { ...state.events, connected: target } })) + Object.entries(events.handlers ?? []).forEach(([name, event]) => { + const [eventName, passive] = DOM_EVENTS[name as keyof typeof DOM_EVENTS] + target.addEventListener(eventName, event, { passive }) + }) + }, + disconnect: () => { + const { set, events } = store.getState() + if (events.connected) { + Object.entries(events.handlers ?? []).forEach(([name, event]) => { + if (events && events.connected instanceof HTMLElement) { + const [eventName] = DOM_EVENTS[name as keyof typeof DOM_EVENTS] + events.connected.removeEventListener(eventName, event) + } + }) + set((state) => ({ events: { ...state.events, connected: undefined } })) + } + }, + } +} From 46d81ce1d714c54a31106cd596d43542afa2e8b8 Mon Sep 17 00:00:00 2001 From: Cody Bennett Date: Sat, 10 Aug 2024 04:42:39 -0500 Subject: [PATCH 7/7] chore: cleanup --- packages/fiber/src/web/Canvas.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/fiber/src/web/Canvas.tsx b/packages/fiber/src/web/Canvas.tsx index 24ae8d1e6a..a25584e357 100644 --- a/packages/fiber/src/web/Canvas.tsx +++ b/packages/fiber/src/web/Canvas.tsx @@ -5,7 +5,8 @@ import type { Options as ResizeOptions } from 'react-use-measure' import { useContextBridge, FiberProvider } from 'its-fine' import { isRef, SetBlock, Block, ErrorBoundary, useMutableCallback, useIsomorphicLayoutEffect } from '../core/utils' import { ReconcilerRoot, extend, createRoot, unmountComponentAtNode, RenderProps } from '../core' -import { createPointerEvents, DomEvent } from '../core/events' +import { createPointerEvents } from './events' +import { DomEvent } from '../core/events' export interface CanvasProps extends Omit, 'size'>,