Fix XZ-constrained corner scaling
This commit is contained in:
parent
17242f5a69
commit
e93b63bb9d
1 changed files with 99 additions and 81 deletions
|
|
@ -370,9 +370,16 @@ 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.
|
||||
* Calculate scale factor for face handles by intersecting with a constraint plane.
|
||||
* The constraint plane must CONTAIN the scale axis, and should be as perpendicular
|
||||
* to the camera view as possible for stable mouse tracking.
|
||||
*
|
||||
* For each scale axis, there are two planes containing it:
|
||||
* - X axis: XY plane (normal=Z) or XZ plane (normal=Y)
|
||||
* - Y axis: XY plane (normal=Z) or YZ plane (normal=X)
|
||||
* - Z axis: XZ plane (normal=Y) or YZ plane (normal=X)
|
||||
*
|
||||
* We pick the plane whose normal is more aligned with the camera view.
|
||||
*/
|
||||
export function getFaceScaleFactor(
|
||||
mouse: THREE.Vector2,
|
||||
|
|
@ -381,23 +388,62 @@ export function getFaceScaleFactor(
|
|||
handleWorldPosition: THREE.Vector3,
|
||||
axis: 'x' | 'y' | 'z'
|
||||
): THREE.Vector3 {
|
||||
// Get axis direction
|
||||
// Get axis direction for the face being scaled
|
||||
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)
|
||||
|
||||
// Get camera view direction
|
||||
const viewDir = new THREE.Vector3()
|
||||
camera.getWorldDirection(viewDir)
|
||||
|
||||
// Choose constraint plane that contains the scale axis and is most perpendicular to camera
|
||||
let planeNormal: THREE.Vector3
|
||||
|
||||
if (axis === 'x') {
|
||||
// Planes containing X: XY (normal=Z) and XZ (normal=Y)
|
||||
// Pick the one whose normal is more aligned with view direction
|
||||
if (Math.abs(viewDir.y) > Math.abs(viewDir.z)) {
|
||||
planeNormal = new THREE.Vector3(0, 1, 0) // XZ plane
|
||||
} else {
|
||||
planeNormal = new THREE.Vector3(0, 0, 1) // XY plane
|
||||
}
|
||||
} else if (axis === 'y') {
|
||||
// Planes containing Y: XY (normal=Z) and YZ (normal=X)
|
||||
if (Math.abs(viewDir.x) > Math.abs(viewDir.z)) {
|
||||
planeNormal = new THREE.Vector3(1, 0, 0) // YZ plane
|
||||
} else {
|
||||
planeNormal = new THREE.Vector3(0, 0, 1) // XY plane
|
||||
}
|
||||
} else {
|
||||
// Planes containing Z: XZ (normal=Y) and YZ (normal=X)
|
||||
if (Math.abs(viewDir.x) > Math.abs(viewDir.y)) {
|
||||
planeNormal = new THREE.Vector3(1, 0, 0) // YZ plane
|
||||
} else {
|
||||
planeNormal = new THREE.Vector3(0, 1, 0) // XZ plane
|
||||
}
|
||||
}
|
||||
|
||||
// Create the constraint plane passing through the handle
|
||||
const plane = new THREE.Plane()
|
||||
plane.setFromNormalAndCoplanarPoint(planeNormal, handleWorldPosition)
|
||||
|
||||
// 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)
|
||||
// Intersect with constraint plane
|
||||
const intersection = new THREE.Vector3()
|
||||
const hit = raycaster.ray.intersectPlane(plane, intersection)
|
||||
|
||||
// Calculate the signed distance from anchor to closest point along axis
|
||||
const anchorToClosest = closestPoint.clone().sub(anchor)
|
||||
const closestDist = anchorToClosest.dot(axisDir)
|
||||
if (!hit) {
|
||||
return new THREE.Vector3(1, 1, 1)
|
||||
}
|
||||
|
||||
// Project the intersection onto the scale axis
|
||||
const anchorToIntersection = intersection.clone().sub(anchor)
|
||||
const targetDist = anchorToIntersection.dot(axisDir)
|
||||
|
||||
// Calculate original distance from anchor to handle along axis
|
||||
const anchorToHandle = handleWorldPosition.clone().sub(anchor)
|
||||
|
|
@ -406,7 +452,7 @@ export function getFaceScaleFactor(
|
|||
// Scale factor is the ratio
|
||||
let scaleFactor = 1.0
|
||||
if (Math.abs(handleDist) > 0.001) {
|
||||
scaleFactor = closestDist / handleDist
|
||||
scaleFactor = targetDist / handleDist
|
||||
}
|
||||
|
||||
scaleFactor = clampScaleFactor(scaleFactor)
|
||||
|
|
@ -420,48 +466,15 @@ export function getFaceScaleFactor(
|
|||
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.
|
||||
* The plane is chosen based on camera angle:
|
||||
* - Vertical axis (Y) is always included
|
||||
* - Horizontal axis (X or Z) is whichever is most perpendicular to camera view
|
||||
* The corner handle follows the mouse position exactly on this plane.
|
||||
* The plane is determined by which direction the camera is looking:
|
||||
* - Looking along X (from sides): scale Y and Z (YZ plane)
|
||||
* - Looking along Y (from above/below): scale X and Z (XZ plane)
|
||||
* - Looking along Z (from front/back): scale X and Y (XY plane)
|
||||
*
|
||||
* This creates 6 pyramid-shaped zones extending from the object.
|
||||
*/
|
||||
export function getFreeformScaleFactor(
|
||||
mouse: THREE.Vector2,
|
||||
|
|
@ -470,28 +483,38 @@ export function getFreeformScaleFactor(
|
|||
handleWorldPosition: THREE.Vector3
|
||||
): THREE.Vector3 {
|
||||
// Get camera's forward direction (where it's looking)
|
||||
const cameraForward = new THREE.Vector3()
|
||||
camera.getWorldDirection(cameraForward)
|
||||
const viewDir = new THREE.Vector3()
|
||||
camera.getWorldDirection(viewDir)
|
||||
|
||||
// Project onto XZ plane to get horizontal look direction
|
||||
const horizontalForward = new THREE.Vector2(cameraForward.x, cameraForward.z)
|
||||
if (horizontalForward.length() > 0.001) {
|
||||
horizontalForward.normalize()
|
||||
// Determine which axis the camera is most aligned with
|
||||
const dotX = Math.abs(viewDir.x)
|
||||
const dotY = Math.abs(viewDir.y)
|
||||
const dotZ = Math.abs(viewDir.z)
|
||||
|
||||
// Choose the constraint plane perpendicular to the dominant view axis
|
||||
let planeNormal: THREE.Vector3
|
||||
let scaleX = false
|
||||
let scaleY = false
|
||||
let scaleZ = false
|
||||
|
||||
if (dotX >= dotY && dotX >= dotZ) {
|
||||
// Looking mostly along X - use YZ plane (scale Y and Z)
|
||||
planeNormal = new THREE.Vector3(1, 0, 0)
|
||||
scaleY = true
|
||||
scaleZ = true
|
||||
} else if (dotY >= dotX && dotY >= dotZ) {
|
||||
// Looking mostly along Y - use XZ plane (scale X and Z)
|
||||
planeNormal = new THREE.Vector3(0, 1, 0)
|
||||
scaleX = true
|
||||
scaleZ = true
|
||||
} else {
|
||||
// Looking mostly along Z - use XY plane (scale X and Y)
|
||||
planeNormal = new THREE.Vector3(0, 0, 1)
|
||||
scaleX = true
|
||||
scaleY = true
|
||||
}
|
||||
|
||||
// Determine which horizontal axis (X or Z) is most perpendicular to camera
|
||||
const dotX = Math.abs(horizontalForward.x) // How much we're looking along X
|
||||
const dotZ = Math.abs(horizontalForward.y) // How much we're looking along Z
|
||||
|
||||
// If looking more along X, Z is perpendicular. If looking more along Z, X is perpendicular.
|
||||
const useXAxis = dotZ > dotX
|
||||
|
||||
// Create a plane passing through the handle, with normal pointing toward camera
|
||||
// The plane is defined by the axis we're NOT scaling (X or Z)
|
||||
const planeNormal = useXAxis
|
||||
? new THREE.Vector3(0, 0, 1) // XY plane (scaling X and Y, Z fixed)
|
||||
: new THREE.Vector3(1, 0, 0) // YZ plane (scaling Y and Z, X fixed)
|
||||
|
||||
// Create the constraint plane passing through the handle
|
||||
const plane = new THREE.Plane()
|
||||
plane.setFromNormalAndCoplanarPoint(planeNormal, handleWorldPosition)
|
||||
|
||||
|
|
@ -515,20 +538,15 @@ export function getFreeformScaleFactor(
|
|||
const result = new THREE.Vector3(1, 1, 1)
|
||||
const minComponent = 0.001
|
||||
|
||||
// Y axis always scales
|
||||
if (Math.abs(anchorToHandle.y) > minComponent) {
|
||||
result.y = clampScaleFactor(anchorToMouse.y / anchorToHandle.y)
|
||||
}
|
||||
|
||||
// Scale X or Z based on which plane we're using
|
||||
if (useXAxis) {
|
||||
if (Math.abs(anchorToHandle.x) > minComponent) {
|
||||
// Scale only the axes in the constraint plane
|
||||
if (scaleX && Math.abs(anchorToHandle.x) > minComponent) {
|
||||
result.x = clampScaleFactor(anchorToMouse.x / anchorToHandle.x)
|
||||
}
|
||||
} else {
|
||||
if (Math.abs(anchorToHandle.z) > minComponent) {
|
||||
result.z = clampScaleFactor(anchorToMouse.z / anchorToHandle.z)
|
||||
if (scaleY && Math.abs(anchorToHandle.y) > minComponent) {
|
||||
result.y = clampScaleFactor(anchorToMouse.y / anchorToHandle.y)
|
||||
}
|
||||
if (scaleZ && Math.abs(anchorToHandle.z) > minComponent) {
|
||||
result.z = clampScaleFactor(anchorToMouse.z / anchorToHandle.z)
|
||||
}
|
||||
|
||||
return result
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue