-
Notifications
You must be signed in to change notification settings - Fork 680
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Implement GizmoHelper, ViewportGizmo and ViewcubeGizmo (#303)
* Implemented NavigationGizmo inspired by Blender and threejs.org/editor * add alignment, margin and text color attributes * separated gizmo logic (helper) from component(s) and added ViewCubeGizmo * fixed story name * updated docs and cleanup * fixed storybook suspense error * code review * gardening * avoid double-rendering main scene * code review * docs + code review * tweaks Co-authored-by: Jens Schmidt <jens.schmidt@gmx.net>
- Loading branch information
Showing
8 changed files
with
493 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import { number, select, withKnobs } from '@storybook/addon-knobs' | ||
import * as React from 'react' | ||
import { Vector3 } from 'three' | ||
import { GizmoHelper, OrbitControls, useGLTF, GizmoViewcube } from '../../src' | ||
import { Setup } from '../Setup' | ||
|
||
export default { | ||
title: 'Gizmos/GizmoViewcube', | ||
component: GizmoViewcube, | ||
decorators: [ | ||
(storyFn) => ( | ||
<Setup controls={false} cameraPosition={new Vector3(0, 0, 10)}> | ||
{storyFn()} | ||
</Setup> | ||
), | ||
withKnobs, | ||
], | ||
} | ||
|
||
const GizmoViewcubeStory = () => { | ||
const controlsRef = React.useRef<OrbitControls>() | ||
const { scene } = useGLTF('LittlestTokyo.glb') | ||
|
||
return ( | ||
<> | ||
<primitive object={scene} scale={[0.01, 0.01, 0.01]} /> | ||
<GizmoHelper | ||
alignment={select('alignment', ['top-left', 'top-right', 'bottom-right', 'bottom-left'], 'bottom-right')} | ||
margin={[number('marginX', 80), number('marginY', 80)]} | ||
onTarget={() => controlsRef?.current?.target as Vector3} | ||
onUpdate={() => controlsRef.current?.update!()} | ||
> | ||
<GizmoViewcube /> | ||
</GizmoHelper> | ||
|
||
<OrbitControls ref={controlsRef} /> | ||
</> | ||
) | ||
} | ||
|
||
export const DefaultStory = () => ( | ||
<React.Suspense fallback={null}> | ||
<GizmoViewcubeStory /> | ||
</React.Suspense> | ||
) | ||
|
||
DefaultStory.storyName = 'Default' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import { color, number, select, withKnobs } from '@storybook/addon-knobs' | ||
import * as React from 'react' | ||
import { Vector3 } from 'three' | ||
import { GizmoViewport, GizmoHelper, OrbitControls, useGLTF } from '../../src' | ||
import { Setup } from '../Setup' | ||
|
||
export default { | ||
title: 'Gizmos/GizmoViewport', | ||
component: GizmoViewport, | ||
decorators: [ | ||
(storyFn) => ( | ||
<Setup controls={false} cameraPosition={new Vector3(0, 0, 10)}> | ||
{storyFn()} | ||
</Setup> | ||
), | ||
withKnobs, | ||
], | ||
} | ||
|
||
const GizmoViewportStory = () => { | ||
const controlsRef = React.useRef<OrbitControls>() | ||
const { scene } = useGLTF('LittlestTokyo.glb') | ||
|
||
return ( | ||
<> | ||
<primitive object={scene} scale={[0.01, 0.01, 0.01]} /> | ||
<GizmoHelper | ||
alignment={select('alignment', ['top-left', 'top-right', 'bottom-right', 'bottom-left'], 'bottom-right')} | ||
margin={[number('marginX', 80), number('marginY', 80)]} | ||
onTarget={() => controlsRef?.current?.target as Vector3} | ||
onUpdate={() => controlsRef.current?.update!()} | ||
> | ||
<GizmoViewport | ||
axisColors={[color('colorX', 'red'), color('colorY', 'green'), color('colorZ', 'blue')]} | ||
labelColor={color('labelColor', 'black')} | ||
/> | ||
</GizmoHelper> | ||
|
||
<OrbitControls ref={controlsRef} /> | ||
</> | ||
) | ||
} | ||
export const DefaultStory = () => ( | ||
<React.Suspense fallback={null}> | ||
<GizmoViewportStory /> | ||
</React.Suspense> | ||
) | ||
|
||
DefaultStory.storyName = 'Default' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
import * as React from 'react' | ||
import { createPortal, useFrame, useThree } from 'react-three-fiber' | ||
import { Camera, Group, Intersection, Matrix4, Object3D, Quaternion, Raycaster, Scene, Vector3 } from 'three' | ||
import { OrthographicCamera } from './OrthographicCamera' | ||
import { useCamera } from './useCamera' | ||
|
||
type GizmoHelperContext = { | ||
tweenCamera: (direction: Vector3) => void | ||
raycast: (raycaster: Raycaster, intersects: Intersection[]) => void | ||
} | ||
|
||
const Context = React.createContext<GizmoHelperContext>({} as GizmoHelperContext) | ||
|
||
export const useGizmoContext = () => { | ||
return React.useContext<GizmoHelperContext>(Context) | ||
} | ||
|
||
const turnRate = 2 * Math.PI // turn rate in angles per second | ||
const dummy = new Object3D() | ||
const matrix = new Matrix4() | ||
const [q1, q2] = [new Quaternion(), new Quaternion()] | ||
const target = new Vector3() | ||
const targetPosition = new Vector3() | ||
const targetQuaternion = new Quaternion() | ||
|
||
export type GizmoHelperProps = JSX.IntrinsicElements['group'] & { | ||
alignment?: 'top-left' | 'top-right' | 'bottom-right' | 'bottom-left' | ||
margin?: [number, number] | ||
onUpdate: () => void // update controls during animation | ||
onTarget: () => Vector3 // return the target to rotate around | ||
} | ||
|
||
export const GizmoHelper = ({ | ||
alignment = 'bottom-right', | ||
margin = [80, 80], | ||
onUpdate, | ||
onTarget, | ||
children: GizmoHelperComponent, | ||
}: GizmoHelperProps): any => { | ||
const { gl, camera: mainCamera, size } = useThree() | ||
const gizmoRef = React.useRef<Group>() | ||
const virtualCam = React.useRef<Camera>(null!) | ||
const [virtualScene] = React.useState(() => new Scene()) | ||
|
||
const animating = React.useRef(false) | ||
const radius = React.useRef(0) | ||
const focusPoint = React.useRef(new Vector3(0, 0, 0)) | ||
|
||
const tweenCamera = (direction: Vector3) => { | ||
animating.current = true | ||
focusPoint.current = onTarget() | ||
radius.current = mainCamera.position.distanceTo(target) | ||
|
||
// Rotate from current camera orientation | ||
dummy.position.copy(target) | ||
dummy.lookAt(mainCamera.position) | ||
q1.copy(dummy.quaternion) | ||
|
||
// To new current camera orientation | ||
targetPosition.copy(direction).multiplyScalar(radius.current).add(target) | ||
dummy.lookAt(targetPosition) | ||
q2.copy(dummy.quaternion) | ||
} | ||
|
||
const animateStep = (delta: number) => { | ||
if (!animating.current) return | ||
|
||
const step = delta * turnRate | ||
|
||
// animate position by doing a slerp and then scaling the position on the unit sphere | ||
q1.rotateTowards(q2, step) | ||
mainCamera.position.set(0, 0, 1).applyQuaternion(q1).multiplyScalar(radius.current).add(focusPoint.current) | ||
|
||
// animate orientation | ||
mainCamera.quaternion.rotateTowards(targetQuaternion, step) | ||
mainCamera.updateProjectionMatrix() | ||
onUpdate && onUpdate() | ||
|
||
if (q1.angleTo(q2) < 0.01) { | ||
animating.current = false | ||
} | ||
} | ||
|
||
const beforeRender = () => { | ||
// Sync gizmo with main camera orientation | ||
matrix.copy(mainCamera.matrix).invert() | ||
gizmoRef.current?.quaternion.setFromRotationMatrix(matrix) | ||
} | ||
|
||
useFrame((_, delta) => { | ||
if (virtualCam.current && gizmoRef.current) { | ||
animateStep(delta) | ||
beforeRender() | ||
gl.autoClear = false | ||
gl.clearDepth() | ||
gl.render(virtualScene, virtualCam.current) | ||
} | ||
}) | ||
|
||
const gizmoHelperContext = { | ||
tweenCamera, | ||
raycast: useCamera(virtualCam), | ||
} | ||
|
||
// Position gizmo component within scene | ||
const [marginX, marginY] = margin | ||
const x = alignment.endsWith('-left') ? -size.width / 2 + marginX : size.width / 2 - marginX | ||
const y = alignment.startsWith('top-') ? size.height / 2 - marginY : -size.height / 2 + marginY | ||
return createPortal( | ||
<Context.Provider value={gizmoHelperContext}> | ||
<OrthographicCamera ref={virtualCam} makeDefault={false} position={[0, 0, 100]} /> | ||
<group ref={gizmoRef} position={[x, y, 0]}> | ||
{GizmoHelperComponent} | ||
</group> | ||
</Context.Provider>, | ||
virtualScene | ||
) | ||
} |
Oops, something went wrong.
54b2953
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to the following URLs: