@pmndrs/handle
Version:
framework agnostic expandable handle implementation for threejs
360 lines (359 loc) • 13.7 kB
JavaScript
import { Euler, Matrix4, Plane, Quaternion, Vector3 } from 'three';
import { clamp } from 'three/src/math/MathUtils.js';
const matrixHelper1 = new Matrix4();
const matrixHelper2 = new Matrix4();
const axisFirstOrder = {
x: 'XYZ',
y: 'YXZ',
z: 'ZXY',
};
export function computeHandleTransformState(time, pointerAmount, targetWorldMatrix, storeData, targetParentWorldMatrix, options) {
matrixHelper1.copy(targetWorldMatrix);
if (targetParentWorldMatrix != null) {
//to transform matrix helper into target local space
matrixHelper1.premultiply(matrixHelper2.copy(targetParentWorldMatrix).invert());
}
//new values
const position = new Vector3();
const quaternion = new Quaternion();
const scale = new Vector3();
//decompose
matrixHelper1.decompose(position, quaternion, scale);
//position and quaternion now contain the resulting transformation before applying the options
//compute position
applyTransformOptionsToVector(position, storeData.initialTargetPosition, options.translate ?? true);
//compute rotation
let rotation;
const rotateOptions = options.rotate ?? true;
if (rotateOptions === false) {
quaternion.copy(storeData.initialTargetQuaternion);
rotation = storeData.initialTargetRotation.clone();
}
else if (Array.isArray(rotateOptions) ||
rotateOptions === true ||
(typeof rotateOptions != 'string' &&
!Array.isArray(rotateOptions) &&
rotateOptions.x === true &&
rotateOptions.y === true &&
rotateOptions.z === true)) {
rotation = new Euler().setFromQuaternion(quaternion, storeData.initialTargetRotation.order);
}
else if (typeof rotateOptions === 'string') {
const order = axisFirstOrder[rotateOptions];
rotation = new Euler().setFromQuaternion(quaternion, order);
for (const orderElement of order) {
const axis = orderElement.toLowerCase();
if (axis === rotateOptions) {
continue;
}
rotation[axis] = 0;
}
rotation.order = storeData.initialTargetRotation.order;
quaternion.setFromEuler(rotation);
}
else {
rotation = applyTransformOptionsToRotation(quaternion, storeData.initialTargetRotation, rotateOptions);
}
//compute scale
if (typeof options.scale != 'object' || !options.scale.uniform) {
applyTransformOptionsToVector(scale, storeData.initialTargetScale, options.scale ?? true);
}
return {
pointerAmount,
position,
quaternion,
rotation,
scale,
time,
};
}
const pHelper = new Plane();
const v1Helper = new Vector3();
const v2Helper = new Vector3();
const v3Helper = new Vector3();
const qHelper = new Quaternion();
export function getDeltaQuaternionOnAxis(normalizedAxis, from, to) {
pHelper.normal.copy(normalizedAxis);
pHelper.constant = 0;
getPerpendicular(v1Helper, pHelper.normal);
v2Helper.copy(v1Helper);
v2Helper.applyQuaternion(qHelper.copy(from).invert().premultiply(to));
pHelper.projectPoint(v1Helper, v1Helper).normalize();
pHelper.projectPoint(v2Helper, v2Helper).normalize();
return (v3Helper.crossVectors(v1Helper, pHelper.normal).dot(v2Helper) < 0 ? 1 : -1) * v1Helper.angleTo(v2Helper);
}
function getPerpendicular(target, from) {
if (from.x === 0) {
target.set(1, 0, 0);
return;
}
if (from.y === 0) {
target.set(0, 1, 0);
return;
}
if (from.z === 0) {
target.set(0, 0, 1);
return;
}
target.set(-from.y, from.x, 0);
}
export function applyTransformOptionsToRotation(currentRotation, initialRotation, options) {
let orderEnabledAxis = '';
let orderDisabledAxis = '';
for (const orderElement of initialRotation.order) {
if (options[orderElement.toLowerCase()] === false) {
orderDisabledAxis += orderElement;
}
else {
orderEnabledAxis += orderElement;
}
}
const order = (orderEnabledAxis + orderDisabledAxis);
const result = new Euler().setFromQuaternion(currentRotation, order);
for (const orderElement of order) {
const axis = orderElement.toLowerCase();
result[axis] = applyTransformOptionsToAxis(axis, result[axis], initialRotation[axis], options);
}
currentRotation.setFromEuler(result);
return result;
}
const applyTransformNormal = new Vector3();
const applyTransformPlane = new Plane();
const applyTransformCross1 = new Vector3();
const applyTransformCross2 = new Vector3();
function applyTransformOptionsToVector(target, initialVector, options) {
if (Array.isArray(options)) {
switch (options.length) {
case 0:
target.copy(initialVector);
return;
case 1:
target.sub(initialVector);
projectPointOntoNormal(target, options[0] instanceof Vector3 ? options[0] : applyTransformNormal.fromArray(options[0]));
target.add(initialVector);
return;
case 2:
applyTransformNormal.crossVectors(options[0] instanceof Vector3 ? options[0] : applyTransformCross1.fromArray(options[0]), options[1] instanceof Vector3 ? options[1] : applyTransformCross2.fromArray(options[1]));
applyTransformPlane.setFromNormalAndCoplanarPoint(applyTransformNormal, initialVector);
applyTransformPlane.projectPoint(target, target);
return;
}
//3 or more, we do nothing
return;
}
target.x = applyTransformOptionsToAxis('x', target.x, initialVector.x, options);
target.y = applyTransformOptionsToAxis('y', target.y, initialVector.y, options);
target.z = applyTransformOptionsToAxis('z', target.z, initialVector.z, options);
}
/**
* @requires that the provided value is a delta value not the absolute value
*/
function applyTransformOptionsToAxis(axis, value, neutralValue, options) {
if (typeof options === 'boolean') {
return options ? value : neutralValue;
}
if (typeof options === 'string') {
return options === axis ? value : neutralValue;
}
const option = options[axis];
if (option === false) {
return neutralValue;
}
if (Array.isArray(option)) {
return clamp(value, ...option);
}
return value;
}
export function projectOntoSpace(projectRays = true, space, initialWorldPoint, worldPointerOrigin, worldPoint, worldDirection) {
if (!projectRays) {
return;
}
switch (space.length) {
case 0:
case 3:
return;
case 1:
projectOntoAxis(initialWorldPoint, ...space, worldPointerOrigin, worldPoint, worldDirection);
return;
case 2:
projectOntoPlane(...space, initialWorldPoint, worldPointerOrigin, worldPoint, worldDirection);
return;
}
throw new Error(`space cannot be ${space.length}D but received (${space.map((s) => s.toArray().join('/')).join('; ')})`);
}
const axisVectorMap = {
x: new Vector3(1, 0, 0),
y: new Vector3(0, 1, 0),
z: new Vector3(0, 0, 1),
};
export function addSpaceFromTransformOptions(target, parentWorldQuaternion, initialLocalRotation, options, type) {
if (options === false) {
return;
}
if (options === true) {
target[0] = axisVectorMap.x;
target[1] = axisVectorMap.y;
target[2] = axisVectorMap.z;
return;
}
if (typeof options === 'string') {
addSpaceFromAxis(target, parentWorldQuaternion, initialLocalRotation, options, type);
return;
}
if (Array.isArray(options)) {
for (const axis of options) {
addSpaceFromAxis(target, parentWorldQuaternion, initialLocalRotation, axis, type);
}
return;
}
if (options.x !== false) {
addSpaceFromAxis(target, parentWorldQuaternion, initialLocalRotation, 'x', type);
}
if (options.y !== false) {
addSpaceFromAxis(target, parentWorldQuaternion, initialLocalRotation, 'y', type);
}
if (options.z !== false) {
addSpaceFromAxis(target, parentWorldQuaternion, initialLocalRotation, 'z', type);
}
}
const rHelper = new Quaternion();
const eHelper = new Euler();
const axisHelper = new Vector3();
const otherVectorHelper = new Vector3();
const resultVectorHelper = new Vector3();
function addSpaceFromAxis(target, targetParentWorldQuaternion, initialTargetRotation, axis, type) {
if (Array.isArray(axis)) {
axisHelper.set(...axis);
}
else if (axis instanceof Vector3) {
axisHelper.copy(axis);
}
else {
axisHelper.copy(axisVectorMap[axis]);
}
if (type === 'translate') {
axisHelper.applyQuaternion(targetParentWorldQuaternion);
addAxisToSpace(target, axisHelper);
return;
}
if (type === 'scale') {
if (Array.isArray(axis)) {
rHelper.identity();
}
else {
rHelper.setFromEuler(initialTargetRotation);
}
rHelper.premultiply(targetParentWorldQuaternion);
axisHelper.applyQuaternion(rHelper);
addAxisToSpace(target, axisHelper);
return;
}
if (Array.isArray(axis)) {
eHelper.set(0, 0, 0);
}
else {
eHelper.copy(initialTargetRotation);
for (let i = 2; i >= 0; i--) {
const rotationAxis = initialTargetRotation.order[i].toLowerCase();
eHelper[rotationAxis] = 0;
if (rotationAxis === axis) {
break;
}
}
}
rHelper.setFromEuler(eHelper).premultiply(targetParentWorldQuaternion);
axisHelper.normalize();
// Step 1: Choose a random vector that is not parallel to the original
otherVectorHelper.set(0, 1, 0);
if (axisHelper.dot(otherVectorHelper) > 0.99) {
otherVectorHelper.set(0, 0, 1); // Change the random vector if it's too parallel
}
// Step 2: First perpendicular vector
resultVectorHelper.crossVectors(axisHelper, otherVectorHelper).normalize();
otherVectorHelper.copy(resultVectorHelper);
resultVectorHelper.applyQuaternion(rHelper);
addAxisToSpace(target, resultVectorHelper);
// Step 3: Second perpendicular vector
resultVectorHelper.crossVectors(axisHelper, otherVectorHelper).normalize();
resultVectorHelper.applyQuaternion(rHelper);
addAxisToSpace(target, resultVectorHelper);
}
const crossHelper = new Vector3();
function addAxisToSpace(target, axis) {
if (target.length === 3) {
return;
}
if (target.length === 0) {
target.push(axis.clone());
return;
}
if (target.length === 1) {
if (Math.abs(target[0].dot(axis)) < 0.999) {
target.push(axis.clone());
}
return;
}
crossHelper.crossVectors(target[0], target[1]);
if (Math.abs(crossHelper.dot(axis)) < 0.001) {
return;
}
target.push(axis.clone());
}
const planeHelper = new Plane();
const normalHelper = new Vector3();
const vectorHelper = new Vector3();
function projectOntoPlane(_axis1, _axis2, initialWorldPoint, worldPointerOrigin, worldPoint, worldDirection) {
normalHelper.crossVectors(_axis1, _axis2).normalize();
planeHelper.setFromNormalAndCoplanarPoint(normalHelper, initialWorldPoint);
const angleDifference = worldDirection == null ? 0 : Math.abs(normalHelper.dot(worldDirection));
if (worldDirection == null || angleDifference < 0.01) {
//project point onto plane
planeHelper.projectPoint(worldPoint, worldPoint);
return;
}
//project ray onto plane
const distanceToPlane = planeHelper.distanceToPoint(worldPointerOrigin);
const distanceAlongDirection = -distanceToPlane / worldDirection.dot(planeHelper.normal);
if (distanceAlongDirection < 0) {
//project point onto plane
planeHelper.projectPoint(worldPoint, worldPoint);
return;
}
vectorHelper.copy(worldPoint);
worldPoint.copy(worldPointerOrigin).addScaledVector(worldDirection, distanceAlongDirection);
}
/**
* finds the intersection between the given axis (infinite line) and another infinite line provided with point and direction
*/
export function projectOntoAxis(initialWorldPoint, axis, worldPointerOrigin, worldPoint, worldDirection) {
const angleDifference = worldDirection == null ? 0 : 1 - Math.abs(axis.dot(worldDirection));
if (worldDirection == null || angleDifference < 0.001) {
projectPointOntoAxis(worldPoint, initialWorldPoint, axis);
return;
}
vectorHelper.subVectors(worldPointerOrigin, initialWorldPoint);
// 2. Calculate the dot products needed
const d1d2 = axis.dot(worldDirection);
const d1p1p2 = axis.dot(vectorHelper);
const d2p1p2 = worldDirection.dot(vectorHelper);
// 3. Calculate the parameters t for the closest points
const denominator = 1 - d1d2 * d1d2;
const t = (d1p1p2 - d1d2 * d2p1p2) / denominator;
const s = (d1d2 * d1p1p2 - d2p1p2) / denominator;
if (s < 0) {
projectPointOntoAxis(worldPoint, initialWorldPoint, axis);
return;
}
vectorHelper.copy(worldPoint);
// 4. Calculate the nearest point on the first line
worldPoint.copy(initialWorldPoint).addScaledVector(axis, t);
}
export function projectPointOntoAxis(target, axisOrigin, axisNormal) {
target.sub(axisOrigin);
projectPointOntoNormal(target, axisNormal);
target.add(axisOrigin);
}
export function projectPointOntoNormal(point, normal) {
const dot = point.dot(normal);
point.copy(normal).multiplyScalar(dot);
}