rotationControls snapping bug fixed

This commit is contained in:
2025-08-20 10:39:29 +05:30
parent a8c6f8d80a
commit 64aadbb53d
2 changed files with 65 additions and 56 deletions

View File

@@ -1,22 +1,15 @@
import * as THREE from "three"; import * as THREE from "three";
function normalizeDegrees(deg: number): number {
// Wrap into [-180, 180]
deg = (deg + 180) % 360;
if (deg < 0) deg += 360;
return deg - 180;
}
export function handleAssetRotationSnap({ export function handleAssetRotationSnap({
currentRotation, object,
rotationDelta, pointerDeltaX,
keyEvent, keyEvent,
snapBaseRef, snapBaseRef,
prevRotationRef, prevRotationRef,
wasShiftHeldRef wasShiftHeldRef
}: { }: {
currentRotation: THREE.Euler; object: THREE.Object3D;
rotationDelta: number; pointerDeltaX: number;
keyEvent: string; keyEvent: string;
snapBaseRef: React.MutableRefObject<number | null>; snapBaseRef: React.MutableRefObject<number | null>;
prevRotationRef: React.MutableRefObject<number | null>; prevRotationRef: React.MutableRefObject<number | null>;
@@ -31,37 +24,51 @@ export function handleAssetRotationSnap({
const isCtrlHeld = keyEvent.includes("Ctrl"); const isCtrlHeld = keyEvent.includes("Ctrl");
const speedFactor = isShiftHeld ? SHIFT_SPEED : NORMAL_SPEED; const speedFactor = isShiftHeld ? SHIFT_SPEED : NORMAL_SPEED;
let newRotationDeltaDeg = THREE.MathUtils.radToDeg(rotationDelta) * speedFactor; let deltaAngle = pointerDeltaX * speedFactor;
// Track if modifier changed
const modifierChanged = isShiftHeld !== wasShiftHeldRef.current; const modifierChanged = isShiftHeld !== wasShiftHeldRef.current;
if (modifierChanged) { if (modifierChanged) {
wasShiftHeldRef.current = isShiftHeld; wasShiftHeldRef.current = isShiftHeld;
if (isCtrlHeld) snapBaseRef.current = null;
} }
if (isCtrlHeld) { if (isCtrlHeld) {
const snapDeg = isShiftHeld ? CTRL_SHIFT_SNAP_DEG : CTRL_SNAP_DEG; const snapDeg = isShiftHeld ? CTRL_SHIFT_SNAP_DEG : CTRL_SNAP_DEG;
const snapRad = THREE.MathUtils.degToRad(snapDeg);
// Store base rotation in degrees when Ctrl first pressed // Get current Y rotation from object's quaternion
const euler = new THREE.Euler().setFromQuaternion(object.quaternion, 'YXZ');
let currentAngle = euler.y;
// Initialize snap base on first frame
if (snapBaseRef.current === null) { if (snapBaseRef.current === null) {
snapBaseRef.current = normalizeDegrees(THREE.MathUtils.radToDeg(currentRotation.y)); snapBaseRef.current = currentAngle;
} else { prevRotationRef.current = currentAngle;
snapBaseRef.current = normalizeDegrees(snapBaseRef.current + newRotationDeltaDeg);
} }
// Accumulate the total rotation from the base
const totalRotation = snapBaseRef.current + deltaAngle;
// Snap to nearest increment // Snap to nearest increment
const snappedDeg = Math.round(snapBaseRef.current / snapDeg) * snapDeg; const snappedAngle = Math.round(totalRotation / snapRad) * snapRad;
// Normalize so it never goes beyond [-180, 180] // Calculate the delta needed to reach the snapped angle from current rotation
const normalizedSnappedDeg = normalizeDegrees(snappedDeg); let targetDelta = snappedAngle - currentAngle;
// Convert back to radians for returning delta // Handle wrapping around 360 degrees
newRotationDeltaDeg = normalizedSnappedDeg - THREE.MathUtils.radToDeg(currentRotation.y); if (Math.abs(targetDelta) > Math.PI) {
targetDelta = targetDelta - Math.sign(targetDelta) * 2 * Math.PI;
} else {
snapBaseRef.current = null;
} }
prevRotationRef.current = currentRotation.y; // Update snap base for next frame
snapBaseRef.current = totalRotation;
return THREE.MathUtils.degToRad(newRotationDeltaDeg); return targetDelta;
} else {
// Reset snapping when Ctrl is not held
snapBaseRef.current = null;
prevRotationRef.current = null;
return deltaAngle;
}
} }

View File

@@ -156,6 +156,7 @@ function RotateControls3D({
}, [camera, scene, toggleView, selectedAssets, rotatedObjects, pastedObjects, duplicatedObjects, movedObjects, keyEvent]); }, [camera, scene, toggleView, selectedAssets, rotatedObjects, pastedObjects, duplicatedObjects, movedObjects, keyEvent]);
const resetToInitialRotations = useCallback(() => { const resetToInitialRotations = useCallback(() => {
setTimeout(() => {
rotatedObjects.forEach((obj: THREE.Object3D) => { rotatedObjects.forEach((obj: THREE.Object3D) => {
const uuid = obj.uuid; const uuid = obj.uuid;
if (obj.userData.modelUuid) { if (obj.userData.modelUuid) {
@@ -176,6 +177,7 @@ function RotateControls3D({
} }
} }
}); });
}, 100)
}, [rotatedObjects, initialRotations, initialPositions, updateAsset]); }, [rotatedObjects, initialRotations, initialPositions, updateAsset]);
useFrame(() => { useFrame(() => {
@@ -192,25 +194,25 @@ function RotateControls3D({
const center = rotationCenter.current; const center = rotationCenter.current;
const deltaX = currentPointer.x - prevPointerPosition.current.x; const deltaX = currentPointer.x - prevPointerPosition.current.x;
const rawAngleDelta = deltaX; rotatedObjects.forEach((obj: THREE.Object3D) => {
if (obj.userData.modelUuid) {
const angleDelta = handleAssetRotationSnap({ const angleDelta = handleAssetRotationSnap({
currentRotation: rotatedObjects[0].rotation, object: obj,
rotationDelta: rawAngleDelta, pointerDeltaX: deltaX,
keyEvent, keyEvent,
snapBaseRef, snapBaseRef,
prevRotationRef, prevRotationRef,
wasShiftHeldRef wasShiftHeldRef
}); });
const rotationMatrix = new THREE.Matrix4().makeRotationY(angleDelta);
rotatedObjects.forEach((obj: THREE.Object3D) => {
if (obj.userData.modelUuid) {
const relativePosition = new THREE.Vector3().subVectors(obj.position, center); const relativePosition = new THREE.Vector3().subVectors(obj.position, center);
const rotationMatrix = new THREE.Matrix4().makeRotationY(angleDelta);
relativePosition.applyMatrix4(rotationMatrix); relativePosition.applyMatrix4(rotationMatrix);
obj.position.copy(center).add(relativePosition); obj.position.copy(center).add(relativePosition);
obj.rotateOnWorldAxis(new THREE.Vector3(0, 1, 0), angleDelta);
const rotationQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), angleDelta);
obj.quaternion.multiply(rotationQuat);
obj.rotation.setFromQuaternion(obj.quaternion);
} }
}); });