Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: PivotControls scaling #1249

Merged
merged 10 commits into from
Mar 7, 2024
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2619,7 +2619,7 @@ Notes:
ScrollControls example

```jsx
<ScrollControls damping={0.2} maxSpeed={0.5} pages={2}>
;<ScrollControls damping={0.2} maxSpeed={0.5} pages={2}>
<SpriteAnimator
position={[0.0, -1.5, -1.5]}
startFrame={0}
Expand Down Expand Up @@ -3306,7 +3306,6 @@ const { spriteObj } = useSpriteLoader(
/>
```


# Performance

#### Instances
Expand Down
2 changes: 1 addition & 1 deletion src/web/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ export * from './FaceControls'
export * from './FaceLandmarker'

// Shapes
export * from './Facemesh'
export * from './Facemesh'
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
Loading