-
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: pivot-controls scaling * fix: reduced sphere segments count * fix: external scaling issue and scaling turbulence * fix: scaling sphere behavior with fixed prop and perspective camera * fix: only update pivot scale if its change is big enough
- Loading branch information
1 parent
297250a
commit 6611a7e
Showing
3 changed files
with
255 additions
and
18 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,214 @@ | ||
import * as React from 'react' | ||
import * as THREE from 'three' | ||
import { ThreeEvent, useThree } from '@react-three/fiber' | ||
|
||
import { Html } from '../../web/Html' | ||
import { context } from './context' | ||
import { calculateScaleFactor } from '../../core/calculateScaleFactor' | ||
|
||
const vec1 = /* @__PURE__ */ new THREE.Vector3() | ||
const vec2 = /* @__PURE__ */ new THREE.Vector3() | ||
|
||
export const calculateOffset = ( | ||
clickPoint: THREE.Vector3, | ||
normal: THREE.Vector3, | ||
rayStart: THREE.Vector3, | ||
rayDir: THREE.Vector3 | ||
) => { | ||
const e1 = normal.dot(normal) | ||
const e2 = normal.dot(clickPoint) - normal.dot(rayStart) | ||
const e3 = normal.dot(rayDir) | ||
|
||
if (e3 === 0) { | ||
return -e2 / e1 | ||
} | ||
|
||
vec1 | ||
.copy(rayDir) | ||
.multiplyScalar(e1 / e3) | ||
.sub(normal) | ||
vec2 | ||
.copy(rayDir) | ||
.multiplyScalar(e2 / e3) | ||
.add(rayStart) | ||
.sub(clickPoint) | ||
|
||
const offset = -vec1.dot(vec2) / vec1.dot(vec1) | ||
return offset | ||
} | ||
|
||
const upV = /* @__PURE__ */ new THREE.Vector3(0, 1, 0) | ||
const scaleV = /* @__PURE__ */ new THREE.Vector3() | ||
const scaleMatrix = /* @__PURE__ */ new THREE.Matrix4() | ||
|
||
export const ScalingSphere: React.FC<{ direction: THREE.Vector3; axis: 0 | 1 | 2 }> = ({ direction, axis }) => { | ||
const { | ||
scaleLimits, | ||
annotations, | ||
annotationsClass, | ||
depthTest, | ||
scale, | ||
lineWidth, | ||
fixed, | ||
axisColors, | ||
hoveredColor, | ||
opacity, | ||
onDragStart, | ||
onDrag, | ||
onDragEnd, | ||
userData, | ||
} = React.useContext(context) | ||
|
||
const size = useThree((state) => state.size) | ||
// @ts-expect-error new in @react-three/fiber@7.0.5 | ||
const camControls = useThree((state) => state.controls) as { enabled: boolean } | ||
const divRef = React.useRef<HTMLDivElement>(null!) | ||
const objRef = React.useRef<THREE.Group>(null!) | ||
const meshRef = React.useRef<THREE.Mesh>(null!) | ||
const scale0 = React.useRef<number>(1) | ||
const scaleCur = React.useRef<number>(1) | ||
const clickInfo = React.useRef<{ | ||
clickPoint: THREE.Vector3 | ||
dir: THREE.Vector3 | ||
mPLG: THREE.Matrix4 | ||
mPLGInv: THREE.Matrix4 | ||
offsetMultiplier: number | ||
} | null>(null) | ||
const [isHovered, setIsHovered] = React.useState(false) | ||
|
||
const position = fixed ? 1.2 : 1.2 * scale | ||
|
||
const onPointerDown = React.useCallback( | ||
(e: ThreeEvent<PointerEvent>) => { | ||
if (annotations) { | ||
divRef.current.innerText = `${scaleCur.current.toFixed(2)}` | ||
divRef.current.style.display = 'block' | ||
} | ||
e.stopPropagation() | ||
const rotation = new THREE.Matrix4().extractRotation(objRef.current.matrixWorld) | ||
const clickPoint = e.point.clone() | ||
const origin = new THREE.Vector3().setFromMatrixPosition(objRef.current.matrixWorld) | ||
const dir = direction.clone().applyMatrix4(rotation).normalize() | ||
const mPLG = objRef.current.matrixWorld.clone() | ||
const mPLGInv = mPLG.clone().invert() | ||
const offsetMultiplier = fixed | ||
? 1 / calculateScaleFactor(objRef.current.getWorldPosition(vec1), scale, e.camera, size) | ||
: 1 | ||
clickInfo.current = { clickPoint, dir, mPLG, mPLGInv, offsetMultiplier } | ||
onDragStart({ component: 'Sphere', axis, origin, directions: [dir] }) | ||
camControls && (camControls.enabled = false) | ||
// @ts-ignore - setPointerCapture is not in the type definition | ||
e.target.setPointerCapture(e.pointerId) | ||
}, | ||
[annotations, camControls, direction, onDragStart, axis, fixed, scale, size] | ||
) | ||
|
||
const onPointerMove = React.useCallback( | ||
(e: ThreeEvent<PointerEvent>) => { | ||
e.stopPropagation() | ||
if (!isHovered) setIsHovered(true) | ||
|
||
if (clickInfo.current) { | ||
const { clickPoint, dir, mPLG, mPLGInv, offsetMultiplier } = clickInfo.current | ||
const [min, max] = scaleLimits?.[axis] || [1e-5, undefined] // always limit the minimal value, since setting it very low might break the transform | ||
|
||
const offsetW = calculateOffset(clickPoint, dir, e.ray.origin, e.ray.direction) | ||
const offsetL = offsetW * offsetMultiplier | ||
const offsetH = fixed ? offsetL : offsetL / scale | ||
let upscale = Math.pow(2, offsetH * 0.2) | ||
|
||
// @ts-ignore | ||
if (e.shiftKey) { | ||
upscale = Math.round(upscale * 10) / 10 | ||
} | ||
|
||
upscale = Math.max(upscale, min / scale0.current) | ||
if (max !== undefined) { | ||
upscale = Math.min(upscale, max / scale0.current) | ||
} | ||
scaleCur.current = scale0.current * upscale | ||
meshRef.current.position.set(0, position + offsetL, 0) | ||
if (annotations) { | ||
divRef.current.innerText = `${scaleCur.current.toFixed(2)}` | ||
} | ||
scaleV.set(1, 1, 1) | ||
scaleV.setComponent(axis, upscale) | ||
scaleMatrix.makeScale(scaleV.x, scaleV.y, scaleV.z).premultiply(mPLG).multiply(mPLGInv) | ||
onDrag(scaleMatrix) | ||
} | ||
}, | ||
[annotations, position, onDrag, isHovered, scaleLimits, axis] | ||
) | ||
|
||
const onPointerUp = React.useCallback( | ||
(e: ThreeEvent<PointerEvent>) => { | ||
if (annotations) { | ||
divRef.current.style.display = 'none' | ||
} | ||
e.stopPropagation() | ||
scale0.current = scaleCur.current | ||
clickInfo.current = null | ||
meshRef.current.position.set(0, position, 0) | ||
onDragEnd() | ||
camControls && (camControls.enabled = true) | ||
// @ts-ignore - releasePointerCapture & PointerEvent#pointerId is not in the type definition | ||
e.target.releasePointerCapture(e.pointerId) | ||
}, | ||
[annotations, camControls, onDragEnd, position] | ||
) | ||
|
||
const onPointerOut = React.useCallback((e: ThreeEvent<PointerEvent>) => { | ||
e.stopPropagation() | ||
setIsHovered(false) | ||
}, []) | ||
|
||
const { radius, matrixL } = React.useMemo(() => { | ||
const radius = fixed ? (lineWidth / scale) * 1.8 : scale / 22.5 | ||
const quaternion = new THREE.Quaternion().setFromUnitVectors(upV, direction.clone().normalize()) | ||
const matrixL = new THREE.Matrix4().makeRotationFromQuaternion(quaternion) | ||
return { radius, matrixL } | ||
}, [direction, scale, lineWidth, fixed]) | ||
|
||
const color = isHovered ? hoveredColor : axisColors[axis] | ||
|
||
return ( | ||
<group ref={objRef}> | ||
<group | ||
matrix={matrixL} | ||
matrixAutoUpdate={false} | ||
onPointerDown={onPointerDown} | ||
onPointerMove={onPointerMove} | ||
onPointerUp={onPointerUp} | ||
onPointerOut={onPointerOut} | ||
> | ||
{annotations && ( | ||
<Html position={[0, position / 2, 0]}> | ||
<div | ||
style={{ | ||
display: 'none', | ||
background: '#151520', | ||
color: 'white', | ||
padding: '6px 8px', | ||
borderRadius: 7, | ||
whiteSpace: 'nowrap', | ||
}} | ||
className={annotationsClass} | ||
ref={divRef} | ||
/> | ||
</Html> | ||
)} | ||
<mesh ref={meshRef} position={[0, position, 0]} renderOrder={500} userData={userData}> | ||
<sphereGeometry args={[radius, 12, 12]} /> | ||
<meshBasicMaterial | ||
transparent | ||
depthTest={depthTest} | ||
color={color} | ||
opacity={opacity} | ||
polygonOffset | ||
polygonOffsetFactor={-10} | ||
/> | ||
</mesh> | ||
</group> | ||
</group> | ||
) | ||
} |
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