Improve mouse tracking when stretching from face
This commit is contained in:
parent
bd3d60ccf1
commit
17242f5a69
2 changed files with 143 additions and 47 deletions
|
|
@ -40,6 +40,7 @@ import {
|
|||
createScaleDragState,
|
||||
getScaleFactor,
|
||||
getFreeformScaleFactor,
|
||||
getFaceScaleFactor,
|
||||
type ScaleGizmo,
|
||||
type ScaleDragState,
|
||||
type ScaleHandleInfo,
|
||||
|
|
@ -427,34 +428,61 @@ function onCanvasMouseMove(event: MouseEvent) {
|
|||
const boundsCenter = startBounds.getCenter(new THREE.Vector3())
|
||||
const boundsSize = startBounds.getSize(new THREE.Vector3())
|
||||
|
||||
// Calculate scale factor based on mouse movement
|
||||
const scaleFactor = getScaleFactor(
|
||||
mouse,
|
||||
scaleDragState.startMousePosition,
|
||||
activeCamera,
|
||||
boundsCenter,
|
||||
handleInfo,
|
||||
scaleDragState.handleWorldPosition
|
||||
)
|
||||
// Calculate scale factor based on handle type and drag mode
|
||||
let finalScaleFactor: THREE.Vector3
|
||||
|
||||
if (handleInfo.type === 'corner') {
|
||||
if (scaleDragState.isRightDrag) {
|
||||
// Right-drag: uniform scaling from center
|
||||
finalScaleFactor = getScaleFactor(
|
||||
mouse,
|
||||
scaleDragState.startMousePosition,
|
||||
activeCamera,
|
||||
boundsCenter,
|
||||
handleInfo,
|
||||
scaleDragState.handleWorldPosition
|
||||
)
|
||||
} else {
|
||||
// Left-drag: free-form scaling where corner follows mouse
|
||||
const corner = handleInfo.corner!
|
||||
const halfSize = boundsSize.clone().multiplyScalar(0.5)
|
||||
const anchor = new THREE.Vector3(
|
||||
boundsCenter.x - corner.x * halfSize.x,
|
||||
boundsCenter.y - corner.y * halfSize.y,
|
||||
boundsCenter.z - corner.z * halfSize.z
|
||||
)
|
||||
finalScaleFactor = getFreeformScaleFactor(
|
||||
mouse,
|
||||
activeCamera,
|
||||
anchor,
|
||||
scaleDragState.handleWorldPosition
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Face handle - use axis-constrained scaling that tracks mouse closely
|
||||
const faceDir = new THREE.Vector3()
|
||||
if (handleInfo.face!.startsWith('x')) faceDir.x = handleInfo.face!.endsWith('+') ? 1 : -1
|
||||
else if (handleInfo.face!.startsWith('y')) faceDir.y = handleInfo.face!.endsWith('+') ? 1 : -1
|
||||
else if (handleInfo.face!.startsWith('z')) faceDir.z = handleInfo.face!.endsWith('+') ? 1 : -1
|
||||
|
||||
// For corner handles with left-drag, use free-form scaling where corner follows mouse
|
||||
let finalScaleFactor = scaleFactor.clone()
|
||||
if (handleInfo.type === 'corner' && !scaleDragState.isRightDrag) {
|
||||
// Calculate anchor point (opposite corner)
|
||||
const corner = handleInfo.corner!
|
||||
const halfSize = boundsSize.clone().multiplyScalar(0.5)
|
||||
const anchor = new THREE.Vector3(
|
||||
boundsCenter.x - corner.x * halfSize.x,
|
||||
boundsCenter.y - corner.y * halfSize.y,
|
||||
boundsCenter.z - corner.z * halfSize.z
|
||||
)
|
||||
|
||||
// Use free-form scaling that makes handle follow mouse position
|
||||
finalScaleFactor = getFreeformScaleFactor(
|
||||
// Anchor point depends on drag mode
|
||||
const anchor = scaleDragState.isRightDrag
|
||||
? boundsCenter.clone() // Symmetrical from center
|
||||
: new THREE.Vector3(
|
||||
// Opposite face
|
||||
boundsCenter.x - faceDir.x * halfSize.x,
|
||||
boundsCenter.y - faceDir.y * halfSize.y,
|
||||
boundsCenter.z - faceDir.z * halfSize.z
|
||||
)
|
||||
|
||||
finalScaleFactor = getFaceScaleFactor(
|
||||
mouse,
|
||||
activeCamera,
|
||||
anchor,
|
||||
scaleDragState.handleWorldPosition
|
||||
scaleDragState.handleWorldPosition,
|
||||
handleInfo.axis as 'x' | 'y' | 'z'
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -331,16 +331,15 @@ function clampScaleFactor(factor: number): number {
|
|||
|
||||
/**
|
||||
* Calculate scale factor based on mouse movement.
|
||||
* For face handles with right-drag (symmetrical), uses axis-projected movement.
|
||||
* For corner handles with right-drag (uniform), uses diagonal movement.
|
||||
*/
|
||||
export function getScaleFactor(
|
||||
mouse: THREE.Vector2,
|
||||
startMouse: THREE.Vector2,
|
||||
camera: THREE.Camera,
|
||||
center: THREE.Vector3,
|
||||
_camera: THREE.Camera,
|
||||
_center: THREE.Vector3,
|
||||
handleInfo: ScaleHandleInfo,
|
||||
handleWorldPosition: THREE.Vector3
|
||||
_handleWorldPosition: THREE.Vector3
|
||||
): THREE.Vector3 {
|
||||
// Calculate mouse delta in screen space
|
||||
const mouseDelta = mouse.clone().sub(startMouse)
|
||||
|
|
@ -348,27 +347,10 @@ export function getScaleFactor(
|
|||
// Scale sensitivity
|
||||
const sensitivity = 2.0
|
||||
|
||||
let baseFactor: number
|
||||
|
||||
if (handleInfo.type === 'face' && handleInfo.face) {
|
||||
// For face handles, project mouse movement onto the direction from center to handle
|
||||
// First, get the screen-space direction from center to handle
|
||||
const centerScreen = center.clone().project(camera)
|
||||
const handleScreen = handleWorldPosition.clone().project(camera)
|
||||
const handleDir = new THREE.Vector2(
|
||||
handleScreen.x - centerScreen.x,
|
||||
handleScreen.y - centerScreen.y
|
||||
).normalize()
|
||||
|
||||
// Project mouse delta onto this direction
|
||||
const projectedDelta = mouseDelta.dot(handleDir) * sensitivity
|
||||
baseFactor = 1 + projectedDelta
|
||||
} else {
|
||||
// For corner handles (right-drag uniform scaling), use diagonal mouse movement
|
||||
const deltaLength = mouseDelta.length() * sensitivity
|
||||
const direction = mouseDelta.x + mouseDelta.y > 0 ? 1 : -1
|
||||
baseFactor = 1 + direction * deltaLength
|
||||
}
|
||||
// For corner handles (right-drag uniform scaling), use diagonal mouse movement
|
||||
const deltaLength = mouseDelta.length() * sensitivity
|
||||
const direction = mouseDelta.x + mouseDelta.y > 0 ? 1 : -1
|
||||
const baseFactor = 1 + direction * deltaLength
|
||||
|
||||
// Clamp to prevent zero scale, but allow negative for inversion
|
||||
const clampedFactor = clampScaleFactor(baseFactor)
|
||||
|
|
@ -387,6 +369,92 @@ export function getScaleFactor(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate scale factor for face handles by finding the closest point
|
||||
* on the axis line to the mouse ray. This makes the handle track the
|
||||
* mouse as closely as possible while staying constrained to its axis.
|
||||
*/
|
||||
export function getFaceScaleFactor(
|
||||
mouse: THREE.Vector2,
|
||||
camera: THREE.Camera,
|
||||
anchor: THREE.Vector3,
|
||||
handleWorldPosition: THREE.Vector3,
|
||||
axis: 'x' | 'y' | 'z'
|
||||
): THREE.Vector3 {
|
||||
// Get axis direction
|
||||
const axisDir = new THREE.Vector3()
|
||||
if (axis === 'x') axisDir.set(1, 0, 0)
|
||||
else if (axis === 'y') axisDir.set(0, 1, 0)
|
||||
else axisDir.set(0, 0, 1)
|
||||
|
||||
// Cast a ray from camera through mouse position
|
||||
const raycaster = new THREE.Raycaster()
|
||||
raycaster.setFromCamera(mouse, camera)
|
||||
|
||||
// Find closest point on axis line to the mouse ray
|
||||
// The axis line passes through anchor in direction axisDir
|
||||
const closestPoint = closestPointOnLineToRay(anchor, axisDir, raycaster.ray)
|
||||
|
||||
// Calculate the signed distance from anchor to closest point along axis
|
||||
const anchorToClosest = closestPoint.clone().sub(anchor)
|
||||
const closestDist = anchorToClosest.dot(axisDir)
|
||||
|
||||
// Calculate original distance from anchor to handle along axis
|
||||
const anchorToHandle = handleWorldPosition.clone().sub(anchor)
|
||||
const handleDist = anchorToHandle.dot(axisDir)
|
||||
|
||||
// Scale factor is the ratio
|
||||
let scaleFactor = 1.0
|
||||
if (Math.abs(handleDist) > 0.001) {
|
||||
scaleFactor = closestDist / handleDist
|
||||
}
|
||||
|
||||
scaleFactor = clampScaleFactor(scaleFactor)
|
||||
|
||||
// Apply to the correct axis
|
||||
const result = new THREE.Vector3(1, 1, 1)
|
||||
if (axis === 'x') result.x = scaleFactor
|
||||
else if (axis === 'y') result.y = scaleFactor
|
||||
else result.z = scaleFactor
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the closest point on a line to a ray.
|
||||
* Line is defined by a point and direction.
|
||||
* Returns the closest point on the line.
|
||||
*/
|
||||
function closestPointOnLineToRay(
|
||||
linePoint: THREE.Vector3,
|
||||
lineDir: THREE.Vector3,
|
||||
ray: THREE.Ray
|
||||
): THREE.Vector3 {
|
||||
// Using the formula for closest points between two lines
|
||||
// Line 1: P = linePoint + t * lineDir
|
||||
// Line 2 (ray): Q = ray.origin + s * ray.direction
|
||||
|
||||
const w0 = linePoint.clone().sub(ray.origin)
|
||||
const a = lineDir.dot(lineDir) // Always 1 if normalized
|
||||
const b = lineDir.dot(ray.direction)
|
||||
const c = ray.direction.dot(ray.direction) // Always 1 if normalized
|
||||
const d = lineDir.dot(w0)
|
||||
const e = ray.direction.dot(w0)
|
||||
|
||||
const denom = a * c - b * b
|
||||
|
||||
let t: number
|
||||
if (Math.abs(denom) < 0.0001) {
|
||||
// Lines are parallel, just project linePoint onto ray and back
|
||||
t = 0
|
||||
} else {
|
||||
t = (b * e - c * d) / denom
|
||||
}
|
||||
|
||||
// Return point on line at parameter t
|
||||
return linePoint.clone().add(lineDir.clone().multiplyScalar(t))
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate scale factor for free-form corner scaling (left-drag).
|
||||
* Constrains scaling to a plane aligned to major world axes.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue