Skip to content

Commit

Permalink
feat: PivotControls scaling (#1249)
Browse files Browse the repository at this point in the history
* 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
STINGNAILS committed Mar 7, 2024
1 parent 297250a commit 6611a7e
Show file tree
Hide file tree
Showing 3 changed files with 255 additions and 18 deletions.
214 changes: 214 additions & 0 deletions src/web/pivotControls/ScalingSphere.tsx
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>
)
}
3 changes: 2 additions & 1 deletion src/web/pivotControls/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as THREE from 'three'
import * as React from 'react'

export type OnDragStartProps = {
component: 'Arrow' | 'Slider' | 'Rotator'
component: 'Arrow' | 'Slider' | 'Rotator' | 'Sphere'
axis: 0 | 1 | 2
origin: THREE.Vector3
directions: THREE.Vector3[]
Expand All @@ -15,6 +15,7 @@ export type PivotContext = {
translation: { current: [number, number, number] }
translationLimits?: [[number, number] | undefined, [number, number] | undefined, [number, number] | undefined]
rotationLimits?: [[number, number] | undefined, [number, number] | undefined, [number, number] | undefined]
scaleLimits?: [[number, number] | undefined, [number, number] | undefined, [number, number] | undefined]
axisColors: [string | number, string | number, string | number]
hoveredColor: string | number
opacity: number
Expand Down
56 changes: 39 additions & 17 deletions src/web/pivotControls/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ForwardRefComponent } from '../../helpers/ts-utils'
import { AxisArrow } from './AxisArrow'
import { AxisRotator } from './AxisRotator'
import { PlaneSlider } from './PlaneSlider'
import { ScalingSphere } from './ScalingSphere'
import { OnDragStartProps, context } from './context'
import { calculateScaleFactor } from '../../core/calculateScaleFactor'

Expand All @@ -17,13 +18,15 @@ const mW = /* @__PURE__ */ new THREE.Matrix4()
const mL = /* @__PURE__ */ new THREE.Matrix4()
const mL0Inv = /* @__PURE__ */ new THREE.Matrix4()
const mdL = /* @__PURE__ */ new THREE.Matrix4()
const mG = /* @__PURE__ */ new THREE.Matrix4()

const bb = /* @__PURE__ */ new THREE.Box3()
const bbObj = /* @__PURE__ */ new THREE.Box3()
const vCenter = /* @__PURE__ */ new THREE.Vector3()
const vSize = /* @__PURE__ */ new THREE.Vector3()
const vAnchorOffset = /* @__PURE__ */ new THREE.Vector3()
const vPosition = /* @__PURE__ */ new THREE.Vector3()
const vScale = /* @__PURE__ */ new THREE.Vector3()

const xDir = /* @__PURE__ */ new THREE.Vector3(1, 0, 0)
const yDir = /* @__PURE__ */ new THREE.Vector3(0, 1, 0)
Expand Down Expand Up @@ -53,10 +56,12 @@ type PivotControlsProps = {
disableAxes?: boolean
disableSliders?: boolean
disableRotations?: boolean
disableScaling?: boolean

/** Limits */
translationLimits?: [[number, number] | undefined, [number, number] | undefined, [number, number] | undefined]
rotationLimits?: [[number, number] | undefined, [number, number] | undefined, [number, number] | undefined]
scaleLimits?: [[number, number] | undefined, [number, number] | undefined, [number, number] | undefined]

/** RGB colors */
axisColors?: [string | number, string | number, string | number]
Expand Down Expand Up @@ -95,6 +100,7 @@ export const PivotControls: ForwardRefComponent<PivotControlsProps, THREE.Group>
disableAxes = false,
disableSliders = false,
disableRotations = false,
disableScaling = false,
activeAxes = [true, true, true],
offset = [0, 0, 0],
rotation = [0, 0, 0],
Expand All @@ -103,6 +109,7 @@ export const PivotControls: ForwardRefComponent<PivotControlsProps, THREE.Group>
fixed = false,
translationLimits,
rotationLimits,
scaleLimits,
depthTest = true,
axisColors = ['#ff2060', '#20df80', '#2080ff'],
hoveredColor = '#ffff40',
Expand All @@ -122,6 +129,8 @@ export const PivotControls: ForwardRefComponent<PivotControlsProps, THREE.Group>
const gizmoRef = React.useRef<THREE.Group>(null!)
const childrenRef = React.useRef<THREE.Group>(null!)
const translation = React.useRef<[number, number, number]>([0, 0, 0])
const cameraScale = React.useRef<THREE.Vector3>(new THREE.Vector3(1, 1, 1))
const gizmoScale = React.useRef<THREE.Vector3>(new THREE.Vector3(1, 1, 1))

React.useLayoutEffect(() => {
if (!anchor) return
Expand Down Expand Up @@ -164,7 +173,9 @@ export const PivotControls: ForwardRefComponent<PivotControlsProps, THREE.Group>
mL.copy(mW).premultiply(mPInv)
mL0Inv.copy(mL0).invert()
mdL.copy(mL).multiply(mL0Inv)
if (autoTransform) ref.current.matrix.copy(mL)
if (autoTransform) {
ref.current.matrix.copy(mL)
}
onDrag && onDrag(mL, mdL, mW, mdW)
invalidate()
},
Expand Down Expand Up @@ -193,6 +204,7 @@ export const PivotControls: ForwardRefComponent<PivotControlsProps, THREE.Group>
translation,
translationLimits,
rotationLimits,
scaleLimits,
depthTest,
scale,
lineWidth,
Expand All @@ -211,27 +223,34 @@ export const PivotControls: ForwardRefComponent<PivotControlsProps, THREE.Group>
useFrame((state) => {
if (fixed) {
const sf = calculateScaleFactor(gizmoRef.current.getWorldPosition(vec), scale, state.camera, state.size)
if (gizmoRef.current) {
if (
gizmoRef.current?.scale.x !== sf ||
gizmoRef.current?.scale.y !== sf ||
gizmoRef.current?.scale.z !== sf
) {
gizmoRef.current.scale.setScalar(sf)
state.invalidate()
}
}
cameraScale.current.setScalar(sf)
}

if (matrix && matrix instanceof THREE.Matrix4) {
ref.current.matrix = matrix
}
// Update gizmo scale in accordance with matrix changes
// Without this, there might be noticable turbulences if scaling happens fast enough
ref.current.updateWorldMatrix(true, true)

mG.makeRotationFromEuler(gizmoRef.current.rotation)
.setPosition(gizmoRef.current.position)
.premultiply(ref.current.matrixWorld)
gizmoScale.current.setFromMatrixScale(mG)

vScale.copy(cameraScale.current).divide(gizmoScale.current)
if (
Math.abs(gizmoRef.current.scale.x - vScale.x) > 1e-4 ||
Math.abs(gizmoRef.current.scale.y - vScale.y) > 1e-4 ||
Math.abs(gizmoRef.current.scale.z - vScale.z) > 1e-4
) {
gizmoRef.current.scale.copy(vScale)
state.invalidate()
}
})

React.useImperativeHandle(fRef, () => ref.current, [])

React.useLayoutEffect(() => {
// If the matrix is a real matrix4 it means that the user wants to control the gizmo
// In that case it should just be set, as a bare prop update would merely copy it
if (matrix && matrix instanceof THREE.Matrix4) ref.current.matrix = matrix
}, [matrix])

return (
<context.Provider value={config}>
<group ref={parentRef}>
Expand All @@ -246,6 +265,9 @@ export const PivotControls: ForwardRefComponent<PivotControlsProps, THREE.Group>
{!disableRotations && activeAxes[0] && activeAxes[1] && <AxisRotator axis={2} dir1={xDir} dir2={yDir} />}
{!disableRotations && activeAxes[0] && activeAxes[2] && <AxisRotator axis={1} dir1={zDir} dir2={xDir} />}
{!disableRotations && activeAxes[2] && activeAxes[1] && <AxisRotator axis={0} dir1={yDir} dir2={zDir} />}
{!disableScaling && activeAxes[0] && <ScalingSphere axis={0} direction={xDir} />}
{!disableScaling && activeAxes[1] && <ScalingSphere axis={1} direction={yDir} />}
{!disableScaling && activeAxes[2] && <ScalingSphere axis={2} direction={zDir} />}
</group>
<group ref={childrenRef}>{children}</group>
</group>
Expand Down

0 comments on commit 6611a7e

Please sign in to comment.