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,
|
createScaleDragState,
|
||||||
getScaleFactor,
|
getScaleFactor,
|
||||||
getFreeformScaleFactor,
|
getFreeformScaleFactor,
|
||||||
|
getFaceScaleFactor,
|
||||||
type ScaleGizmo,
|
type ScaleGizmo,
|
||||||
type ScaleDragState,
|
type ScaleDragState,
|
||||||
type ScaleHandleInfo,
|
type ScaleHandleInfo,
|
||||||
|
|
@ -427,8 +428,13 @@ function onCanvasMouseMove(event: MouseEvent) {
|
||||||
const boundsCenter = startBounds.getCenter(new THREE.Vector3())
|
const boundsCenter = startBounds.getCenter(new THREE.Vector3())
|
||||||
const boundsSize = startBounds.getSize(new THREE.Vector3())
|
const boundsSize = startBounds.getSize(new THREE.Vector3())
|
||||||
|
|
||||||
// Calculate scale factor based on mouse movement
|
// Calculate scale factor based on handle type and drag mode
|
||||||
const scaleFactor = getScaleFactor(
|
let finalScaleFactor: THREE.Vector3
|
||||||
|
|
||||||
|
if (handleInfo.type === 'corner') {
|
||||||
|
if (scaleDragState.isRightDrag) {
|
||||||
|
// Right-drag: uniform scaling from center
|
||||||
|
finalScaleFactor = getScaleFactor(
|
||||||
mouse,
|
mouse,
|
||||||
scaleDragState.startMousePosition,
|
scaleDragState.startMousePosition,
|
||||||
activeCamera,
|
activeCamera,
|
||||||
|
|
@ -436,11 +442,8 @@ function onCanvasMouseMove(event: MouseEvent) {
|
||||||
handleInfo,
|
handleInfo,
|
||||||
scaleDragState.handleWorldPosition
|
scaleDragState.handleWorldPosition
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
// For corner handles with left-drag, use free-form scaling where corner follows mouse
|
// Left-drag: 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 corner = handleInfo.corner!
|
||||||
const halfSize = boundsSize.clone().multiplyScalar(0.5)
|
const halfSize = boundsSize.clone().multiplyScalar(0.5)
|
||||||
const anchor = new THREE.Vector3(
|
const anchor = new THREE.Vector3(
|
||||||
|
|
@ -448,8 +451,6 @@ function onCanvasMouseMove(event: MouseEvent) {
|
||||||
boundsCenter.y - corner.y * halfSize.y,
|
boundsCenter.y - corner.y * halfSize.y,
|
||||||
boundsCenter.z - corner.z * halfSize.z
|
boundsCenter.z - corner.z * halfSize.z
|
||||||
)
|
)
|
||||||
|
|
||||||
// Use free-form scaling that makes handle follow mouse position
|
|
||||||
finalScaleFactor = getFreeformScaleFactor(
|
finalScaleFactor = getFreeformScaleFactor(
|
||||||
mouse,
|
mouse,
|
||||||
activeCamera,
|
activeCamera,
|
||||||
|
|
@ -457,6 +458,33 @@ function onCanvasMouseMove(event: MouseEvent) {
|
||||||
scaleDragState.handleWorldPosition
|
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
|
||||||
|
|
||||||
|
const halfSize = boundsSize.clone().multiplyScalar(0.5)
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
handleInfo.axis as 'x' | 'y' | 'z'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Apply scale to all selected objects
|
// Apply scale to all selected objects
|
||||||
for (const mesh of selectedMeshes) {
|
for (const mesh of selectedMeshes) {
|
||||||
|
|
|
||||||
|
|
@ -331,16 +331,15 @@ function clampScaleFactor(factor: number): number {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate scale factor based on mouse movement.
|
* 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.
|
* For corner handles with right-drag (uniform), uses diagonal movement.
|
||||||
*/
|
*/
|
||||||
export function getScaleFactor(
|
export function getScaleFactor(
|
||||||
mouse: THREE.Vector2,
|
mouse: THREE.Vector2,
|
||||||
startMouse: THREE.Vector2,
|
startMouse: THREE.Vector2,
|
||||||
camera: THREE.Camera,
|
_camera: THREE.Camera,
|
||||||
center: THREE.Vector3,
|
_center: THREE.Vector3,
|
||||||
handleInfo: ScaleHandleInfo,
|
handleInfo: ScaleHandleInfo,
|
||||||
handleWorldPosition: THREE.Vector3
|
_handleWorldPosition: THREE.Vector3
|
||||||
): THREE.Vector3 {
|
): THREE.Vector3 {
|
||||||
// Calculate mouse delta in screen space
|
// Calculate mouse delta in screen space
|
||||||
const mouseDelta = mouse.clone().sub(startMouse)
|
const mouseDelta = mouse.clone().sub(startMouse)
|
||||||
|
|
@ -348,27 +347,10 @@ export function getScaleFactor(
|
||||||
// Scale sensitivity
|
// Scale sensitivity
|
||||||
const sensitivity = 2.0
|
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
|
// For corner handles (right-drag uniform scaling), use diagonal mouse movement
|
||||||
const deltaLength = mouseDelta.length() * sensitivity
|
const deltaLength = mouseDelta.length() * sensitivity
|
||||||
const direction = mouseDelta.x + mouseDelta.y > 0 ? 1 : -1
|
const direction = mouseDelta.x + mouseDelta.y > 0 ? 1 : -1
|
||||||
baseFactor = 1 + direction * deltaLength
|
const baseFactor = 1 + direction * deltaLength
|
||||||
}
|
|
||||||
|
|
||||||
// Clamp to prevent zero scale, but allow negative for inversion
|
// Clamp to prevent zero scale, but allow negative for inversion
|
||||||
const clampedFactor = clampScaleFactor(baseFactor)
|
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).
|
* Calculate scale factor for free-form corner scaling (left-drag).
|
||||||
* Constrains scaling to a plane aligned to major world axes.
|
* Constrains scaling to a plane aligned to major world axes.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue