diff --git a/README.md b/README.md index 3d7dd2e18..bc3d4c93d 100644 --- a/README.md +++ b/README.md @@ -2312,7 +2312,13 @@ function Effects() { Views use gl.scissor to cut the viewport into segments. You tie a view to a tracking div which then controls the position and bounds of the viewport. This allows you to have multiple views with a single, performant canvas. These views will follow their tracking elements, scroll along, resize, etc. -It is advisable to re-connect the event system to a parent that contains both the canvas and the html content. This ensures that both are accessible/selectable and even allows you to mount controls or other deeper integrations into your view. +It is advisable to re-connect the event system to a parent that contains both the canvas and the html content. +This ensures that both are accessible/selectable and even allows you to mount controls or other deeper +integrations into your view. + +> Note that `@react-three/fiber` newer than `^8.1.0` is required for `View` to work correctly if the +> canvas/react three fiber root is not fullscreen. A warning will be logged if drei is used with older +> versions of `@react-three/fiber`. ```tsx ``` - -The Bounds component also acts as a context provider, use the `useBounds` hook to refresh the bounds, fit the camera, clip near/far planes or focus objects. `refresh(object?: THREE.Object3D | THREE.Box3)` will recalculate bounds. Since this can be expensive only call it when you know the view has changed. `clip` sets the cameras near/far planes. `fit` zooms and centers the view. +The Bounds component also acts as a context provider, use the `useBounds` hook to refresh the bounds, fit the camera, clip near/far planes, go to camera orientations or focus objects. `refresh(object?: THREE.Object3D | THREE.Box3)` will recalculate bounds, since this can be expensive only call it when you know the view has changed. `clip` sets the cameras near/far planes. `to` sets a position and target for the camera. `fit` zooms and centers the view. ```jsx function Foo() { @@ -2567,10 +2572,13 @@ function Foo() { useEffect(() => { // Calculate scene bounds bounds.refresh().clip().fit() + // Or, focus a specific object or box3 // bounds.refresh(ref.current).clip().fit() // bounds.refresh(new THREE.Box3()).clip().fit() + // Or, send the camera to a specific orientatin + // bounds.to({position: [0, 10, 10], target: {[5, 5, 0]}}) ``` diff --git a/src/core/Bounds.tsx b/src/core/Bounds.tsx index 9dd6856e5..edcc9fc86 100644 --- a/src/core/Bounds.tsx +++ b/src/core/Bounds.tsx @@ -94,7 +94,7 @@ export function Bounds({ children, damping = 6, fit, clip, observe, margin = 1.2 } if (controls?.constructor.name === 'OrthographicTrackballControls') { - // Put camera on a sphere along which it should moves + // Put camera on a sphere along which it should move const { distance } = getSize() const direction = camera.position.clone().sub(controls.target).normalize().multiplyScalar(distance) const newPos = controls.target.clone().add(direction) @@ -113,6 +113,25 @@ export function Bounds({ children, damping = 6, fit, clip, observe, margin = 1.2 invalidate() return this }, + to({ position, target }: { position: [number, number, number]; target?: [number, number, number] }) { + current.camera.copy(camera.position) + const { center } = getSize() + goal.camera.set(...position) + + if (target) { + goal.focus.set(...target) + } else { + goal.focus.copy(center) + } + + if (damping) { + current.animating = true + } else { + camera.position.set(...position) + } + + return this + }, fit() { current.camera.copy(camera.position) if (controls) current.focus.copy(controls.target) diff --git a/src/web/View.tsx b/src/web/View.tsx index eb40c27b3..7cdc68738 100644 --- a/src/web/View.tsx +++ b/src/web/View.tsx @@ -6,6 +6,26 @@ const isOrthographicCamera = (def: any): def is THREE.OrthographicCamera => def && (def as THREE.OrthographicCamera).isOrthographicCamera const col = new THREE.Color() +/** + * In `@react-three/fiber` after `v8.0.0` but prior to `v8.1.0`, `state.size` contained only dimension + * information. After `v8.1.0`, position information (`top`, `left`) was added + * + * @todo remove this when drei supports v9 and up + */ +type LegacyCanvasSize = { + height: number + width: number +} + +type CanvasSize = LegacyCanvasSize & { + top: number + left: number +} + +function isNonLegacyCanvasSize(size: Record): size is CanvasSize { + return 'top' in size +} + export type ContainerProps = { scene: THREE.Scene index: number @@ -13,7 +33,7 @@ export type ContainerProps = { frames: number rect: React.MutableRefObject track: React.MutableRefObject - canvasSize: Size + canvasSize: LegacyCanvasSize | CanvasSize } export type ViewProps = { @@ -27,6 +47,30 @@ export type ViewProps = { children?: React.ReactNode } +function computeContainerPosition( + canvasSize: LegacyCanvasSize | CanvasSize, + trackRect: DOMRect +): { + position: CanvasSize & { bottom: number, right: number } + isOffscreen: boolean +} { + const { right, top, left: trackLeft, bottom: trackBottom, width, height } = trackRect + const isOffscreen = trackRect.bottom < 0 || top > canvasSize.height || right < 0 || trackRect.left > canvasSize.width + + if (isNonLegacyCanvasSize(canvasSize)) { + const canvasBottom = canvasSize.top + canvasSize.height + const bottom = canvasBottom - trackBottom + const left = trackLeft - canvasSize.left + + return { position: { width, height, left, top, bottom, right }, isOffscreen } + } + + // Fall back on old behavior if r3f < 8.1.0 + const bottom = canvasSize.height - trackBottom + + return { position: { width, height, top, left: trackLeft, bottom, right }, isOffscreen } +} + function Container({ canvasSize, scene, index, children, frames, rect, track }: ContainerProps) { const get = useThree((state) => state.get) const camera = useThree((state) => state.camera) @@ -41,9 +85,11 @@ function Container({ canvasSize, scene, index, children, frames, rect, track }: } if (rect.current) { - const { left, right, top, bottom, width, height } = rect.current - const isOffscreen = bottom < 0 || top > canvasSize.height || right < 0 || left > canvasSize.width - const positiveYUpBottom = canvasSize.height - bottom + const { + position: { left, bottom, width, height }, + isOffscreen, + } = computeContainerPosition(canvasSize, rect.current) + const aspect = width / height if (isOrthographicCamera(camera)) { @@ -61,8 +107,8 @@ function Container({ canvasSize, scene, index, children, frames, rect, track }: camera.updateProjectionMatrix() } - state.gl.setViewport(left, positiveYUpBottom, width, height) - state.gl.setScissor(left, positiveYUpBottom, width, height) + state.gl.setViewport(left, bottom, width, height) + state.gl.setScissor(left, bottom, width, height) state.gl.setScissorTest(true) if (isOffscreen) { @@ -84,6 +130,16 @@ function Container({ canvasSize, scene, index, children, frames, rect, track }: return () => setEvents({ connected: old }) }, []) + React.useEffect(() => { + if (isNonLegacyCanvasSize(canvasSize)) { + return + } + console.warn( + 'Detected @react-three/fiber canvas size does not include position information. may not work as expected. ' + + 'Upgrade to @react-three/fiber ^8.1.0 for support.\n See https://github.com/pmndrs/drei/issues/944' + ) + }, []) + return <>{children} } diff --git a/yarn.lock b/yarn.lock index 0c2094c36..2b29d1cff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5043,9 +5043,9 @@ deep-is@^0.1.3, deep-is@~0.1.3: integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== deep-object-diff@^1.1.0: - version "1.1.7" - resolved "https://registry.yarnpkg.com/deep-object-diff/-/deep-object-diff-1.1.7.tgz#348b3246f426427dd633eaa50e1ed1fc2eafc7e4" - integrity sha512-QkgBca0mL08P6HiOjoqvmm6xOAl2W6CT2+34Ljhg0OeFan8cwlcdq8jrLKsBBuUFAZLsN5b6y491KdKEoSo9lg== + version "1.1.9" + resolved "https://registry.yarnpkg.com/deep-object-diff/-/deep-object-diff-1.1.9.tgz#6df7ef035ad6a0caa44479c536ed7b02570f4595" + integrity sha512-Rn+RuwkmkDwCi2/oXOFS9Gsr5lJZu/yTGpK7wAaAIE75CC+LCGEZHpY6VQJa/RoJcrmaA/docWJZvYohlNkWPA== deepmerge@^4.2.2: version "4.2.2" @@ -8199,9 +8199,9 @@ loader-runner@^2.4.0: integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw== loader-utils@^1.1.0, loader-utils@^1.2.3: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613" - integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA== + version "1.4.2" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.2.tgz#29a957f3a63973883eb684f10ffd3d151fec01a3" + integrity sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg== dependencies: big.js "^5.2.2" emojis-list "^3.0.0" @@ -8634,11 +8634,16 @@ minimist-options@4.1.0: is-plain-obj "^1.1.0" kind-of "^6.0.3" -minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: +minimist@^1.1.1, minimist@^1.2.5, minimist@^1.2.6: version "1.2.6" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== +minimist@^1.2.0: + version "1.2.7" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" + integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== + minipass-collect@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617" @@ -11291,9 +11296,9 @@ terser-webpack-plugin@^4.2.3: webpack-sources "^1.4.3" terser@^4.1.2, terser@^4.6.3: - version "4.8.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.0.tgz#63056343d7c70bb29f3af665865a46fe03a0df17" - integrity sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw== + version "4.8.1" + resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.1.tgz#a00e5634562de2239fd404c649051bf6fc21144f" + integrity sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw== dependencies: commander "^2.20.0" source-map "~0.6.1"