@babylonjs/core
Version:
Getting started? Play directly with the Babylon.js API using our [playground](https://playground.babylonjs.com/). It also contains a lot of samples to learn how to use it.
234 lines • 13.2 kB
JavaScript
import { Vector3, Quaternion, Matrix, TmpVectors } from "../../Maths/math.vector.js";
import { Observable } from "../../Misc/observable.js";
import { BaseSixDofDragBehavior } from "./baseSixDofDragBehavior.js";
import { TransformNode } from "../../Meshes/transformNode.js";
/**
* A behavior that when attached to a mesh will allow the mesh to be dragged around based on directions and origin of the pointer's ray
*/
export class SixDofDragBehavior extends BaseSixDofDragBehavior {
constructor() {
super(...arguments);
this._sceneRenderObserver = null;
this._targetPosition = new Vector3(0, 0, 0);
this._targetOrientation = new Quaternion();
this._targetScaling = new Vector3(1, 1, 1);
this._startingPosition = new Vector3(0, 0, 0);
this._startingOrientation = new Quaternion();
this._startingScaling = new Vector3(1, 1, 1);
/**
* Fires when position is updated
*/
this.onPositionChangedObservable = new Observable();
/**
* The distance towards the target drag position to move each frame. This can be useful to avoid jitter. Set this to 1 for no delay. (Default: 0.2)
*/
this.dragDeltaRatio = 0.2;
/**
* If the object should rotate to face the drag origin
*/
this.rotateDraggedObject = true;
/**
* If `rotateDraggedObject` is set to `true`, this parameter determines if we are only rotating around the y axis (yaw)
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
this.rotateAroundYOnly = false;
/**
* Should the behavior rotate 1:1 with the motion controller, when one is used.
*/
this.rotateWithMotionController = true;
/**
* Use this flag to update the target but not move the owner node towards the target
*/
this.disableMovement = false;
/**
* Should the object rotate towards the camera when we start dragging it
*/
this.faceCameraOnDragStart = false;
}
/**
* The name of the behavior
*/
get name() {
return "SixDofDrag";
}
/**
* Attaches the six DoF drag behavior
* In XR mode the mesh and its children will have their isNearGrabbable property set to true
* @param ownerNode The mesh that will be dragged around once attached
*/
attach(ownerNode) {
super.attach(ownerNode);
ownerNode.isNearGrabbable = true;
// if it has children, make sure they are grabbable too
const children = ownerNode.getChildMeshes();
for (const m of children) {
m.isNearGrabbable = true;
}
// Node that will save the owner's transform
this._virtualTransformNode = new TransformNode("virtual_sixDof", BaseSixDofDragBehavior._VirtualScene);
this._virtualTransformNode.rotationQuaternion = Quaternion.Identity();
// On every frame move towards target scaling to avoid jitter caused by vr controllers
this._sceneRenderObserver = ownerNode.getScene().onBeforeRenderObservable.add(() => {
if (this.currentDraggingPointerIds.length === 1 && this._moving && !this.disableMovement) {
// 1 pointer only drags mesh
const deltaToAdd = TmpVectors.Vector3[0];
deltaToAdd.copyFrom(this._targetPosition).subtractInPlace(ownerNode.absolutePosition).scaleInPlace(this.dragDeltaRatio);
const deltaToAddTransformed = TmpVectors.Vector3[1];
deltaToAddTransformed.copyFrom(deltaToAdd);
// If the node has a parent, transform the delta to local space, so it can be added to the
// position in local space
if (ownerNode.parent) {
const parentRotationMatrixInverse = TmpVectors.Matrix[0];
ownerNode.parent.absoluteRotationQuaternion.toRotationMatrix(parentRotationMatrixInverse);
parentRotationMatrixInverse.invert();
Vector3.TransformNormalToRef(deltaToAdd, parentRotationMatrixInverse, deltaToAddTransformed);
}
ownerNode.position.addInPlace(deltaToAddTransformed);
this.onPositionChangedObservable.notifyObservers({ position: ownerNode.absolutePosition });
// Only rotate the mesh if it's parent has uniform scaling
if (!ownerNode.parent || (ownerNode.parent.scaling && !ownerNode.parent.scaling.isNonUniformWithinEpsilon(0.001))) {
const rotationToApply = TmpVectors.Quaternion[0];
rotationToApply.copyFrom(this._targetOrientation);
if (ownerNode.parent) {
const parentRotationInverse = TmpVectors.Quaternion[0];
parentRotationInverse.copyFrom(ownerNode.parent.absoluteRotationQuaternion);
parentRotationInverse.invertInPlace();
parentRotationInverse.multiplyToRef(this._targetOrientation, rotationToApply);
}
Quaternion.SlerpToRef(ownerNode.rotationQuaternion, rotationToApply, this.dragDeltaRatio, ownerNode.rotationQuaternion);
}
}
});
}
_getPositionOffsetAround(transformationLocalOrigin, scaling, rotation) {
const translationMatrix = TmpVectors.Matrix[0]; // T
const translationMatrixInv = TmpVectors.Matrix[1]; // T'
const rotationMatrix = TmpVectors.Matrix[2]; // R
const scaleMatrix = TmpVectors.Matrix[3]; // S
const finalMatrix = TmpVectors.Matrix[4]; // T' x R x S x T
Matrix.TranslationToRef(transformationLocalOrigin.x, transformationLocalOrigin.y, transformationLocalOrigin.z, translationMatrix); // T
Matrix.TranslationToRef(-transformationLocalOrigin.x, -transformationLocalOrigin.y, -transformationLocalOrigin.z, translationMatrixInv); // T'
Matrix.FromQuaternionToRef(rotation, rotationMatrix); // R
Matrix.ScalingToRef(scaling, scaling, scaling, scaleMatrix);
translationMatrixInv.multiplyToRef(rotationMatrix, finalMatrix); // T' x R
finalMatrix.multiplyToRef(scaleMatrix, finalMatrix); // T' x R x S
finalMatrix.multiplyToRef(translationMatrix, finalMatrix); // T' x R x S x T
return finalMatrix.getTranslation();
}
_onePointerPositionUpdated(worldDeltaPosition, worldDeltaRotation) {
const pointerDelta = TmpVectors.Vector3[0];
pointerDelta.setAll(0);
if (this._dragging === this._dragType.DRAG) {
if (this.rotateDraggedObject) {
if (this.rotateAroundYOnly) {
// Convert change in rotation to only y axis rotation
Quaternion.RotationYawPitchRollToRef(worldDeltaRotation.toEulerAngles().y, 0, 0, TmpVectors.Quaternion[0]);
}
else {
TmpVectors.Quaternion[0].copyFrom(worldDeltaRotation);
}
TmpVectors.Quaternion[0].multiplyToRef(this._startingOrientation, this._targetOrientation);
}
}
else if (this._dragging === this._dragType.NEAR_DRAG || (this._dragging === this._dragType.DRAG_WITH_CONTROLLER && this.rotateWithMotionController)) {
worldDeltaRotation.multiplyToRef(this._startingOrientation, this._targetOrientation);
}
this._targetPosition.copyFrom(this._startingPosition).addInPlace(worldDeltaPosition);
}
_twoPointersPositionUpdated() {
const startingPosition0 = this._virtualMeshesInfo[this.currentDraggingPointerIds[0]].startingPosition;
const startingPosition1 = this._virtualMeshesInfo[this.currentDraggingPointerIds[1]].startingPosition;
const startingCenter = TmpVectors.Vector3[0];
startingPosition0.addToRef(startingPosition1, startingCenter);
startingCenter.scaleInPlace(0.5);
const startingVector = TmpVectors.Vector3[1];
startingPosition1.subtractToRef(startingPosition0, startingVector);
const currentPosition0 = this._virtualMeshesInfo[this.currentDraggingPointerIds[0]].dragMesh.absolutePosition;
const currentPosition1 = this._virtualMeshesInfo[this.currentDraggingPointerIds[1]].dragMesh.absolutePosition;
const currentCenter = TmpVectors.Vector3[2];
currentPosition0.addToRef(currentPosition1, currentCenter);
currentCenter.scaleInPlace(0.5);
const currentVector = TmpVectors.Vector3[3];
currentPosition1.subtractToRef(currentPosition0, currentVector);
const scaling = currentVector.length() / startingVector.length();
const translation = currentCenter.subtract(startingCenter);
const rotationQuaternion = Quaternion.FromEulerAngles(0, Vector3.GetAngleBetweenVectorsOnPlane(startingVector.normalize(), currentVector.normalize(), Vector3.UpReadOnly), 0);
const oldParent = this._ownerNode.parent;
this._ownerNode.setParent(null);
const positionOffset = this._getPositionOffsetAround(startingCenter.subtract(this._virtualTransformNode.getAbsolutePivotPoint()), scaling, rotationQuaternion);
this._virtualTransformNode.rotationQuaternion.multiplyToRef(rotationQuaternion, this._ownerNode.rotationQuaternion);
this._virtualTransformNode.scaling.scaleToRef(scaling, this._ownerNode.scaling);
this._virtualTransformNode.position.addToRef(translation.addInPlace(positionOffset), this._ownerNode.position);
this.onPositionChangedObservable.notifyObservers({ position: this._ownerNode.position });
this._ownerNode.setParent(oldParent);
}
_targetDragStart() {
const pointerCount = this.currentDraggingPointerIds.length;
if (!this._ownerNode.rotationQuaternion) {
this._ownerNode.rotationQuaternion = Quaternion.RotationYawPitchRoll(this._ownerNode.rotation.y, this._ownerNode.rotation.x, this._ownerNode.rotation.z);
}
const worldPivot = this._ownerNode.getAbsolutePivotPoint();
if (pointerCount === 1) {
this._targetPosition.copyFrom(this._ownerNode.absolutePosition);
this._targetOrientation.copyFrom(this._ownerNode.rotationQuaternion);
this._targetScaling.copyFrom(this._ownerNode.absoluteScaling);
if (this.faceCameraOnDragStart && this._scene.activeCamera) {
const toCamera = TmpVectors.Vector3[0];
this._scene.activeCamera.position.subtractToRef(worldPivot, toCamera);
toCamera.normalize();
const quat = TmpVectors.Quaternion[0];
if (this._scene.useRightHandedSystem) {
Quaternion.FromLookDirectionRHToRef(toCamera, new Vector3(0, 1, 0), quat);
}
else {
Quaternion.FromLookDirectionLHToRef(toCamera, new Vector3(0, 1, 0), quat);
}
quat.normalize();
Quaternion.RotationYawPitchRollToRef(quat.toEulerAngles().y, 0, 0, TmpVectors.Quaternion[0]);
this._targetOrientation.copyFrom(TmpVectors.Quaternion[0]);
}
this._startingPosition.copyFrom(this._targetPosition);
this._startingOrientation.copyFrom(this._targetOrientation);
this._startingScaling.copyFrom(this._targetScaling);
}
else if (pointerCount === 2) {
this._virtualTransformNode.setPivotPoint(new Vector3(0, 0, 0), 0 /* Space.LOCAL */);
this._virtualTransformNode.position.copyFrom(this._ownerNode.absolutePosition);
this._virtualTransformNode.scaling.copyFrom(this._ownerNode.absoluteScaling);
this._virtualTransformNode.rotationQuaternion.copyFrom(this._ownerNode.absoluteRotationQuaternion);
this._virtualTransformNode.setPivotPoint(worldPivot, 1 /* Space.WORLD */);
this._resetVirtualMeshesPosition();
}
}
_targetDrag(worldDeltaPosition, worldDeltaRotation) {
if (this.currentDraggingPointerIds.length === 1) {
this._onePointerPositionUpdated(worldDeltaPosition, worldDeltaRotation);
}
else if (this.currentDraggingPointerIds.length === 2) {
this._twoPointersPositionUpdated();
}
}
_targetDragEnd() {
if (this.currentDraggingPointerIds.length === 1) {
// We still have 1 active pointer, we must simulate a dragstart with a reseted position/orientation
this._resetVirtualMeshesPosition();
const previousFaceCameraFlag = this.faceCameraOnDragStart;
this.faceCameraOnDragStart = false;
this._targetDragStart();
this.faceCameraOnDragStart = previousFaceCameraFlag;
}
}
/**
* Detaches the behavior from the mesh
*/
detach() {
super.detach();
if (this._ownerNode) {
this._ownerNode.getScene().onBeforeRenderObservable.remove(this._sceneRenderObserver);
}
if (this._virtualTransformNode) {
this._virtualTransformNode.dispose();
}
}
}
//# sourceMappingURL=sixDofDragBehavior.js.map