From 6611a7e331dc25bc4d553f66ce64e65c8dea09fd Mon Sep 17 00:00:00 2001 From: STINGNAILS Date: Thu, 7 Mar 2024 15:08:12 +0700 Subject: [PATCH] feat: PivotControls scaling (#1249) * 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 --- src/web/pivotControls/ScalingSphere.tsx | 214 ++++++++++++++++++++++++ src/web/pivotControls/context.ts | 3 +- src/web/pivotControls/index.tsx | 56 +++++-- 3 files changed, 255 insertions(+), 18 deletions(-) create mode 100644 src/web/pivotControls/ScalingSphere.tsx diff --git a/src/web/pivotControls/ScalingSphere.tsx b/src/web/pivotControls/ScalingSphere.tsx new file mode 100644 index 000000000..82f9636ed --- /dev/null +++ b/src/web/pivotControls/ScalingSphere.tsx @@ -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(null!) + const objRef = React.useRef(null!) + const meshRef = React.useRef(null!) + const scale0 = React.useRef(1) + const scaleCur = React.useRef(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) => { + 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) => { + 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) => { + 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) => { + 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 ( + + + {annotations && ( + +
+ + )} + + + + + + + ) +} diff --git a/src/web/pivotControls/context.ts b/src/web/pivotControls/context.ts index 5b1349624..76c57775a 100644 --- a/src/web/pivotControls/context.ts +++ b/src/web/pivotControls/context.ts @@ -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[] @@ -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 diff --git a/src/web/pivotControls/index.tsx b/src/web/pivotControls/index.tsx index 30e9ba9d2..6d8b3ba86 100644 --- a/src/web/pivotControls/index.tsx +++ b/src/web/pivotControls/index.tsx @@ -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' @@ -17,6 +18,7 @@ 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() @@ -24,6 +26,7 @@ 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) @@ -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] @@ -95,6 +100,7 @@ export const PivotControls: ForwardRefComponent disableAxes = false, disableSliders = false, disableRotations = false, + disableScaling = false, activeAxes = [true, true, true], offset = [0, 0, 0], rotation = [0, 0, 0], @@ -103,6 +109,7 @@ export const PivotControls: ForwardRefComponent fixed = false, translationLimits, rotationLimits, + scaleLimits, depthTest = true, axisColors = ['#ff2060', '#20df80', '#2080ff'], hoveredColor = '#ffff40', @@ -122,6 +129,8 @@ export const PivotControls: ForwardRefComponent const gizmoRef = React.useRef(null!) const childrenRef = React.useRef(null!) const translation = React.useRef<[number, number, number]>([0, 0, 0]) + const cameraScale = React.useRef(new THREE.Vector3(1, 1, 1)) + const gizmoScale = React.useRef(new THREE.Vector3(1, 1, 1)) React.useLayoutEffect(() => { if (!anchor) return @@ -164,7 +173,9 @@ export const PivotControls: ForwardRefComponent 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() }, @@ -193,6 +204,7 @@ export const PivotControls: ForwardRefComponent translation, translationLimits, rotationLimits, + scaleLimits, depthTest, scale, lineWidth, @@ -211,27 +223,34 @@ export const PivotControls: ForwardRefComponent 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 ( @@ -246,6 +265,9 @@ export const PivotControls: ForwardRefComponent {!disableRotations && activeAxes[0] && activeAxes[1] && } {!disableRotations && activeAxes[0] && activeAxes[2] && } {!disableRotations && activeAxes[2] && activeAxes[1] && } + {!disableScaling && activeAxes[0] && } + {!disableScaling && activeAxes[1] && } + {!disableScaling && activeAxes[2] && } {children}