/* <copyright> This file contains proprietary software owned by Motorola Mobility, Inc.<br/> No rights, expressed or implied, whatsoever to this software are provided by Motorola Mobility, Inc. hereunder.<br/> (c) Copyright 2011 Motorola Mobility, Inc. All Rights Reserved. </copyright> */ /* Base class for the 3D rotation tools Subclass RotateObject3DTool will rotate the object that was clicked. Subclass RotateStage3DTool will rotate the stage. */ var Montage = require("montage/core/core").Montage, ModifierToolBase = require("js/tools/modifier-tool-base").ModifierToolBase, snapManager = require("js/helper-classes/3D/snap-manager").SnapManager, viewUtils = require("js/helper-classes/3D/view-utils").ViewUtils, vecUtils = require("js/helper-classes/3D/vec-utils").VecUtils, drawUtils = require("js/helper-classes/3D/draw-utils").DrawUtils, ElementsMediator = require("js/mediators/element-mediator").ElementMediator; exports.Rotate3DToolBase = Montage.create(ModifierToolBase, { _canSnap: { value: false }, _inLocalMode: { value: true, enumerable: true }, rotateStage: { value: false }, initializeSnapping : { value : function(event) { // console.log( "initializeSnapping" ); var selectedElements = this.application.ninja.selectedElements; if(this.rotateStage) { selectedElements = [this.application.ninja.currentDocument.model.documentRoot]; } this._mouseDownHitRec = null; this._mouseUpHitRec = null; snapManager.clearAvoidList(); snapManager.clearDragPlane(); // the translate tool does snap align to the bounds of the object only. // turn off snap align to the cursor. This needs to be re-enabled in the mouse up method snapManager.enableSnapAlign( false ); // snap to element and snap to grid are conditionally enabled based // on the snap results of the mouse down. enable everything for the first snap this._snapToElements = snapManager.elementSnapEnabledAppLevel(); this._snapToGrid = snapManager.gridSnapEnabledAppLevel(); this._dragPlane = null; this._clickedOnStage = false; var do3DSnap = true; if(this._handleMode === null) { snapManager.enableElementSnap ( true ); snapManager.enableGridSnap ( true ); } // else // { // this._delta = null; // special case for z-translation // if(this._handleMode === 0) // { // this._dragPlane = viewUtils.getNormalToUnprojectedElementPlane(this._target); // snapManager.setupDragPlaneFromPlane(this._dragPlane); // do3DSnap = false; // snapManager.enableElementSnap ( false ); // snapManager.enableGridSnap ( false ); // } // } if (selectedElements.length) { var point = webkitConvertPointFromPageToNode(this.application.ninja.stage.canvas, new WebKitPoint(event.pageX, event.pageY)); // do the snap before setting up the avoid list to allow // a snap on the mouse down var hitRec = snapManager.snap(point.x, point.y, do3DSnap); // if(this._handleMode === 2) // { // // translate z doesn't snap to element so hitRec's element will always be different // // from what the browser says we clicked on. So, skip this check. // } // else // { // // Check that hitRec's element matches element that browser says we clicked on // // TODO - This is still not working when using a handle that is on top of an // // element that is not currently selected // var elt = this.application.ninja.stage.GetSelectableElement(event); // if(elt && (elt !== hitRec.getElement())) // { // hitRec = snapManager.findHitRecordForElement(elt); // } // if(elt === this.application.ninja.currentDocument.model.domContainer) // { // this._clickedOnStage = true; // } // } // we don't want to snap to selected objects during the drag // var len = this._targets.length; // for(var i=0; i<len; i++) // { // snapManager.addToAvoidList( this._targets[i].elt ); // } if (hitRec) { // disable snap attributes if (hitRec.getType() == hitRec.SNAP_TYPE_ELEMENT) { this._snapToElements = false; this._snapToGrid = false; } else if (hitRec.getType() == hitRec.SNAP_TYPE_ELEMENT_CENTER) { snapManager.enableSnapAlign( snapManager.snapAlignEnabledAppLevel() ); } if(this._handleMode === 0) this.clickedObject = this._target; // parameterize the snap point on the target this._snapParam = this.parameterizeSnap( hitRec ); if(!this._dragPlane) { if (selectedElements.length === 1) { this._dragPlane = viewUtils.getUnprojectedElementPlane(this._clickedObject); snapManager.setupDragPlaneFromPlane(this._dragPlane); } else { this._dragPlane = snapManager.setupDragPlanes( hitRec, true ); } } // no quadrant snapping for the rotate tool this._shouldUseQuadPt = false; var wpHitRec = hitRec.convertToWorkingPlane( this._dragPlane ); this._mouseDownHitRec = wpHitRec; this._mouseUpHitRec = null; var pt = hitRec.getScreenPoint(); this.downPoint.x = pt[0]; this.downPoint.y = pt[1]; this.downPoint.z = pt[2]; // TODO - need to figure out snapManager dependency by drawUtils. // For now, bypassing by calling snapManager.drawLastHit(); // drawUtils.refreshDisplay(); // snapManager.drawLastHit(); } } else { this.target = null; } } }, drawWithoutSnapping: { value: function(event) { if(this._handleMode !== null) { this._matL = this._handles[this._handleMode]._matL.slice(0); this._planeEq = this._handles[this._handleMode]._planeEq.slice(0); this._dirVecL = this._handles[this._handleMode]._dirVecL.slice(0); this._startPoint = MathUtils.getLocalPoint(this.downPoint.x, this.downPoint.y, this._planeEq, this._matL); if(!this._startPoint) { this._startPoint = [this.downPoint.x, this.downPoint.y]; } } } }, HandleMouseMove: { value: function(event) { if(this._escape) { this._escape = false; this.isDrawing = true; } if(this.isDrawing) { this._hasDraw = true; // Flag for position of element this.doDraw(event); } else { this._showFeedbackOnMouseMove(event); if(this._canSnap) { this.doSnap(event); } } this.DrawHandles(this._delta); if(this._canSnap) { snapManager.drawLastHit(); } } }, modifyElements: { value: function(data, event) { var mat, angle, pt0 = data.pt0, pt1 = data.pt1; var selectedElements = this.application.ninja.selectedElements; if(this.rotateStage) { selectedElements = [this.application.ninja.currentDocument.model.documentRoot]; } if(this._handleMode !== null) { if(this._activateOriginHandle) { // move the transform origin handle directly to the snap point (pt1) this._origin[0] = pt1.x; this._origin[1] = pt1.y; this._origin[2] = pt1.z; var sw2gMat = viewUtils.getStageWorldToGlobalMatrix(); var g2swMat = glmat4.inverse( sw2gMat, [] ); var swOrigin = MathUtils.transformAndDivideHomogeneousPoint( this._origin, g2swMat ); //console.log( "modifyElements, _origin: " + this._origin + ", in stageWorld: " + swOrigin ); var len = selectedElements.length; if(len === 1) { var elt = selectedElements[0]; var g2lMat = elt.elementModel.getProperty("g2l"); var localOrigin = MathUtils.transformAndDivideHomogeneousPoint( this._origin, g2lMat ); viewUtils.pushViewportObj( elt ); var viewOrigin = viewUtils.screenToView( localOrigin[0], localOrigin[1], localOrigin[2] ); viewUtils.popViewportObj(); this._startOriginArray[0] = viewOrigin; //console.log( "Rotate3DToolBase.modifyElements, _startOriginArray[0]: " + this._startOriginArray[0] ); } this.downPoint.x = pt1.x; this.downPoint.y = pt1.y; this.downPoint.z = pt1.z; this.DrawHandles(); return; } angle = this._getAngleToRotate(pt1.x, pt1.y); if(event.shiftKey) { var f = Math.floor(angle/(Math.PI/4)); angle = f*Math.PI/4; } this._delta = angle; switch(this._handleMode) { case 0: // Rotate X; mat = Matrix.RotationX(angle); break; case 1: // Rotate Y mat = Matrix.RotationY(angle); break; case 2: // Rotate Z mat = Matrix.RotationZ(angle); break; default: break; } } else { if (event.ctrlKey || event.metaKey) { var zAngle = this._mouseSpeed * (pt1.y - this.downPoint.y) * Math.PI / 180.0; if (zAngle === 0) { zAngle = 0.01 * Math.PI / 180.0; } if(event.shiftKey) { var f = Math.floor(zAngle/(Math.PI/4)); zAngle = f*Math.PI/4; } mat = Matrix.RotationZ(zAngle); } else { var yAngle = this._mouseSpeed * (pt1.x - this.downPoint.x) * Math.PI / 180.0; var xAngle = -this._mouseSpeed * (pt1.y - this.downPoint.y) * Math.PI / 180.0; if(event.shiftKey) { var f = Math.floor(yAngle/(Math.PI/4)); yAngle = f*Math.PI/4; f = Math.floor(xAngle/(Math.PI/4)); xAngle = f*Math.PI/4; } // check the orientation of the X axis //if (drawUtils.drawYZ) xAngle = -xAngle; var yMat = Matrix.RotationY( yAngle ), xMat = Matrix.RotationX( xAngle ); mat = glmat4.multiply(yMat, xMat, []); } } if(this._inLocalMode && (this.application.ninja.selectedElements.length === 1 || this.rotateStage) ) { // console.log( "modifyElements: rotateLocally " ); this._rotateLocally(mat); } else { this._rotateGlobally(mat); } // this.UpdateSelection(); NJevent("elementChanging", {type : "Changing", redraw: false}); } }, _rotateLocally: { value: function (rotMat) { var selectedElements = this.application.ninja.selectedElements; if(this.rotateStage) { selectedElements = [this.application.ninja.currentDocument.model.documentRoot]; } var len = selectedElements.length; for(var i = 0; i < len; i++) { var elt = selectedElements[i]; var curMat = elt.elementModel.getProperty("mat"); // pre-translate by the transformation center var tMat = Matrix.I(4); // _startOriginArray is the location of the center of rotation // in view space of the element. var transformCtr = this._startOriginArray[i]; tMat[12] = transformCtr[0]; tMat[13] = transformCtr[1]; tMat[14] = transformCtr[2]; var mat = glmat4.multiply(curMat, tMat, []); // translate back tMat[12] = -transformCtr[0]; tMat[13] = -transformCtr[1]; tMat[14] = -transformCtr[2]; glmat4.multiply(mat, rotMat, mat); glmat4.multiply(mat, tMat, mat); // while moving, set inline style to improve performance viewUtils.setMatrixForElement( elt, mat, true ); } } }, _rotateGlobally: { value: function (rotMat) { var len = this.application.ninja.selectedElements.length; for(var i = 0; i < len; i++) { var elt = this.application.ninja.selectedElements[i].elementModel.getProperty("elt"); var curMat = this.application.ninja.selectedElements[i].elementModel.getProperty("mat"); // pre-translate by the transformation center var tMat = Matrix.I(4); var transformCtr = this._startOriginArray[i].slice(0); transformCtr = MathUtils.transformPoint(transformCtr, curMat); // console.log( "modifyElements: rotateGlobally, ctr: " + transformCtr ); tMat[12] = transformCtr[0]; tMat[13] = transformCtr[1]; tMat[14] = transformCtr[2]; var mat = glmat4.multiply(tMat, rotMat, []); // translate back tMat[12] = -transformCtr[0]; tMat[13] = -transformCtr[1]; tMat[14] = -transformCtr[2]; glmat4.multiply(mat, tMat, mat); glmat4.multiply(mat, curMat, mat); viewUtils.setMatrixForElement(this.application.ninja.selectedElements[i], mat, true ); } } }, _getAngleToRotate: { value: function (x, y) { var angle; var pt = MathUtils.getLocalPoint(x, y, this._planeEq, this._matL); if(!pt) { //TODO - should this be _startPoint.x/y instead of downPoint.x/y? var st = [this.downPoint.x, this.downPoint.y]; pt = [x, y]; var sub = vecUtils.vecSubtract(2, pt, st); var dot = vecUtils.vecDot(2, sub, this._dirVecL); angle = vecUtils.vecDist(2, pt, st) * 0.1; if (dot < 0) { angle = -angle; } } else { angle = MathUtils.getAngleBetweenPoints(this._startPoint, pt); } return angle; } }, captureSelectionChange: { value: function(event){ this.eventManager.addEventListener("selectionDrawn", this, true); } }, captureSelectionDrawn: { value: function(event){ this._origin = null; this._startOriginArray = null; var len = this.application.ninja.selectedElements.length; if(len) { if(len === 1) { this.target = this.application.ninja.selectedElements[0]; drawUtils.addElement(this.target); viewUtils.pushViewportObj( this.target ); var eltCtr = viewUtils.getCenterOfProjection(); eltCtr[2] = 0; viewUtils.popViewportObj(); var ctrOffset = this.target.elementModel.props3D.m_transformCtr; if(ctrOffset) { eltCtr[2] = 0; eltCtr = vecUtils.vecAdd(3, eltCtr, ctrOffset); } this._origin = viewUtils.localToGlobal(eltCtr, this.target); //console.log( "Rotate3DToolBase.captureSelectionDrawn _origin: " + this._origin ); this._updateTargets(); this._setTransformOrigin(false); } else { this.target = this.application.ninja.currentDocument.model.documentRoot; //this._origin = drawUtils._selectionCtr.slice(0); //this._origin[0] += this.application.ninja.stage.userContentLeft; //this._origin[1] += this.application.ninja.stage.userContentTop; this._updateTargets(); this._origin = this.calculateMultiSelOrigin(); this._setTransformOrigin(true); } } else { this.target = null; } this.DrawHandles(); if(event) { this.eventManager.removeEventListener("selectionDrawn", this, true); } } }, /* _updateHandlesOrigin: { value: function () { } }, */ _updateTargets: { value: function(addToUndo) { var mod3dObject = [], self = this; this.application.ninja.selectedElements.forEach(function(element) { var curMat = viewUtils.getMatrixFromElement(element); var curMatInv = glmat4.inverse(curMat, []); viewUtils.pushViewportObj( element ); var eltCtr = viewUtils.getCenterOfProjection(); viewUtils.popViewportObj(); // cache the local to global and global to local matrices var l2gMat = viewUtils.getLocalToGlobalMatrix( element ); var g2lMat = glmat4.inverse( l2gMat, [] ); eltCtr = MathUtils.transformAndDivideHomogeneousPoint( eltCtr, l2gMat ); element.elementModel.setProperty("ctr", eltCtr); element.elementModel.setProperty("mat", curMat); element.elementModel.setProperty("matInv", curMatInv); element.elementModel.setProperty("l2g", l2gMat); element.elementModel.setProperty("g2l", g2lMat); if(addToUndo) { var previousMat = element.elementModel.getProperty("mat").slice(0); var previousStyleStr = {dist:element.elementModel.getProperty("dist"), mat:MathUtils.scientificToDecimal(previousMat, 5)}; var newStyleStr = {dist:viewUtils.getPerspectiveDistFromElement(element), mat:MathUtils.scientificToDecimal(viewUtils.getMatrixFromElement(element), 5)}; mod3dObject.push({element:element, properties:newStyleStr, previousProperties: previousStyleStr}); } }); if(addToUndo) { ElementsMediator.set3DProperties(mod3dObject, "Change", "rotateTool"); } this.application.ninja.selectedElements.forEach(function(element) { element.elementModel.setProperty("mat", viewUtils.getMatrixFromElement(element)); element.elementModel.setProperty("matInv", glmat4.inverse(element.elementModel.getProperty("mat"), [])); element.elementModel.setProperty("dist", viewUtils.getPerspectiveDistFromElement(element)); }); } }, calculateMultiSelOrigin: { value: function() { var minPt, maxPt, i,j; this._startOriginArray = []; var len = this.application.ninja.selectedElements.length; for (i = 0; i < len; i++) { // get the next element and localToGlobal matrix var elt = this.application.ninja.selectedElements[i]; var l2g = elt.elementModel.getProperty("l2g"); // get the element bounds in 'plane' space var bounds = viewUtils.getElementViewBounds3D( elt ); for (j=0; j<4; j++) { var localPt = bounds[j]; //var pt = MathUtils.transformAndDivideHomogeneousPoint( localPt, l2g ); var pt = viewUtils.localToStageWorld( localPt, elt ); if (!minPt) { minPt = pt.slice(); maxPt = pt.slice(); } else { minPt[0] = Math.min(minPt[0],pt[0]); minPt[1] = Math.min(minPt[1],pt[1]); minPt[2] = Math.min(minPt[2],pt[2]); maxPt[0] = Math.max(maxPt[0],pt[0]); maxPt[1] = Math.max(maxPt[1],pt[1]); maxPt[2] = Math.max(maxPt[2],pt[2]); } } } var stageWorldCtr = [ 0.5*(minPt[0] + maxPt[0]), 0.5*(minPt[1] + maxPt[1]), 0.5*(minPt[2] + maxPt[2]) ]; var globalCtr = MathUtils.transformAndDivideHomogeneousPoint( stageWorldCtr, viewUtils.getStageWorldToGlobalMatrix() ); // console.log( "resetting _origin to: " + this._origin ); return globalCtr; } }, _setTransformOrigin: { value: function(shouldUpdateCenter) { if(!this._origin) { return; } var elt, element, eltCtr, ctrOffset, matInv; if(this.rotateStage || (this.application.ninja.selectedElements.length === 1)) { if(this.rotateStage) { element = this.application.ninja.currentDocument.model.documentRoot; } else { element = this.application.ninja.selectedElements[0]; } if(shouldUpdateCenter) { // eltCtr = element.elementModel.getProperty("ctr"); // ctrOffset = vecUtils.vecSubtract(3, this._origin, eltCtr); // matInv = element.elementModel.getProperty("matInv"); // ctrOffset = MathUtils.transformVector(ctrOffset, matInv); element.elementModel.props3D.m_transformCtr = this._startOriginArray[0].slice(); } else { this._startOriginArray = []; ctrOffset = element.elementModel.props3D.m_transformCtr; if(!ctrOffset) { ctrOffset = [0,0,0]; } this._startOriginArray[0] = ctrOffset; } } else { /* this._startOriginArray = []; var len = this.application.ninja.selectedElements.length; for (var i = 0; i < len; i++) { eltCtr = this.application.ninja.selectedElements[i].elementModel.getProperty("ctr"); ctrOffset = vecUtils.vecSubtract(3, this._origin, eltCtr); matInv = this.application.ninja.selectedElements[i].elementModel.getProperty("matInv"); ctrOffset = MathUtils.transformVector(ctrOffset, matInv); this._startOriginArray[i] = ctrOffset; } */ // Update transform ctr for all elements if transform origin was modified if (!this._origin) this._origin = this.calculateMultiSelOrigin(); var globalCtr = this._origin; var len = this.application.ninja.selectedElements.length; for (var i=0; i<len; i++) { // get the next element and localToGlobal matrix elt = this.application.ninja.selectedElements[i]; var l2g = viewUtils.getLocalToGlobalMatrix( elt ); var g2l = glmat4.inverse( l2g, [] ); elt.elementModel.setProperty("g2l", g2l); elt.elementModel.setProperty("l2g", l2g); var localCtr = MathUtils.transformAndDivideHomogeneousPoint(globalCtr, g2l); viewUtils.pushViewportObj( elt ); localCtr = viewUtils.screenToView( localCtr[0], localCtr[1], localCtr[2] ); viewUtils.popViewportObj(); this._startOriginArray[i] = localCtr; } } } }, HandleDoubleClick: { value: function () { if(!this._target) { return; } if(this._activateOriginHandle) { var len = this.application.ninja.selectedElements.length; if( (len === 1) || (this._toolID === "rotateStage3DTool") ) { this._target.elementModel.props3D.m_transformCtr = null; } this._handleMode = null; this._activateOriginHandle = false; this.application.ninja.stage.drawingCanvas.style.cursor = this._cursor; this.captureSelectionDrawn(null); } } }, Reset: { value: function() { var mat, iMat, dist, mod3dObject = [], self = this; this.application.ninja.selectedElements.forEach(function(element) { // Reset to the identity matrix iMat = Matrix.I(4); mat = ElementsMediator.getMatrix(element); // iMat[12] = mat[12]; // iMat[13] = mat[13]; // iMat[14] = mat[14]; dist = ElementsMediator.getPerspectiveDist(element); var previousStyleStr = {dist:dist, mat:mat}; var newStyleStr = {dist:dist, mat:iMat}; mod3dObject.push({element:element, properties:newStyleStr, previousProperties: previousStyleStr}); }); ElementsMediator.set3DProperties(mod3dObject, "Change", "rotateTool"); this.isDrawing = false; this.endDraw(event); // Need to force stage to draw immediately so the new selection center is calculated this.application.ninja.stage.draw(); // And captureSelectionDrawn to draw the transform handles this.captureSelectionDrawn(null); } }, /** * SHIFT/ALT/SPACE Key Handlers */ HandleShiftKeyDown: { value: function (event) { } }, HandleShiftKeyUp: { value: function () { } }, HandleSpaceKeyDown: { value: function () { } }, HandleSpaceUp: { value: function () { } }, HandleAltKeyDown: { value: function(event) { this._inLocalMode = !this._inLocalMode; this.DrawHandles(); } }, HandleAltKeyUp: { value: function(event) { this._inLocalMode = !this._inLocalMode; this.DrawHandles(); } }, handleScroll: { value: function(event) { this.captureSelectionDrawn(null); } }, /** * This function is for specifying custom feedback routine * upon mouse over. * For example, the drawing tools will add a glow when mousing * over existing canvas elements to signal to the user that * the drawing operation will act on the targeted canvas. */ _showFeedbackOnMouseMove : { value: function (event) { this._canSnap = false; if(this._target && this._handles) { var len = this._handles.length; var i = 0, toolHandle, c, point = webkitConvertPointFromPageToNode(this.application.ninja.stage.canvas, new WebKitPoint(event.pageX, event.pageY)); for (i=0; i<len; i++) { toolHandle = this._handles[i]; c = toolHandle.collidesWithPoint(point.x, point.y); if(c === 1) { this.application.ninja.stage.drawingCanvas.style.cursor = "move"; this._handleMode = i; this._activateOriginHandle = true; this._canSnap = true; return; } else if(c === 2) { this.application.ninja.stage.drawingCanvas.style.cursor = toolHandle._cursor; this._handleMode = i; this._activateOriginHandle = false; return; } } } this._handleMode = null; this._activateOriginHandle = false; // this.application.ninja.stage.drawingCanvas.style.cursor = this._cursor; this.application.ninja.stage.drawingCanvas.style.cursor = "auto"; } }, getMousePoints: { value: function() { var pt0 = { x:this.downPoint.x, y:this.downPoint.y, z:this.downPoint.z }; var pt1 = { x:this.upPoint.x, y:this.upPoint.y, z:this.upPoint.z }; return {pt0:pt0, pt1:pt1}; } }, DrawHandles: { value: function (angle) { this.application.ninja.stage.clearDrawingCanvas(); var item = this._target; if(!item) { return; } // Draw tool handles // set the element to be the viewport object - temporarily var lMode = this._inLocalMode; if( (this._toolID !== "rotateStage3DTool") && (this.application.ninja.selectedElements.length === 1) ) { viewUtils.pushViewportObj( item ); } else { lMode = false; viewUtils.pushViewportObj( this.application.ninja.currentDocument.model.documentRoot ); } var base = this._origin; //console.log( "Rotate3DToolBase.DrawHandles, base: " + base ); if( (this._handleMode !== null) && !this._activateOriginHandle ) { switch(this._handleMode) { case 0: this._handles[1]._strokeStyle = 'rgba(0, 255, 0, 0.2)'; this._handles[2]._strokeStyle = 'rgba(0, 0, 255, 0.2)'; break; case 1: this._handles[0]._strokeStyle = 'rgba(255, 0, 0, 0.2)'; this._handles[2]._strokeStyle = 'rgba(0, 0, 255, 0.2)'; break; case 2: this._handles[0]._strokeStyle = 'rgba(255, 0, 0, 0.2)'; this._handles[1]._strokeStyle = 'rgba(0, 255, 0, 0.2)'; break; } } this._handles[0].draw(base, item, lMode); this._handles[1].draw(base, item, lMode); this._handles[2].draw(base, item, lMode); if(angle && (this._handleMode !== null)) { this._handles[this._handleMode].drawShadedAngle(angle, this._startPoint); } this._handles[0]._strokeStyle = 'rgba(255, 0, 0, 1)'; this._handles[1]._strokeStyle = 'rgba(0, 255, 0, 1)'; this._handles[2]._strokeStyle = 'rgba(0, 0, 255, 1)'; viewUtils.popViewportObj(); } } });