From 7a76b8c16b27b5b40a6f6fb215d4231b67f94152 Mon Sep 17 00:00:00 2001 From: Alexander Nanberg Date: Fri, 7 Jun 2024 19:51:10 +0200 Subject: [PATCH] Migrate to props.ref --- packages/fiber/__mocks__/react-native.ts | 34 +- packages/fiber/src/native/Canvas.tsx | 385 +++++++++++------------ packages/fiber/src/web/Canvas.tsx | 63 ++-- 3 files changed, 236 insertions(+), 246 deletions(-) diff --git a/packages/fiber/__mocks__/react-native.ts b/packages/fiber/__mocks__/react-native.ts index 129eed47d5..a43031723c 100644 --- a/packages/fiber/__mocks__/react-native.ts +++ b/packages/fiber/__mocks__/react-native.ts @@ -1,27 +1,25 @@ import * as React from 'react' -import { ViewProps, LayoutChangeEvent } from 'react-native' +import { ViewProps, LayoutChangeEvent, View as RNView } from 'react-native' // Mocks a View or container as React sees it -const Container = React.memo( - React.forwardRef(({ onLayout, ...props }: ViewProps, ref) => { - React.useLayoutEffect(() => { - onLayout?.({ - nativeEvent: { - layout: { - x: 0, - y: 0, - width: 1280, - height: 800, - }, +const Container = React.memo(({ onLayout, ...props }: ViewProps) => { + React.useLayoutEffect(() => { + onLayout?.({ + nativeEvent: { + layout: { + x: 0, + y: 0, + width: 1280, + height: 800, }, - } as LayoutChangeEvent) - }, [onLayout]) + }, + } as LayoutChangeEvent) + }, [onLayout]) - React.useImperativeHandle(ref, () => props) + // React.useImperativeHandle(ref, () => props) - return null - }), -) + return null +}) export const View = Container export const Pressable = Container diff --git a/packages/fiber/src/native/Canvas.tsx b/packages/fiber/src/native/Canvas.tsx index 6b9f9e5021..acbeb41c82 100644 --- a/packages/fiber/src/native/Canvas.tsx +++ b/packages/fiber/src/native/Canvas.tsx @@ -24,21 +24,158 @@ const _View = View as any export interface CanvasProps extends Omit, 'size' | 'dpr'>, Omit { children: React.ReactNode style?: ViewStyle + ref?: React.Ref } export interface Props extends CanvasProps {} -/** - * A native canvas which accepts threejs elements as children. - * @see https://docs.pmnd.rs/react-three-fiber/api/canvas - */ -const CanvasImpl = /*#__PURE__*/ React.forwardRef( - ( - { - children, - style, +/*#__PURE__*/ function CanvasImpl({ + children, + style, + gl, + events = createPointerEvents, + shadows, + linear, + flat, + legacy, + orthographic, + frameloop, + performance, + raycaster, + camera, + scene, + onPointerMissed, + onCreated, + ref, + ...props +}: Props) { + // Create a known catalogue of Threejs-native elements + // This will include the entire THREE namespace by default, users can extend + // their own elements by using the createRoot API instead + React.useMemo(() => extend(THREE as any), []) + + const Bridge = useBridge() + + 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() + React.useImperativeHandle(ref, () => viewRef.current) + + const handlePointerMissed = useMutableCallback(onPointerMissed) + const [block, setBlock] = React.useState(false) + const [error, setError] = React.useState(undefined) + + // Suspend this component if block is a promise (2nd run) + if (block) throw block + // Throw exception outwards if anything within canvas throws + if (error) throw error + + const viewRef = React.useRef(null!) + const root = React.useRef>(null!) + + const [antialias, setAntialias] = React.useState(true) + + const onLayout = React.useCallback((e: LayoutChangeEvent) => { + const { width, height, x, y } = e.nativeEvent.layout + setSize({ width, height, top: y, left: x }) + }, []) + + // Called on context create or swap + // https://github.com/pmndrs/react-three-fiber/pull/2297 + const onContextCreate = React.useCallback((context: ExpoWebGLRenderingContext) => { + const listeners = new Map() + + const canvas = { + style: {}, + width: context.drawingBufferWidth, + height: context.drawingBufferHeight, + clientWidth: context.drawingBufferWidth, + clientHeight: context.drawingBufferHeight, + getContext: (_: any, { antialias = false }) => { + setAntialias(antialias) + return context + }, + 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) + } + } + }, + setPointerCapture() { + // TODO + }, + releasePointerCapture() { + // TODO + }, + } as unknown as HTMLCanvasElement + + // TODO: this is wrong but necessary to trick controls + // @ts-ignore + canvas.ownerDocument = canvas + canvas.getRootNode = () => canvas + + 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, + pointerType: 'touch', + pointerId: gestureEvent.nativeEvent.identifier, + }) as unknown as Event, + ) + + return true + } + + const responder = PanResponder.create({ + onStartShouldSetPanResponder: () => true, + onMoveShouldSetPanResponder: () => true, + onMoveShouldSetPanResponderCapture: () => true, + onPanResponderTerminationRequest: () => true, + onStartShouldSetPanResponderCapture: (e) => handleTouch(e, 'pointercapture'), + onPanResponderStart: (e) => handleTouch(e, 'pointerdown'), + onPanResponderMove: (e) => handleTouch(e, 'pointermove'), + onPanResponderEnd: (e, state) => { + handleTouch(e, 'pointerup') + if (Math.hypot(state.dx, state.dy) < 20) handleTouch(e, 'click') + }, + onPanResponderRelease: (e) => handleTouch(e, 'pointerleave'), + onPanResponderTerminate: (e) => handleTouch(e, 'lostpointercapture'), + onPanResponderReject: (e) => handleTouch(e, 'lostpointercapture'), + }) + setBind(responder.panHandlers) + }, []) + + if (root.current && width > 0 && height > 0) { + root.current.configure({ gl, - events = createPointerEvents, + events, shadows, linear, flat, @@ -49,201 +186,57 @@ const CanvasImpl = /*#__PURE__*/ React.forwardRef( raycaster, camera, scene, - onPointerMissed, - onCreated, - ...props - }, - forwardedRef, - ) => { - // Create a known catalogue of Threejs-native elements - // This will include the entire THREE namespace by default, users can extend - // their own elements by using the createRoot API instead - React.useMemo(() => extend(THREE as any), []) - - const Bridge = useBridge() - - 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() - React.useImperativeHandle(forwardedRef, () => viewRef.current) - - const handlePointerMissed = useMutableCallback(onPointerMissed) - const [block, setBlock] = React.useState(false) - const [error, setError] = React.useState(undefined) - - // Suspend this component if block is a promise (2nd run) - if (block) throw block - // Throw exception outwards if anything within canvas throws - if (error) throw error - - const viewRef = React.useRef(null!) - const root = React.useRef>(null!) - - const [antialias, setAntialias] = React.useState(true) - - const onLayout = React.useCallback((e: LayoutChangeEvent) => { - const { width, height, x, y } = e.nativeEvent.layout - setSize({ width, height, top: y, left: x }) - }, []) - - // Called on context create or swap - // https://github.com/pmndrs/react-three-fiber/pull/2297 - const onContextCreate = React.useCallback((context: ExpoWebGLRenderingContext) => { - const listeners = new Map() - - const canvas = { - style: {}, - width: context.drawingBufferWidth, - height: context.drawingBufferHeight, - clientWidth: context.drawingBufferWidth, - clientHeight: context.drawingBufferHeight, - getContext: (_: any, { antialias = false }) => { - setAntialias(antialias) - return context - }, - 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) - } - } - }, - setPointerCapture() { - // TODO - }, - releasePointerCapture() { - // TODO - }, - } as unknown as HTMLCanvasElement - - // TODO: this is wrong but necessary to trick controls - // @ts-ignore - canvas.ownerDocument = canvas - canvas.getRootNode = () => canvas - - 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, - pointerType: 'touch', - pointerId: gestureEvent.nativeEvent.identifier, - }) as unknown as Event, - ) - - return true - } - - const responder = PanResponder.create({ - onStartShouldSetPanResponder: () => true, - onMoveShouldSetPanResponder: () => true, - onMoveShouldSetPanResponderCapture: () => true, - onPanResponderTerminationRequest: () => true, - onStartShouldSetPanResponderCapture: (e) => handleTouch(e, 'pointercapture'), - onPanResponderStart: (e) => handleTouch(e, 'pointerdown'), - onPanResponderMove: (e) => handleTouch(e, 'pointermove'), - onPanResponderEnd: (e, state) => { - handleTouch(e, 'pointerup') - if (Math.hypot(state.dx, state.dy) < 20) handleTouch(e, 'click') - }, - onPanResponderRelease: (e) => handleTouch(e, 'pointerleave'), - onPanResponderTerminate: (e) => handleTouch(e, 'lostpointercapture'), - onPanResponderReject: (e) => handleTouch(e, 'lostpointercapture'), - }) - setBind(responder.panHandlers) - }, []) - - if (root.current && width > 0 && height > 0) { - root.current.configure({ - gl, - events, - shadows, - linear, - flat, - legacy, - orthographic, - frameloop, - performance, - raycaster, - camera, - scene, - // expo-gl can only render at native dpr/resolution - // https://github.com/expo/expo-three/issues/39 - dpr: PixelRatio.get(), - size: { width, height, top, left }, - // Pass mutable reference to onPointerMissed so it's free to update - onPointerMissed: (...args) => handlePointerMissed.current?.(...args), - // Overwrite onCreated to apply RN bindings - onCreated: (state: RootState) => { - // Bind render to RN bridge - const context = state.gl.getContext() as ExpoWebGLRenderingContext - const renderFrame = state.gl.render.bind(state.gl) - state.gl.render = (scene: THREE.Scene, camera: THREE.Camera) => { - renderFrame(scene, camera) - context.endFrameEXP() - } + // expo-gl can only render at native dpr/resolution + // https://github.com/expo/expo-three/issues/39 + dpr: PixelRatio.get(), + size: { width, height, top, left }, + // Pass mutable reference to onPointerMissed so it's free to update + onPointerMissed: (...args) => handlePointerMissed.current?.(...args), + // Overwrite onCreated to apply RN bindings + onCreated: (state: RootState) => { + // Bind render to RN bridge + const context = state.gl.getContext() as ExpoWebGLRenderingContext + const renderFrame = state.gl.render.bind(state.gl) + state.gl.render = (scene: THREE.Scene, camera: THREE.Camera) => { + renderFrame(scene, camera) + context.endFrameEXP() + } + + return onCreated?.(state) + }, + }) + root.current.render( + + + }>{children} + + , + ) + } - return onCreated?.(state) - }, - }) - root.current.render( - - - }>{children} - - , - ) + React.useEffect(() => { + if (canvas) { + return () => unmountComponentAtNode(canvas!) } + }, [canvas]) - React.useEffect(() => { - if (canvas) { - return () => unmountComponentAtNode(canvas!) - } - }, [canvas]) - - return ( - <_View {...props} ref={viewRef} onLayout={onLayout} style={{ flex: 1, ...style }} {...bind}> - {width > 0 && ( - - )} - - ) - }, -) + return ( + <_View {...props} ref={viewRef} onLayout={onLayout} style={{ flex: 1, ...style }} {...bind}> + {width > 0 && ( + + )} + + ) +} /** * A native canvas which accepts threejs elements as children. * @see https://docs.pmnd.rs/react-three-fiber/api/canvas */ -export const Canvas = React.forwardRef(function CanvasWrapper(props, ref) { +export function Canvas(props: CanvasProps) { return ( - + ) -}) +} diff --git a/packages/fiber/src/web/Canvas.tsx b/packages/fiber/src/web/Canvas.tsx index 0fb9d20104..a869cdcda3 100644 --- a/packages/fiber/src/web/Canvas.tsx +++ b/packages/fiber/src/web/Canvas.tsx @@ -19,6 +19,7 @@ export interface CanvasProps extends Omit, 'size'>, React.HTMLAttributes { children: React.ReactNode + ref?: React.Ref /** Canvas fallback content, similar to img's alt prop */ fallback?: React.ReactNode /** @@ -34,33 +35,31 @@ export interface CanvasProps export interface Props extends CanvasProps {} -const CanvasImpl = /*#__PURE__*/ React.forwardRef(function Canvas( - { - children, - fallback, - resize, - style, - gl, - events = createPointerEvents, - eventSource, - eventPrefix, - shadows, - linear, - flat, - legacy, - orthographic, - frameloop, - dpr, - performance, - raycaster, - camera, - scene, - onPointerMissed, - onCreated, - ...props - }, - forwardedRef, -) { +/*#__PURE__*/ function CanvasImpl({ + ref, + children, + fallback, + resize, + style, + gl, + events = createPointerEvents, + eventSource, + eventPrefix, + shadows, + linear, + flat, + legacy, + orthographic, + frameloop, + dpr, + performance, + raycaster, + camera, + scene, + onPointerMissed, + onCreated, + ...props +}: Props) { // Create a known catalogue of Threejs-native elements // This will include the entire THREE namespace by default, users can extend // their own elements by using the createRoot API instead @@ -71,7 +70,7 @@ const CanvasImpl = /*#__PURE__*/ React.forwardRef(func const [containerRef, containerRect] = useMeasure({ scroll: true, debounce: { scroll: 50, resize: 0 }, ...resize }) const canvasRef = React.useRef(null!) const divRef = React.useRef(null!) - React.useImperativeHandle(forwardedRef, () => canvasRef.current) + React.useImperativeHandle(ref, () => canvasRef.current) const handlePointerMissed = useMutableCallback(onPointerMissed) const [block, setBlock] = React.useState(false) @@ -163,16 +162,16 @@ const CanvasImpl = /*#__PURE__*/ React.forwardRef(func ) -}) +} /** * A DOM canvas which accepts threejs elements as children. * @see https://docs.pmnd.rs/react-three-fiber/api/canvas */ -export const Canvas = React.forwardRef(function CanvasWrapper(props, ref) { +export function Canvas(props: CanvasProps) { return ( - + ) -}) +}