babylonjs-editcontrol
Version:
A transform control for Babylonjs mesh
1,302 lines (1,150 loc) • 81.4 kB
text/typescript
import {
AbstractMesh,
Axis,
BoundingBox,
Camera,
Color3,
LinesMesh,
Material,
Matrix,
Mesh,
MeshBuilder,
Node,
PickingInfo,
Quaternion,
Scene,
Space,
StandardMaterial,
Vector3,
TransformNode,
Engine,
DeepImmutableObject,
UtilityLayerRenderer
}
from 'babylonjs';
enum ActionType {
TRANS = 0,
ROT = 1,
SCALE = 2
}
/**
* Draws a transform widget at the mesh's location (its pivot location).
* The widget transforms(translates,rotates and scales) the mesh based on user
* interactions with the widget.
* The widget shows the mesh position and rotation at any time.
* The widget follows the mesh constantly.
* Note: An alternate approach would have been for the mesh to follow the widget.
* The problem with the alternate approach - syncing the transforms
* if the mesh was being transformed by entities other than the widget say physics
* or script for example.
*
*/
export class EditControl {
private _mesh: TransformNode;
private _canvas: HTMLCanvasElement;
private _scene: Scene;
private _utilLayer: UtilityLayerRenderer;
private _mainCamera: Camera;
//root of the edit control
private _ecRoot: Mesh;
private _local: boolean = true;
private _snapT: boolean = false;
private _snapR: boolean = false;
private _transSnap: number = 1;
private _rotSnap: number = Math.PI / 18;
private _axesLen: number = 0.4;
private _axesScale: number = 1;
//how close to an axis should we get before we can pick it
private _pickWidth: number = 0.02;
private _redMat: StandardMaterial;
private _greenMat: StandardMaterial;
private _blueMat: StandardMaterial;
private _whiteMat: StandardMaterial;
private _yellowMat: StandardMaterial;
private _redCol: Color3 = new Color3(1, 0.2, 0.2);
private _greenCol: Color3 = new Color3(0.2, 1, 0.2);
private _blueCol: Color3 = new Color3(0.2, 0.2, 1);
private _whiteCol: Color3 = new Color3(1, 1, 1);
private _yellowCol: Color3 = new Color3(1, 1, 0.2);
private _actHist: ActHist;
private _renderer: () => void;
private _pointerdown: EventListener;
private _pointerup: EventListener;
private _pointermove: EventListener;
//axes visibility
private _visibility: number = 0.75;
//lhs-rhs issue. lhs mesh in rhs or rhs mesh in lhs
private _lhsRhs: boolean = false;
public constructor(mesh: TransformNode, camera: Camera, canvas: HTMLCanvasElement, scale?: number, eulerian?: boolean, pickWidth?: number) {
this._mesh = mesh;
this._mainCamera = camera;
this._canvas = canvas;
if (scale != null) {
this._axesScale = scale;
}
if (eulerian !== null) {
this._eulerian = eulerian;
} else {
this._eulerian = false;
}
this._checkQuaternion();
if (pickWidth != null) {
this._pickWidth = pickWidth;
}
this._utilLayer = UtilityLayerRenderer.DefaultUtilityLayer;
this._utilLayer.onlyCheckPointerDownEvents = false;
this._scene = this._utilLayer.utilityLayerScene;
this._actHist = new ActHist(mesh, 10);
mesh.computeWorldMatrix(true);
this._boundingDimesion = this._getBoundingDimension(mesh);
this._setLocalAxes(mesh);
this._lhsRhs = this._check_LHS_RHS(mesh);
console.log("lhs rhs issue " + this._lhsRhs);
//build the edit control axes
this._ecRoot = new Mesh("", this._scene);
this._ecRoot.rotationQuaternion = Quaternion.Identity();
this._ecRoot.visibility = 0;
this._ecRoot.isPickable = false;
this._createMaterials(this._scene);
let guideAxes: Mesh = this._createCommonAxes();
guideAxes.parent = this._ecRoot;
//build the pickplanes
let pickPlanes: Mesh = this._createPickPlanes();
pickPlanes.parent = this._ecRoot;
this._pointerdown = (evt) => { return this._onPointerDown(evt) };
this._pointerup = (evt) => { return this._onPointerUp(evt) };
this._pointermove = (evt) => { return this._onPointerMove(evt) };
//use canvas rather than scene to handle pointer events
//scene cannot have mutiple eventlisteners for an event
//with canvas one will have to do ones own pickinfo generation.
canvas.addEventListener("pointerdown", this._pointerdown, false);
canvas.addEventListener("pointerup", this._pointerup, false);
canvas.addEventListener("pointermove", this._pointermove, false);
this._renderer = () => { return this._renderLoopProcess() };
this._scene.registerBeforeRender(this._renderer);
}
public getRoot(): AbstractMesh {
return this._ecRoot;
}
//make sure that if eulerian is set to false then mesh's rotation is in quaternion
//throw error and exit if not so.
private _checkQuaternion() {
if (!this._eulerian) {
if ((this._mesh.rotationQuaternion == null) || (this._mesh.rotationQuaternion == undefined)) {
throw "Error: Eulerian is set to false but the mesh's rotationQuaternion is not set.";
}
}
}
/**
* checks if a have left hand , right hand issue.
* In other words if a mesh is a LHS mesh in RHS system or
* a RHS mesh in LHS system
* The X axis will be reversed in such cases.
* thus Cross product of X and Y should be inverse of Z.
*
* if no parent then we are ok.
* If parent and parent has issue then we have issue.
*
*/
private _check_LHS_RHS(mesh: TransformNode) {
let _issue: boolean = false;
let root: Node = mesh.parent;
if (root == null) return false;
this._setLocalAxes(root);
let actualZ = Vector3.Cross(this._localX, this._localY);
//same direction or opposite direction of Z
if (Vector3.Dot(actualZ, this._localZ) < 0) _issue = true;
else _issue = false;
this._setLocalAxes(mesh);
return _issue;
}
private _ecMatrix: Matrix = new Matrix();
//edit control to camera vector
private _ecTOcamera: Vector3 = new Vector3(0, 0, 0);
private _renderLoopProcess() {
//sync the edit control position and rotation with that of mesh
this._ecRoot.position = this._mesh.getAbsolutePivotPoint();
this._setECRotation();
//scale the EditControl so it seems at the same distance from camera/user
this._setECScale();
//rotate the free move,rotate,scale pick plane to face the camera/user
if (this._local) {
this._ecRoot.getWorldMatrix().invertToRef(this._ecMatrix);
Vector3.TransformCoordinatesToRef(this._mainCamera.position, this._ecMatrix, this._ecTOcamera);
//note pALL is child of ecRoot hence lookAt in local space
this._pALL.lookAt(this._ecTOcamera, 0, 0, 0, Space.LOCAL);
} else {
this._mainCamera.position.subtractToRef(this._ecRoot.position, this._ecTOcamera);
this._pALL.lookAt(this._mainCamera.position, 0, 0, 0, Space.WORLD);
}
//rotate the rotation and planar guide to face the camera/user
if (this._rotEnabled) {
this._rotRotGuides();
}
else if (this._transEnabled) this._rotPlanarGuides(this._tXZ, this._tZY, this._tYX);
else if (this._scaleEnabled) this._rotPlanarGuides(this._sXZ, this._sZY, this._sYX);
//check pointer over axes only during pointer moves
//this.onPointerOver();
}
/**
* sets rotaion of edit control to that of the mesh
*/
private _setECRotation() {
if (this._local) {
if (this._mesh.parent == null) {
if (this._eulerian) {
let rot: Vector3 = this._mesh.rotation;
Quaternion.RotationYawPitchRollToRef(rot.y, rot.x, rot.z, this._ecRoot.rotationQuaternion);
} else {
this._ecRoot.rotationQuaternion.copyFrom(this._mesh.rotationQuaternion);
}
} else {
if (this._isScaleUnEqual(this._mesh)) return;
this._mesh.getWorldMatrix().getRotationMatrixToRef(this._tm);
Quaternion.FromRotationMatrixToRef(this._tm, this._ecRoot.rotationQuaternion);
//this._ecRoot.rotationQuaternion.normalize();
}
}
}
/**
* checks if any of the mesh's ancestors has non uniform scale
*/
private _isScaleUnEqual(mesh: TransformNode): boolean {
if (mesh.parent == null) return false;
while (mesh.parent != null) {
if (((<Mesh>mesh.parent).scaling.x != (<Mesh>mesh.parent).scaling.y ||
(<Mesh>mesh.parent).scaling.y != (<Mesh>mesh.parent).scaling.z)) {
return true;
} else {
mesh = <Mesh>mesh.parent;
}
}
return false;
}
//how far away from camera should the edit control appear to be
private _distFromCamera: number = 2;
//vector from camera to edit control
private _cameraTOec: Vector3 = new Vector3(0, 0, 0);
private _cameraNormal: Vector3 = new Vector3(0, 0, 0);
private _setECScale() {
this._ecRoot.position.subtractToRef(this._mainCamera.position, this._cameraTOec);
Vector3.FromArrayToRef(<DeepImmutableObject<Float32Array>>this._mainCamera.getWorldMatrix().asArray(), 8, this._cameraNormal);
//get distance of edit control from the camera plane
//project "camera to edit control" vector onto the camera normal
let parentOnNormal: number = Vector3.Dot(this._cameraTOec, this._cameraNormal) / this._cameraNormal.length();
let s: number = Math.abs(parentOnNormal / this._distFromCamera);
Vector3.FromFloatsToRef(s, s, s, this._ecRoot.scaling);
//Vector3.FromFloatsToRef(s,s,s,this.pALL.scaling);
}
//rotate the rotation guides so that they are facing the camera
private _rotRotGuides() {
let rotX = Math.atan(this._ecTOcamera.y / this._ecTOcamera.z);
if (this._ecTOcamera.z >= 0) {
this._rX.rotation.x = -rotX;
} else {
this._rX.rotation.x = -rotX - Math.PI;
}
let rotY = Math.atan(this._ecTOcamera.x / this._ecTOcamera.z);
if (this._ecTOcamera.z >= 0) {
this._rY.rotation.y = rotY;
} else {
this._rY.rotation.y = rotY + Math.PI;
}
let rotZ = Math.atan(this._ecTOcamera.x / this._ecTOcamera.y);
if (this._ecTOcamera.y >= 0) {
this._rZ.rotation.z = -rotZ;
} else {
this._rZ.rotation.z = -rotZ - Math.PI;
}
}
/**
* rotate the planar guide so that they are facing the camera
*/
private _rotPlanarGuides(XZ: Mesh, ZY: Mesh, YX: Mesh) {
let ec: Vector3 = this._ecTOcamera;
XZ.rotation.x = 0;
XZ.rotation.y = 0;
XZ.rotation.z = 0;
ZY.rotation.x = 0;
ZY.rotation.y = 0;
ZY.rotation.z = 0;
YX.rotation.x = 0;
YX.rotation.y = 0;
YX.rotation.z = 0;
if (ec.x <= 0 && ec.y >= 0 && ec.z >= 0) {
XZ.rotation.z = 3.14;
YX.rotation.y = 3.14;
} else if (ec.x <= 0 && ec.y >= 0 && ec.z <= 0) {
XZ.rotation.y = 3.14;
ZY.rotation.y = 3.14;
YX.rotation.y = 3.14;
} else if (ec.x >= 0 && ec.y >= 0 && ec.z <= 0) {
XZ.rotation.x = 3.14;
ZY.rotation.y = 3.14;
} else if (ec.x >= 0 && ec.y <= 0 && ec.z >= 0) {
ZY.rotation.z = 3.14;
YX.rotation.x = 3.14;
} else if (ec.x <= 0 && ec.y <= 0 && ec.z >= 0) {
XZ.rotation.z = 3.14;
ZY.rotation.z = 3.14;
YX.rotation.z = 3.14;
} else if (ec.x <= 0 && ec.y <= 0 && ec.z <= 0) {
XZ.rotation.y = 3.14;
ZY.rotation.x = 3.14;
YX.rotation.z = 3.14;
} else if (ec.x >= 0 && ec.y <= 0 && ec.z <= 0) {
XZ.rotation.x = 3.14;
ZY.rotation.x = 3.14;
YX.rotation.x = 3.14;
}
}
public switchTo(mesh: TransformNode, eulerian?: boolean) {
mesh.computeWorldMatrix(true);
this._mesh = mesh;
if (eulerian != null) {
this._eulerian = eulerian;
}
this._checkQuaternion();
this._setLocalAxes(mesh);
this._actHist = new ActHist(mesh, 10);
this._lhsRhs = this._check_LHS_RHS(mesh);
this.refreshBoundingInfo();
}
public switchCamera(camera: Camera) {
this._mainCamera = camera;
}
public setUndoCount(c: number) {
this._actHist.setCapacity(c);
}
public undo() {
let at: number = this._actHist.undo();
this._mesh.computeWorldMatrix(true);
this._setLocalAxes(this._mesh);
this._callActionStartListener(at);
this._callActionListener(at);
this._callActionEndListener(at);
}
public redo() {
let at: number = this._actHist.redo();
this._mesh.computeWorldMatrix(true);
this._setLocalAxes(this._mesh);
this._callActionStartListener(at);
this._callActionListener(at);
this._callActionEndListener(at);
}
/**
* detach the edit control from the mesh and dispose off all
* resources created by the edit control
*/
public detach() {
this._canvas.removeEventListener("pointerdown", this._pointerdown, false);
this._canvas.removeEventListener("pointerup", this._pointerup, false);
this._canvas.removeEventListener("pointermove", this._pointermove, false);
this._scene.unregisterBeforeRender(this._renderer);
this.removeAllActionListeners();
this._disposeAll();
}
private _prevState: String = "";
private _hidden: boolean = false;
/**
* hide the edit control. use show() to unhide the control.
*/
public hide() {
this._hidden = true;
if (this._transEnabled) {
this._prevState = "T";
this.disableTranslation();
} else if (this._rotEnabled) {
this._prevState = "R";
this.disableRotation();
} else if (this._scaleEnabled) {
this._prevState = "S";
this.disableScaling();
}
this._hideCommonAxes();
}
private _hideCommonAxes() {
this._xaxis.visibility = 0;
this._yaxis.visibility = 0;
this._zaxis.visibility = 0;
}
private _showCommonAxes() {
this._xaxis.visibility = this._visibility;
this._yaxis.visibility = this._visibility;
this._zaxis.visibility = this._visibility;
}
/**
* unhide the editcontrol hidden using the hide() method
*/
public show() {
this._hidden = false;
this._showCommonAxes();
if (this._prevState == "T") this.enableTranslation();
else if (this._prevState == "R") this.enableRotation();
else if (this._prevState == "S") this.enableScaling();
}
/**
* check if the editcontrol was hidden using the hide() methods
*/
public isHidden(): boolean {
return this._hidden;
}
private _disposeAll() {
this._ecRoot.dispose();
this._disposeMaterials();
this._actHist = null;
}
private _actionListener: (actionType: number) => void = null;
private _actionStartListener: (actionType: number) => void = null;
private _actionEndListener: (actionType: number) => void = null;
public addActionListener(actionListener: (actionType: number) => void) {
this._actionListener = actionListener;
}
public removeActionListener() {
this._actionListener = null;
}
public addActionStartListener(actionStartListener: (actionType: number) => void) {
this._actionStartListener = actionStartListener;
}
public removeActionStartListener() {
this._actionStartListener = null;
}
public addActionEndListener(actionEndListener: (actionType: number) => void) {
this._actionEndListener = actionEndListener;
}
public removeActionEndListener() {
this._actionEndListener = null;
}
public removeAllActionListeners() {
this._actionListener = null;
this._actionStartListener = null;
this._actionEndListener = null;
}
private _pDown: boolean = false;
private _axisPicked: Mesh;
private _onPointerDown(evt: Event) {
evt.preventDefault();
this._pDown = true;
if ((<PointerEvent>evt).button != 0) return;
let engine: Engine = this._scene.getEngine();
let x = (engine.isPointerLock) ? this._canvas.width * 0.5 : this._scene.pointerX;
let y = (engine.isPointerLock) ? this._canvas.height * 0.5 : this._scene.pointerY;
let pickResult: PickingInfo = this._scene.pick(x, y, (mesh) => {
if (this._transEnabled) {
if ((mesh == this._tX) || (mesh == this._tY) || (mesh == this._tZ) || (mesh == this._tXZ) || (mesh == this._tZY) || (mesh == this._tYX) || (mesh == this._tAll)) return true;
} else if ((this._rotEnabled)) {
if ((mesh == this._rX) || (mesh == this._rY) || (mesh == this._rZ) || (mesh == this._rAll)) return true;
} else if ((this._scaleEnabled)) {
if ((mesh == this._sX) || (mesh == this._sY) || (mesh == this._sZ) || (mesh == this._sXZ) || (mesh == this._sZY) || (mesh == this._sYX) || (mesh == this._sAll)) return true;
}
return false;
}, false, this._mainCamera);
if (pickResult.hit) {
//this.setAxesVisiblity(0);
this._axisPicked = <Mesh>pickResult.pickedMesh;
let childs: Node[] = this._axisPicked.getChildren();
if (childs.length > 0) {
(<Mesh>childs[0]).visibility = this._visibility;
} else {
this._axisPicked.visibility = this._visibility;
}
let name: string = this._axisPicked.name;
if ((name == "X")) this._bXaxis.visibility = 1;
else if ((name == "Y")) this._bYaxis.visibility = 1;
else if ((name == "Z")) this._bZaxis.visibility = 1;
else if ((name == "XZ")) {
this._bXaxis.visibility = 1;
this._bZaxis.visibility = 1;
} else if ((name == "ZY")) {
this._bZaxis.visibility = 1;
this._bYaxis.visibility = 1;
} else if ((name == "YX")) {
this._bYaxis.visibility = 1;
this._bXaxis.visibility = 1;
} else if ((name == "ALL")) {
this._bXaxis.visibility = 1;
this._bYaxis.visibility = 1;
this._bZaxis.visibility = 1;
}
this._setEditing(true);
//lets find out where we are on the pickplane
this._pickedPlane = this._getPickPlane(this._axisPicked);
if (this._pickedPlane != null) {
this._prevPos = this._getPosOnPickPlane();
} else {
this._prevPos = null;
}
window.setTimeout(((cam, can) => { return this._detachCamera(cam, can) }), 0, this._mainCamera, this._canvas);
}
}
private _setEditing(editing: boolean) {
this._editing = editing;
if (editing) {
this._setActionType();
if (this._actionType == ActionType.ROT) {
this._snapRA = 0;
}
this._callActionStartListener(this._actionType);
} else {
this._callActionEndListener(this._actionType);
}
}
public isEditing(): boolean {
return this._editing;
}
/**
* no camera movement during edit
*/
private _detachCamera(cam: Object, can: Object) {
let camera: Camera = <Camera>cam;
let canvas: HTMLCanvasElement = <HTMLCanvasElement>can;
let engine: Engine = this._scene.getEngine();
if (!engine.isPointerLock) {
camera.detachControl(canvas)
}
}
private _prevOverMesh: Mesh;
private _pointerIsOver: boolean = false;
public isPointerOver(): boolean {
return this._pointerIsOver;
}
private _savedMat: Material;
private _savedCol: Color3;
private _onPointerOver() {
//if(this.pDown) return;
let engine: Engine = this._scene.getEngine();
let x = (engine.isPointerLock) ? this._canvas.width * 0.5 : this._scene.pointerX;
let y = (engine.isPointerLock) ? this._canvas.height * 0.5 : this._scene.pointerY;
let pickResult: PickingInfo = this._scene.pick(x, y, (mesh) => {
if (this._transEnabled) {
if ((mesh == this._tX) || (mesh == this._tY) || (mesh == this._tZ) || (mesh == this._tXZ) || (mesh == this._tZY) || (mesh == this._tYX) || (mesh == this._tAll)) return true;
} else if ((this._rotEnabled)) {
if ((mesh == this._rX) || (mesh == this._rY) || (mesh == this._rZ) || (mesh == this._rAll)) return true;
} else if (this._scaleEnabled) {
if ((mesh == this._sX) || (mesh == this._sY) || (mesh == this._sZ) || (mesh == this._sXZ) || (mesh == this._sZY) || (mesh == this._sYX) || (mesh == this._sAll)) return true;
}
return false;
}, false, this._mainCamera);
if (pickResult.hit) {
//if we are still over the same axis mesh then don't do anything
if (<Mesh>pickResult.pickedMesh != this._prevOverMesh) {
this._pointerIsOver = true;
//if we moved directly from one axis mesh to this then clean up the prev axis mesh
this._clearPrevOverMesh();
this._prevOverMesh = <Mesh>pickResult.pickedMesh;
if (this._rotEnabled) {
this._savedCol = (<LinesMesh>this._prevOverMesh.getChildren()[0]).color;
(<LinesMesh>this._prevOverMesh.getChildren()[0]).color = this._whiteCol;
} else {
let childs: Node[] = this._prevOverMesh.getChildren();
if (childs.length > 0) {
this._savedMat = (<Mesh>childs[0]).material;
(<Mesh>childs[0]).material = this._whiteMat;
} else {
this._savedMat = this._prevOverMesh.material;
this._prevOverMesh.material = this._whiteMat;
}
}
if (this._prevOverMesh.name == "X") {
this._xaxis.color = this._whiteCol;
} else if (this._prevOverMesh.name == "Y") {
this._yaxis.color = this._whiteCol;
} else if (this._prevOverMesh.name == "Z") {
this._zaxis.color = this._whiteCol;
}
}
} else {
this._pointerIsOver = false;
if (this._prevOverMesh != null) {
this._restoreColor(this._prevOverMesh);
this._prevOverMesh = null;
}
}
}
//clean up any axis we might have been howering over before
private _clearPrevOverMesh() {
if (this._prevOverMesh != null) {
this._prevOverMesh.visibility = 0;
this._restoreColor(this._prevOverMesh);
}
}
private _restoreColor(mesh: Mesh) {
switch (mesh.name) {
case "X":
this._xaxis.color = this._redCol;
break;
case "Y":
this._yaxis.color = this._greenCol;
break;
case "Z":
this._zaxis.color = this._blueCol;
break;
}
if (this._rotEnabled) {
(<LinesMesh>mesh.getChildren()[0]).color = this._savedCol;
} else {
let childs: Node[] = mesh.getChildren();
if (childs.length > 0) {
(<Mesh>childs[0]).material = this._savedMat;
} else {
mesh.material = this._savedMat;
}
}
}
private _editing: boolean = false;
private _onPointerUp(evt: Event) {
this._pDown = false;
if (this._editing) {
let engine: Engine = this._scene.getEngine();
if (!engine.isPointerLock) {
this._mainCamera.attachControl(true);
}
this._setEditing(false);
//this.setAxesVisiblity(1);
this._hideBaxis();
if (this._prevOverMesh != null) {
this._restoreColor(this._prevOverMesh);
this._prevOverMesh = null;
}
this._actHist.add(this._actionType);
}
}
private _actionType: number;
private _setActionType() {
if (this._transEnabled) {
this._actionType = ActionType.TRANS;
} else if ((this._rotEnabled)) {
this._actionType = ActionType.ROT;
} else if ((this._scaleEnabled)) {
this._actionType = ActionType.SCALE;
}
}
private _callActionListener(at: number) {
//call actionListener if registered
if (this._actionListener != null) {
this._actionListener(at);
}
}
private _callActionStartListener(at: number) {
//call actionListener if registered
if (this._actionStartListener != null) {
this._actionStartListener(at);
}
}
private _callActionEndListener(at: number) {
//call actionListener if registered
if (this._actionEndListener != null) {
this._actionEndListener(at);
}
}
private _prevPos: Vector3;
private _onPointerMove(evt: Event) {
if (!this._pDown) {
this._onPointerOver();
return;
}
if (!this._editing) return;
if (this._prevPos == null) return;
let newPos: Vector3 = this._getPosOnPickPlane();
if (newPos == null) return;
if (this._rotEnabled) {
this._doRotation(this._mesh, this._axisPicked, newPos, this._prevPos);
} else {
let diff: Vector3 = newPos.subtract(this._prevPos);
if (diff.x == 0 && diff.y == 0 && diff.z == 0) return;
if (this._transEnabled) {
this._doTranslation(diff);
} else {
if (this._scaleEnabled && this._local) this._doScaling(diff);
}
}
this._prevPos = newPos;
this._callActionListener(this._actionType);
}
//rotate differently if camera is too close to the rotation plane
private _rotate2: boolean = false;
private _getPickPlane(axis: Mesh): Mesh {
let n: string = axis.name;
if (this._transEnabled || this._scaleEnabled) {
if (n == "XZ") return this._pXZ;
else if (n == "ZY") return this._pZY;
else if (n == "YX") return this._pYX;
else if (n == "ALL") return this._pALL;
else {
//get the position of camera in the edit control frame of reference
this._ecRoot.getWorldMatrix().invertToRef(this._ecMatrix);
Vector3.TransformCoordinatesToRef(this._mainCamera.position, this._ecMatrix, this._ecTOcamera);
let c = this._ecTOcamera;
if (n === "X") {
if (Math.abs(c.y) > Math.abs(c.z)) {
return this._pXZ;
} else return this._pYX;
} else if (n === "Z") {
if (Math.abs(c.y) > Math.abs(c.x)) {
return this._pXZ;
} else return this._pZY;
} else if (n === "Y") {
if (Math.abs(c.z) > Math.abs(c.x)) {
return this._pYX;
} else return this._pZY;
}
}
} else if (this._rotEnabled) {
this._rotate2 = false;
//get the position of camera in the edit control frame of reference
this._ecRoot.getWorldMatrix().invertToRef(this._ecMatrix);
Vector3.TransformCoordinatesToRef(this._mainCamera.position, this._ecMatrix, this._ecTOcamera);
let c = this._ecTOcamera;
//if camera is too close to the rotation plane then use alternate rotation process
switch (n) {
case "X":
if (Math.abs(c.x) < 0.2) {
this._rotate2 = true;
return this._pALL;
} else return this._pZY;
case "Y":
if (Math.abs(c.y) < 0.2) {
this._rotate2 = true;
return this._pALL;
} else return this._pXZ;
case "Z":
if (Math.abs(c.z) < 0.2) {
this._rotate2 = true;
return this._pALL;
} else return this._pYX;
default:
return this._pALL;
}
} else return null;
}
//TODO when translating, the orientation of pALL keeps changing
//TODo this is not so with rotation or scaling
//TODO so for translation instead of pALL maybe we should use the camera view plane for picking
private _transBy: Vector3 = new Vector3(0, 0, 0);
private _doTranslation(diff: Vector3) {
if ((this._mesh.parent != null) && this._isScaleUnEqual(this._mesh)) {
this._setLocalAxes(this._ecRoot);
} else {
this._setLocalAxes(this._mesh);
}
let n: string = this._axisPicked.name;
this._transBy.x = 0; this._transBy.y = 0; this._transBy.z = 0;
if ((n == "X") || (n == "XZ") || (n == "YX") || (n == "ALL")) {
if (this._local) this._transBy.x = Vector3.Dot(diff, this._localX) / this._localX.length();
else this._transBy.x = diff.x;
}
if ((n == "Y") || (n == "ZY") || (n == "YX") || (n == "ALL")) {
if (this._local) this._transBy.y = Vector3.Dot(diff, this._localY) / this._localY.length();
else this._transBy.y = diff.y;
}
if ((n == "Z") || (n == "XZ") || (n == "ZY") || (n == "ALL")) {
if (this._local) this._transBy.z = Vector3.Dot(diff, this._localZ) / this._localZ.length();
else this._transBy.z = diff.z;
}
this._transWithSnap(this._mesh, this._transBy, this._local);
// bound the translation
if (this._transBoundsMin) {
this._mesh.position.x = Math.max(this._mesh.position.x, this._transBoundsMin.x);
this._mesh.position.y = Math.max(this._mesh.position.y, this._transBoundsMin.y);
this._mesh.position.z = Math.max(this._mesh.position.z, this._transBoundsMin.z);
}
if (this._transBoundsMax) {
this._mesh.position.x = Math.min(this._mesh.position.x, this._transBoundsMax.x);
this._mesh.position.y = Math.min(this._mesh.position.y, this._transBoundsMax.y);
this._mesh.position.z = Math.min(this._mesh.position.z, this._transBoundsMax.z);
}
this._mesh.computeWorldMatrix(true);
}
private _snapTV: Vector3 = new Vector3(0, 0, 0);
private _transWithSnap(mesh: TransformNode, trans: Vector3, local: boolean) {
if (this._snapT) {
let snapit: boolean = false;
this._snapTV.addInPlace(trans);
if (Math.abs(this._snapTV.x) > this._tSnap.x) {
if (this._snapTV.x > 0) trans.x = this._tSnap.x; else trans.x = -this._tSnap.x;
snapit = true;
}
if (Math.abs(this._snapTV.y) > this._tSnap.y) {
if (this._snapTV.y > 0) trans.y = this._tSnap.y; else trans.y = -this._tSnap.y;
snapit = true;
}
if (Math.abs(this._snapTV.z) > this._tSnap.z) {
if (this._snapTV.z > 0) trans.z = this._tSnap.z; else trans.z = -this._tSnap.z;
snapit = true;
}
if (snapit) {
if (Math.abs(trans.x) !== this._tSnap.x) trans.x = 0;
if (Math.abs(trans.y) !== this._tSnap.y) trans.y = 0;
if (Math.abs(trans.z) !== this._tSnap.z) trans.z = 0;
Vector3.FromFloatsToRef(0, 0, 0, this._snapTV);
snapit = false;
} else {
return;
}
}
if (local) {
//locallyTranslate moves the mesh wrt the absolute location not pivotlocation :(
//this.mesh.locallyTranslate(trans);
//
this._localX.normalizeToRef(this._tv1);
this._localY.normalizeToRef(this._tv2);
this._localZ.normalizeToRef(this._tv3);
this._mesh.translate(this._tv1, trans.x, Space.WORLD);
this._mesh.translate(this._tv2, trans.y, Space.WORLD);
this._mesh.translate(this._tv3, trans.z, Space.WORLD);
} else {
if (this._mesh.parent == null) {
this._mesh.position.addInPlace(trans);
} else {
this._mesh.setAbsolutePosition(trans.addInPlace(this._mesh.absolutePosition));
}
}
}
private _snapS: boolean = false;
private _snapSV: Vector3 = new Vector3(0, 0, 0);
private _scaleSnap: number = 0.25;
private _scale: Vector3 = new Vector3(0, 0, 0);
private _doScaling(diff: Vector3) {
this._setLocalAxes(this._mesh);
this._scale.x = 0;
this._scale.y = 0;
this._scale.z = 0;
let n: string = this._axisPicked.name;
if ((n == "X") || (n == "XZ") || (n == "YX")) {
this._scale.x = Vector3.Dot(diff, this._localX) / this._localX.length();
if (this._mesh.scaling.x < 0) this._scale.x = -this._scale.x;
//if(this.lhsRhs) this.scale.x=-this.scale.x;
}
if ((n == "Y") || (n == "ZY") || (n == "YX")) {
this._scale.y = Vector3.Dot(diff, this._localY) / this._localY.length();
if (this._mesh.scaling.y < 0) this._scale.y = -this._scale.y;
}
if ((n == "Z") || (n == "XZ") || (n == "ZY")) {
this._scale.z = Vector3.Dot(diff, this._localZ) / this._localZ.length();
if (this._mesh.scaling.z < 0) this._scale.z = -this._scale.z;
}
//as the mesh becomes large reduce the amount by which we scale.
let bbd = this._boundingDimesion;
this._scale.x = this._scale.x / bbd.x;
this._scale.y = this._scale.y / bbd.y;
this._scale.z = this._scale.z / bbd.z;
if (n == "ALL") {
//project movement along camera up vector
//if up then scale up else scale down
let s: number = Vector3.Dot(diff, this._mainCamera.upVector);
s = s / Math.max(bbd.x, bbd.y, bbd.z);
this._scale.copyFromFloats(s, s, s);
} else {
let inPlane: boolean = false;
if (n == "XZ") {
inPlane = true;
if (Math.abs(this._scale.x) > Math.abs(this._scale.z)) {
this._scale.z = this._scale.x;
} else this._scale.x = this._scale.z;
} else if (n == "ZY") {
inPlane = true;
if (Math.abs(this._scale.z) > Math.abs(this._scale.y)) {
this._scale.y = this._scale.z;
} else this._scale.z = this._scale.y;
} else if (n == "YX") {
inPlane = true;
if (Math.abs(this._scale.y) > Math.abs(this._scale.x)) {
this._scale.x = this._scale.y;
} else this._scale.y = this._scale.x;
}
if (inPlane) {
//check if the mouse/pointer was moved towards camera or away from camera
//if towards then scale up else scale down
this._ecRoot.position.subtractToRef(this._mainCamera.position, this._cameraTOec);
let s: number = Vector3.Dot(diff, this._cameraTOec);
this._scale.x = Math.abs(this._scale.x);
this._scale.y = Math.abs(this._scale.y);
this._scale.z = Math.abs(this._scale.z);
if (s > 0) {
if (this._mesh.scaling.x > 0) this._scale.x = -this._scale.x;
//if(this.lhsRhs) this.scale.y=Math.abs(this.scale.y);
if (this._mesh.scaling.y > 0) this._scale.y = -this._scale.y;
if (this._mesh.scaling.z > 0) this._scale.z = -this._scale.z;
} else {
//this.scale.x=Math.abs(this.scale.x);
//if(this.lhsRhs) this.scale.y=-Math.abs(this.scale.y);
//else this.scale.y=Math.abs(this.scale.y);
if (this._mesh.scaling.x < 0) this._scale.x = -this._scale.x;
if (this._mesh.scaling.y < 0) this._scale.y = -this._scale.y;
if (this._mesh.scaling.z < 0) this._scale.z = -this._scale.z;
}
}
}
this._scaleWithSnap(this._mesh, this._scale);
// bound the scale
if (this._scaleBoundsMin) {
this._mesh.scaling.x = Math.max(this._mesh.scaling.x, this._scaleBoundsMin.x);
this._mesh.scaling.y = Math.max(this._mesh.scaling.y, this._scaleBoundsMin.y);
this._mesh.scaling.z = Math.max(this._mesh.scaling.z, this._scaleBoundsMin.z);
}
if (this._scaleBoundsMax) {
this._mesh.scaling.x = Math.min(this._mesh.scaling.x, this._scaleBoundsMax.x);
this._mesh.scaling.y = Math.min(this._mesh.scaling.y, this._scaleBoundsMax.y);
this._mesh.scaling.z = Math.min(this._mesh.scaling.z, this._scaleBoundsMax.z);
}
}
private _scaleWithSnap(mesh: TransformNode, p: Vector3) {
if (this._snapS) {
let snapit: boolean = false;
this._snapSV.addInPlace(p);
if (Math.abs(this._snapSV.x) > this._scaleSnap) {
if (p.x > 0) p.x = this._scaleSnap; else p.x = -this._scaleSnap;
snapit = true;
}
if (Math.abs(this._snapSV.y) > this._scaleSnap) {
if (p.y > 0) p.y = this._scaleSnap; else p.y = -this._scaleSnap;
snapit = true;
}
if (Math.abs(this._snapSV.z) > this._scaleSnap) {
if (p.z > 0) p.z = this._scaleSnap; else p.z = -this._scaleSnap;
snapit = true;
}
if (!snapit) return;
if ((Math.abs(p.x) !== this._scaleSnap) && (p.x !== 0)) p.x = 0;
if ((Math.abs(p.y) !== this._scaleSnap) && (p.y !== 0)) p.y = 0;
if ((Math.abs(p.z) !== this._scaleSnap) && (p.z !== 0)) p.z = 0;
Vector3.FromFloatsToRef(0, 0, 0, this._snapSV);
snapit = false;
}
mesh.scaling.addInPlace(p);
}
private _localX: Vector3 = new Vector3(0, 0, 0);
private _localY: Vector3 = new Vector3(0, 0, 0);
private _localZ: Vector3 = new Vector3(0, 0, 0);
/*
* This would be called after rotation or scaling as the local axes direction or length might have changed
* We need to set the local axis as these are used in all three modes to figure out
* direction of mouse move wrt the axes
* TODO should use world pivotmatrix instead of worldmatrix - incase pivot axes were rotated?
*/
private _setLocalAxes(mesh: Node) {
let meshMatrix: Matrix = mesh.getWorldMatrix();
Vector3.FromArrayToRef(<DeepImmutableObject<Float32Array>>meshMatrix.m, 0, this._localX);
Vector3.FromArrayToRef(<DeepImmutableObject<Float32Array>>meshMatrix.m, 4, this._localY);
Vector3.FromArrayToRef(<DeepImmutableObject<Float32Array>>meshMatrix.m, 8, this._localZ);
}
/*
* boundingDimesion is used by scaling to adjust rate at which a mesh is scaled
* with respect to mouse movement.
*
*/
private _boundingDimesion: Vector3;
private _getBoundingDimension(mesh: TransformNode): Vector3 {
if (mesh instanceof AbstractMesh) {
{ } let bb: BoundingBox = mesh.getBoundingInfo().boundingBox;
let bd: Vector3 = bb.maximum.subtract(bb.minimum);
if (bd.x == 0) bd.x = 1;
if (bd.y == 0) bd.y = 1;
if (bd.z == 0) bd.z = 1;
return bd;
} else return new Vector3(1, 1, 1);
}
/*
*
* For the sake of speed the editcontrol calculates bounding info only once.
* This is in the constructor.
* Now The boundingbox dimension can change if the mesh is baked.
* If the editcontrol is attached to the mesh when the mesh was baked then
* the scaling speed will be incorrect.
* Thus client application should call refreshBoundingInfo if it bakes the mesh.
*
*/
public refreshBoundingInfo() {
this._boundingDimesion = this._getBoundingDimension(this._mesh);
}
private _eulerian: boolean = false;
private _snapRA: number = 0;
private _doRotation(mesh: TransformNode, axis: Mesh, newPos: Vector3, prevPos: Vector3) {
//for now no rotation if parents have non uniform scale
if (this._local && (this._mesh.parent != null) && this._isScaleUnEqual(mesh)) {
this._setLocalAxes(this._ecRoot);
} else {
this._setLocalAxes(mesh);
}
let angle: number = 0;
//rotation axis
let rAxis: Vector3;
if (axis == this._rX) rAxis = this._local ? this._localX : Axis.X;
else if (axis == this._rY) rAxis = this._local ? this._localY : Axis.Y;
else if (axis == this._rZ) rAxis = this._local ? this._localZ : Axis.Z;
this._ecRoot.position.subtractToRef(this._mainCamera.position, this._cameraTOec);
/**
* A)first find the angle and the direction (clockwise or anticlockwise) by which the user was trying to rotate
* from the user(camera) perspective
*/
if (this._rotate2) {
angle = this._getAngle2(prevPos, newPos, this._mainCamera.position, this._cameraTOec, rAxis);
//TODO check why we need to handle righ hand this way
if (this._scene.useRightHandedSystem) angle = -angle;
} else {
angle = this._getAngle(prevPos, newPos, mesh.getAbsolutePivotPoint(), this._cameraTOec);
}
if (this._lhsRhs) {
angle = -angle;
}
/**
* B)then rotate based on users(camera) postion and orientation in the local/world space
*
*/
if (this._snapR) {
this._snapRA += angle;
angle = 0;
if (Math.abs(this._snapRA) >= this._rotSnap) {
if (this._snapRA > 0) angle = this._rotSnap; else angle = -this._rotSnap;
this._snapRA = 0;
}
}
if (angle !== 0) {
this._cameraTOec.normalize();
if (axis == this._rAll) {
mesh.rotate(this._cameraTOec, -angle, Space.WORLD);
} else {
if (Vector3.Dot(rAxis, this._cameraTOec) >= 0) angle = -angle;
mesh.rotate(rAxis, angle, Space.WORLD);
}
if (this._eulerian) {
mesh.rotation = mesh.rotationQuaternion.toEulerAngles();
mesh.rotationQuaternion = null;
}
if (this._local) {
if (this._lhsRhs) {
angle = -angle;
}
if ((this._mesh.parent != null) && this._isScaleUnEqual(mesh)) {
if (axis == this._rAll) {
this._ecRoot.rotate(this._cameraTOec, -angle, Space.WORLD);
} else {
this._ecRoot.rotate(rAxis, angle, Space.WORLD);
}
}
}
}
}
private _getPosOnPickPlane(): Vector3 {
let engine: Engine = this._scene.getEngine();
let x = (engine.isPointerLock) ? this._canvas.width * 0.5 : this._scene.pointerX;
let y = (engine.isPointerLock) ? this._canvas.height * 0.5 : this._scene.pointerY;
let pickinfo: PickingInfo = this._scene.pick(x, y, (mesh) => {
return mesh == this._pickedPlane;
}, null, this._mainCamera);
if (pickinfo.hit) {
return pickinfo.pickedPoint;
} else {
return null;
}
}
private _hideBaxis() {
this._bXaxis.visibility = 0;
this._bYaxis.visibility = 0;
this._bZaxis.visibility = 0;
}
// private _setAxesVisiblity(v: number) {
// if (this._transEnabled) {
// this._tEndX.visibility = v;
// this._tEndY.visibility = v;
// this._tEndZ.visibility = v;
// this._tEndXZ.visibility = v;
// this._tEndZY.visibility = v;
// this._tEndYX.visibility = v;
// this._tEndAll.visibility = v;
// }
// if (this._rotEnabled) {
// this._rEndX.visibility = v;
// this._rEndY.visibility = v;
// this._rEndZ.visibility = v;
// this._rEndAll.visibility = v;
// }
// if (this._scaleEnabled) {
// this._sEndX.visibility = v;
// this._sEndY.visibility = v;
// this._sEndZ.visibility = v;
// this._sEndXZ.visibility = v;
// this._sEndZY.visibility = v;
// this._sEndYX.visibility = v;
// this._sEndAll.visibility = v;
// }
// }
public getRotationQuaternion(): Quaternion {
return this._ecRoot.rotationQuaternion
}
public getPosition(): Vector3 {
return this._ecRoot.position;
}
private _transEnabled: boolean = false;
public isTranslationEnabled(): boolean {
return this._transEnabled;
}
public enableTranslation() {
if (this._hidden) return;
if (this._tX == null) {
this._createTransAxes();
this._tCtl.parent = this._ecRoot;
}
this._clearPrevOverMesh();
if (!this._transEnabled) {
this._setVisibility(this._all_tEnd, this._visibility);
this._transEnabled = true;
this.disableRotation();
this.disableScaling();
}
}
public disableTranslation() {
if (this._transEnabled) {
this._setVisibility(this._all_tEnd, 0);
this._transEnabled = false;
}
}
private _rotEnabled: boolean = false;
public isRotationEnabled(): boolean {
return this._rotEnabled;
}
public returnEuler(euler: boolean) {
this._eulerian = euler;
}
public enableRotation() {
if (this._hidden) return;
if (this._rCtl == null) {
this._createRotAxes();
this._rCtl.parent = this._ecRoot;
}
this._clearPrevOverMesh();
if (!this._rotEnabled) {
this._setVisibility(this._all_rEnd, this._visibility);
this._rotEnabled = true;
this.disableTranslation();
this.disableScaling();
}
}
public disableRotation() {
if (this._rotEnabled) {
this._setVisibility(this._all_rEnd, 0);
this._rotEnabled = false;
}
}
private _scaleEnabled: boolean = false;
public isScalingEnabled(): boolean {
return this._scaleEnabled;
}
public enableScaling() {
if (this._hidden) return;
if (this._sX == null) {
this._createScaleAxes();
this._sCtl.parent = this._ecRoot;
}
this._clearPrevOverMesh();
if (!this._scaleEnabled) {
this._setVisibility(this._all_sEnd, this._visibility);
this._scaleEnabled = true;
this.disableTranslation();
this.disableRotation();
}
}
public disableScaling() {
if (this._scaleEnabled) {
this._setVisibility(this._all_sEnd, 0);
this._scaleEnabled