From 7468e516a17c279f65b2f6a681d1aa6e655b6746 Mon Sep 17 00:00:00 2001 From: Tiago Cavalcante Trindade Date: Sat, 17 Feb 2024 19:35:14 -0300 Subject: [PATCH] v1.0.10 (#34) * feat: Add ignoreQuickPress, which defaults to false * fix: Avoid division by 0 * Update dependencies * Add prepublishOnly script in package.json * Change version to 1.0.10 and build lib --- README.md | 55 ++++++++++++++++++++++---------------- lib/index.cjs.js | 60 +++++++++++++++++++++++++++++++++++------- lib/index.d.ts | 12 ++++++--- lib/index.esm.js | 60 +++++++++++++++++++++++++++++++++++------- package.json | 29 ++++++++++---------- src/OrbitControls.tsx | 61 +++++++++++++++++++++++++++++++++++++------ 6 files changed, 212 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index 66db679..e6f3479 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ License Ziped size -OrbitControls for React Three Fiber in React Native +OrbitControls for [React Three Fiber](https://github.com/pmndrs/react-three-fiber) in React Native ## Install @@ -25,7 +25,7 @@ import useControls from "r3f-native-orbitcontrols" import { Canvas } from "@react-three/fiber/native" import { View } from "react-native" -// The import bellow is used only in Canvases: +// The import below is used only in Canvases: import { PerspectiveCamera } from "three" function SingleCanvas() { @@ -33,7 +33,7 @@ function SingleCanvas() { return ( // If this isn't working check if you have set the size of the View. - // The easiest way to do it is use the full size, e.g.: + // The easiest way to do it is to use the full size, e.g.: // @@ -71,26 +71,37 @@ function Canvases() { } ``` +You can find an example app [here](https://github.com/TiagoCavalcante/r3f-orbitcontrols-example). + ## Options The `` element _may_ receive the following properties: -| Property | Type | Description | -| :-------------- | :-------------: | ----------------------------------------------: | -| camera | Camera | readonly, available to onChange | -| enabled | boolean | | -| target | Vector3 | | -| minPolarAngle | number | how close you can orbit vertically | -| maxPolarAngle | number | how far you can orbit vertically | -| minAzimuthAngle | number | how close you can orbit horizontally | -| maxAzimuthAngle | number | how far you can orbit horizontally | -| dampingFactor | number | inertia factor | -| enableZoom | boolean | | -| zoomSpeed | number | | -| minZoom | number | | -| maxZoom | number | | -| enableRotate | boolean | | -| rotateSpeed | number | | -| enablePan | boolean | | -| panSpeed | number | | -| onChange | (event) => void | receives an event with all the properties above | +| Property | Type | Description | +| :--------------- | :-------------: | ----------------------------------------------: | +| camera | Camera | read-only, available to onChange | +| enabled | boolean | | +| target | Vector3 | | +| minPolarAngle | number | how close you can orbit vertically | +| maxPolarAngle | number | how far you can orbit vertically | +| minAzimuthAngle | number | how close you can orbit horizontally | +| maxAzimuthAngle | number | how far you can orbit horizontally | +| dampingFactor | number | inertia factor | +| enableZoom | boolean | | +| zoomSpeed | number | | +| minZoom | number | | +| maxZoom | number | | +| enableRotate | boolean | | +| rotateSpeed | number | | +| enablePan | boolean | | +| panSpeed | number | | +| ignoreQuickPress | boolean | may cause bugs when enabled\* | +| onChange | (event) => void | receives an event with all the properties above | + +You can find the defaults for each option [here](...). + +\*: This option is **not** recommended in modern devices. It's only useful in older devices, which don't propagate touch events to prevent "bubbling". You can find more information about this [here](...). + +## Why not use [drei](https://github.com/pmndrs/drei)'s OrbitControls? + +The answer is very simple: they don't work on native, only on the web and (much) older versions of [React Three Fiber](https://github.com/pmndrs/react-three-fiber). diff --git a/lib/index.cjs.js b/lib/index.cjs.js index 83a603b..299220f 100644 --- a/lib/index.cjs.js +++ b/lib/index.cjs.js @@ -78,6 +78,7 @@ var partialScope = { rotateSpeed: 1.0, enablePan: true, panSpeed: 1.0, + ignoreQuickPress: false, }; function createControls() { var height = 0; @@ -100,14 +101,49 @@ function createControls() { }; var functions = { shouldClaimTouch: function (event) { - // If there's 1 touch it may not be related to orbit-controls, - // therefore we delay "claiming" the touch. + // If there's 1 touch it may not be related to orbit controls, + // therefore we delay "claiming" the touch, as on older devices this stops the + // event propagation to prevent bubbling. + // This option is disabled by default because on newer devices (I tested on + // Android 8+ and iOS 15+) this behavior is (happily) inexistent (the + // propagation only stops if the code explicitly tells it to do so). + // See https://github.com/TiagoCavalcante/r3f-native-orbitcontrols/issues/27 + // Unfortunately, this feature may cause bugs in newer devices or browsers, + // where the first presses (quick or long) aren't detected. + // See https://github.com/TiagoCavalcante/r3f-native-orbitcontrols/issues/30 + // See https://github.com/TiagoCavalcante/r3f-native-orbitcontrols/issues/31 + // Therefore it is **not** recommended to enable it if you are targeting newer + // devices. + // There are other options to fix this behavior on older devices: + // 1. Use the events `onTouchStart`, `onTouchMove`, `onTouchEnd`, + // `onTouchCancel` from @react-three/fiber's `Canvas`. I didn't choose this + // option because it seems to be slower than using the gesture responder + // system directly, and it would also make it harder to use these events + // in the `Canvas`. + // 2. Add a transparent `Plane` that covers the whole screen and use its + // touch events, which are exposed by @react-three/fiber. I didn't choose + // this option because it would hurt performance and just seems to be too + // hacky. + // 3. Use `View`'s `onTouchStart`, `onTouchMove`, etc. I think this would have + // the same behavior in older devices, but I still didn't test it. If you + // want me to test it, please just open an issue. + // Note that using @react-three/fiber's + // `useThree().gl.domElement.addEventListener` doesn't work, just look at the + // code of the function: + // https://github.com/pmndrs/react-three-fiber/blob/6c830bd793cfd15d980299f2582f8a70cc53e30c/packages/fiber/src/native/Canvas.tsx#L83-L84 + // Ideally, this should be fixed by implementing something like an + // `addEventListener`-like in @react-three/fiber. + // I have suggested this feature here: + // https://github.com/pmndrs/react-three-fiber/issues/3173 + if (!scope.ignoreQuickPress) + return true; if (event.nativeEvent.touches.length === 1) { var _a = event.nativeEvent.touches[0], x = _a.locationX, y = _a.locationY, t = _a.timestamp; var dx = Math.abs(internals.moveStart.x - x); var dy = Math.abs(internals.moveStart.y - y); var dt = Math.pow(internals.moveStart.z - t, 2); - if (!internals.moveStart.length() || (dx * dt <= 1000 && dy * dt <= 1000)) { + if (!internals.moveStart.length() || + (dx * dt <= 1000 && dy * dt <= 1000)) { internals.moveStart.set(x, y, t); return false; } @@ -200,9 +236,12 @@ function createControls() { internals.rotateDelta .subVectors(internals.rotateEnd, internals.rotateStart) .multiplyScalar(scope.rotateSpeed); - // yes, height - this.rotateLeft((2 * Math.PI * internals.rotateDelta.x) / height); - this.rotateUp((2 * Math.PI * internals.rotateDelta.y) / height); + // Avoid division by 0. + if (height) { + // yes, height + this.rotateLeft((2 * Math.PI * internals.rotateDelta.x) / height); + this.rotateUp((2 * Math.PI * internals.rotateDelta.y) / height); + } internals.rotateStart.copy(internals.rotateEnd); }, dollyOut: function (dollyScale) { @@ -248,9 +287,12 @@ function createControls() { : // scale the zoom speed by a factor of 300 (1 / linearSquare(scope.camera.zoom)) * scope.zoomSpeed * 300; targetDistance *= Math.tan((distanceScale * Math.PI) / 180.0); - // we use only height here so aspect ratio does not distort speed - this.panLeft((2 * deltaX * targetDistance) / height, scope.camera.matrix); - this.panUp((2 * deltaY * targetDistance) / height, scope.camera.matrix); + // Avoid division by 0. + if (height) { + // we use only height here so aspect ratio does not distort speed + this.panLeft((2 * deltaX * targetDistance) / height, scope.camera.matrix); + this.panUp((2 * deltaY * targetDistance) / height, scope.camera.matrix); + } }, handleTouchMovePan: function (event) { if (event.nativeEvent.touches.length === 1) { diff --git a/lib/index.d.ts b/lib/index.d.ts index 48c4184..3eddcd7 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -20,6 +20,7 @@ declare const partialScope: { rotateSpeed: number enablePan: boolean panSpeed: number + ignoreQuickPress: boolean } declare function createControls(): { scope: { @@ -40,6 +41,7 @@ declare function createControls(): { rotateSpeed: number enablePan: boolean panSpeed: number + ignoreQuickPress: boolean } functions: { update: () => void @@ -84,12 +86,16 @@ declare function useControls(): readonly [ { onLayout(event: react_native.LayoutChangeEvent): void onStartShouldSetResponder( - event: react_native.GestureResponderEvent + event: react_native.GestureResponderEvent, ): boolean onMoveShouldSetResponder(event: react_native.GestureResponderEvent): boolean onResponderMove(event: react_native.GestureResponderEvent): void onResponderRelease(): void - } + }, ] -export { OrbitControlsChangeEvent, OrbitControlsProps, useControls as default } +export { + type OrbitControlsChangeEvent, + type OrbitControlsProps, + useControls as default, +} diff --git a/lib/index.esm.js b/lib/index.esm.js index 6c34efb..fdfde30 100644 --- a/lib/index.esm.js +++ b/lib/index.esm.js @@ -76,6 +76,7 @@ var partialScope = { rotateSpeed: 1.0, enablePan: true, panSpeed: 1.0, + ignoreQuickPress: false, }; function createControls() { var height = 0; @@ -98,14 +99,49 @@ function createControls() { }; var functions = { shouldClaimTouch: function (event) { - // If there's 1 touch it may not be related to orbit-controls, - // therefore we delay "claiming" the touch. + // If there's 1 touch it may not be related to orbit controls, + // therefore we delay "claiming" the touch, as on older devices this stops the + // event propagation to prevent bubbling. + // This option is disabled by default because on newer devices (I tested on + // Android 8+ and iOS 15+) this behavior is (happily) inexistent (the + // propagation only stops if the code explicitly tells it to do so). + // See https://github.com/TiagoCavalcante/r3f-native-orbitcontrols/issues/27 + // Unfortunately, this feature may cause bugs in newer devices or browsers, + // where the first presses (quick or long) aren't detected. + // See https://github.com/TiagoCavalcante/r3f-native-orbitcontrols/issues/30 + // See https://github.com/TiagoCavalcante/r3f-native-orbitcontrols/issues/31 + // Therefore it is **not** recommended to enable it if you are targeting newer + // devices. + // There are other options to fix this behavior on older devices: + // 1. Use the events `onTouchStart`, `onTouchMove`, `onTouchEnd`, + // `onTouchCancel` from @react-three/fiber's `Canvas`. I didn't choose this + // option because it seems to be slower than using the gesture responder + // system directly, and it would also make it harder to use these events + // in the `Canvas`. + // 2. Add a transparent `Plane` that covers the whole screen and use its + // touch events, which are exposed by @react-three/fiber. I didn't choose + // this option because it would hurt performance and just seems to be too + // hacky. + // 3. Use `View`'s `onTouchStart`, `onTouchMove`, etc. I think this would have + // the same behavior in older devices, but I still didn't test it. If you + // want me to test it, please just open an issue. + // Note that using @react-three/fiber's + // `useThree().gl.domElement.addEventListener` doesn't work, just look at the + // code of the function: + // https://github.com/pmndrs/react-three-fiber/blob/6c830bd793cfd15d980299f2582f8a70cc53e30c/packages/fiber/src/native/Canvas.tsx#L83-L84 + // Ideally, this should be fixed by implementing something like an + // `addEventListener`-like in @react-three/fiber. + // I have suggested this feature here: + // https://github.com/pmndrs/react-three-fiber/issues/3173 + if (!scope.ignoreQuickPress) + return true; if (event.nativeEvent.touches.length === 1) { var _a = event.nativeEvent.touches[0], x = _a.locationX, y = _a.locationY, t = _a.timestamp; var dx = Math.abs(internals.moveStart.x - x); var dy = Math.abs(internals.moveStart.y - y); var dt = Math.pow(internals.moveStart.z - t, 2); - if (!internals.moveStart.length() || (dx * dt <= 1000 && dy * dt <= 1000)) { + if (!internals.moveStart.length() || + (dx * dt <= 1000 && dy * dt <= 1000)) { internals.moveStart.set(x, y, t); return false; } @@ -198,9 +234,12 @@ function createControls() { internals.rotateDelta .subVectors(internals.rotateEnd, internals.rotateStart) .multiplyScalar(scope.rotateSpeed); - // yes, height - this.rotateLeft((2 * Math.PI * internals.rotateDelta.x) / height); - this.rotateUp((2 * Math.PI * internals.rotateDelta.y) / height); + // Avoid division by 0. + if (height) { + // yes, height + this.rotateLeft((2 * Math.PI * internals.rotateDelta.x) / height); + this.rotateUp((2 * Math.PI * internals.rotateDelta.y) / height); + } internals.rotateStart.copy(internals.rotateEnd); }, dollyOut: function (dollyScale) { @@ -246,9 +285,12 @@ function createControls() { : // scale the zoom speed by a factor of 300 (1 / linearSquare(scope.camera.zoom)) * scope.zoomSpeed * 300; targetDistance *= Math.tan((distanceScale * Math.PI) / 180.0); - // we use only height here so aspect ratio does not distort speed - this.panLeft((2 * deltaX * targetDistance) / height, scope.camera.matrix); - this.panUp((2 * deltaY * targetDistance) / height, scope.camera.matrix); + // Avoid division by 0. + if (height) { + // we use only height here so aspect ratio does not distort speed + this.panLeft((2 * deltaX * targetDistance) / height, scope.camera.matrix); + this.panUp((2 * deltaY * targetDistance) / height, scope.camera.matrix); + } }, handleTouchMovePan: function (event) { if (event.nativeEvent.touches.length === 1) { diff --git a/package.json b/package.json index 327863d..e59552f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "r3f-native-orbitcontrols", - "version": "1.0.9", + "version": "1.0.10", "description": "OrbitControls for React Three Fiber in React Native", "main": "lib/index.cjs.js", "module": "lib/index.esm.js", @@ -11,7 +11,8 @@ }, "scripts": { "build": "rollup -c", - "prepare": "husky install" + "prepare": "husky install", + "prepublishOnly": "npm run build" }, "keywords": [ "r3f", @@ -28,19 +29,19 @@ "three": ">=0.139.0" }, "devDependencies": { - "@rollup/plugin-commonjs": "^24.1.0", - "@rollup/plugin-node-resolve": "^15.0.2", - "@types/react": "^18.0.35", - "@types/react-native": "^0.71.5", - "@types/three": "^0.150.1", - "husky": "^8.0.3", - "lint-staged": "^13.2.1", - "prettier": "^2.8.7", - "rollup": "^3.20.3", + "@rollup/plugin-commonjs": "^25.0.7", + "@rollup/plugin-node-resolve": "^15.2.3", + "@types/react": "^18.2.56", + "@types/react-native": "^0.72.8", + "@types/three": "^0.161.2", + "husky": "^9.0.11", + "lint-staged": "^15.2.2", + "prettier": "^3.2.5", + "rollup": "^4.12.0", "rollup-plugin-delete": "^2.0.0", - "rollup-plugin-dts": "^5.3.0", - "rollup-plugin-typescript2": "^0.34.1", - "typescript": "^5.0.4" + "rollup-plugin-dts": "^6.1.0", + "rollup-plugin-typescript2": "^0.36.0", + "typescript": "^5.3.3" }, "lint-staged": { "*.{ts,tsx,md}": "prettier --write --no-semi" diff --git a/src/OrbitControls.tsx b/src/OrbitControls.tsx index 35b90d1..f8548f4 100644 --- a/src/OrbitControls.tsx +++ b/src/OrbitControls.tsx @@ -50,6 +50,8 @@ const partialScope = { enablePan: true, panSpeed: 1.0, + + ignoreQuickPress: false, } export function createControls() { @@ -82,8 +84,42 @@ export function createControls() { const functions = { shouldClaimTouch(event: GestureResponderEvent) { - // If there's 1 touch it may not be related to orbit-controls, - // therefore we delay "claiming" the touch. + // If there's 1 touch it may not be related to orbit controls, + // therefore we delay "claiming" the touch, as on older devices this stops the + // event propagation to prevent bubbling. + // This option is disabled by default because on newer devices (I tested on + // Android 8+ and iOS 15+) this behavior is (happily) inexistent (the + // propagation only stops if the code explicitly tells it to do so). + // See https://github.com/TiagoCavalcante/r3f-native-orbitcontrols/issues/27 + // Unfortunately, this feature may cause bugs in newer devices or browsers, + // where the first presses (quick or long) aren't detected. + // See https://github.com/TiagoCavalcante/r3f-native-orbitcontrols/issues/30 + // See https://github.com/TiagoCavalcante/r3f-native-orbitcontrols/issues/31 + // Therefore it is **not** recommended to enable it if you are targeting newer + // devices. + // There are other options to fix this behavior on older devices: + // 1. Use the events `onTouchStart`, `onTouchMove`, `onTouchEnd`, + // `onTouchCancel` from @react-three/fiber's `Canvas`. I didn't choose this + // option because it seems to be slower than using the gesture responder + // system directly, and it would also make it harder to use these events + // in the `Canvas`. + // 2. Add a transparent `Plane` that covers the whole screen and use its + // touch events, which are exposed by @react-three/fiber. I didn't choose + // this option because it would hurt performance and just seems to be too + // hacky. + // 3. Use `View`'s `onTouchStart`, `onTouchMove`, etc. I think this would have + // the same behavior in older devices, but I still didn't test it. If you + // want me to test it, please just open an issue. + // Note that using @react-three/fiber's + // `useThree().gl.domElement.addEventListener` doesn't work, just look at the + // code of the function: + // https://github.com/pmndrs/react-three-fiber/blob/6c830bd793cfd15d980299f2582f8a70cc53e30c/packages/fiber/src/native/Canvas.tsx#L83-L84 + // Ideally, this should be fixed by implementing something like an + // `addEventListener`-like in @react-three/fiber. + // I have suggested this feature here: + // https://github.com/pmndrs/react-three-fiber/issues/3173 + if (!scope.ignoreQuickPress) return true + if (event.nativeEvent.touches.length === 1) { const { locationX: x, @@ -220,9 +256,12 @@ export function createControls() { .subVectors(internals.rotateEnd, internals.rotateStart) .multiplyScalar(scope.rotateSpeed) - // yes, height - this.rotateLeft((2 * Math.PI * internals.rotateDelta.x) / height) - this.rotateUp((2 * Math.PI * internals.rotateDelta.y) / height) + // Avoid division by 0. + if (height) { + // yes, height + this.rotateLeft((2 * Math.PI * internals.rotateDelta.x) / height) + this.rotateUp((2 * Math.PI * internals.rotateDelta.y) / height) + } internals.rotateStart.copy(internals.rotateEnd) }, @@ -288,9 +327,15 @@ export function createControls() { targetDistance *= Math.tan((distanceScale * Math.PI) / 180.0) - // we use only height here so aspect ratio does not distort speed - this.panLeft((2 * deltaX * targetDistance) / height, scope.camera.matrix) - this.panUp((2 * deltaY * targetDistance) / height, scope.camera.matrix) + // Avoid division by 0. + if (height) { + // we use only height here so aspect ratio does not distort speed + this.panLeft( + (2 * deltaX * targetDistance) / height, + scope.camera.matrix + ) + this.panUp((2 * deltaY * targetDistance) / height, scope.camera.matrix) + } }, handleTouchMovePan(event: GestureResponderEvent) {