/* Copyright (c) 2012, Motorola Mobility LLC. All Rights Reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Motorola Mobility LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ var Montage = require("montage/core/core").Montage; var Component = require("montage/ui/component").Component; var snapManager = require("js/helper-classes/3D/snap-manager").SnapManager; var viewUtils = require("js/helper-classes/3D/view-utils").ViewUtils; var vecUtils = require("js/helper-classes/3D/vec-utils").VecUtils; var drawUtils = require("js/helper-classes/3D/draw-utils").DrawUtils; exports.DrawingToolBase = Montage.create(Component, { dragPlane: { get: function () { return this.application.ninja.toolsData.selectedToolInstance._dragPlane; }, set: function (value) { this.application.ninja.toolsData.selectedToolInstance._dragPlane = value; } }, /** * Used on the initial MouseDown for Drawing Tools * * Returns: An array containing: * 0 - HitRec point * 1 - X value converted to screen point * 2 - Y value converted to screen point */ getInitialSnapPoint: { value: function(x, y, shapeCanvas) { // update the snap settings snapManager.enableSnapAlign( snapManager.snapAlignEnabledAppLevel() ); snapManager.enableElementSnap( snapManager.elementSnapEnabledAppLevel() ); snapManager.enableGridSnap( snapManager.gridSnapEnabledAppLevel() ); // do the snap this.dragPlane = null; var hitRec = snapManager.snap(x, y, true); if (hitRec) { if (shapeCanvas) { this.dragPlane = viewUtils.getUnprojectedElementPlane( shapeCanvas ); snapManager.setupDragPlaneFromPlane( this.dragPlane ); } else { this.dragPlane = snapManager.setupDragPlanes( hitRec, true ); } // console.log( "drag plane: " + this.dragPlane ); var wpHitRec = hitRec.convertToWorkingPlane( this.dragPlane ); var pt = hitRec.getScreenPoint(); return( [wpHitRec, pt[0], pt[1]] ); } } }, /** * Used on the Mouse Move to calculate new snap point. */ getUpdatedSnapPoint: { value: function(x,y, snap3d, downHitRec) { // update the snap settings snapManager.enableSnapAlign( snapManager.snapAlignEnabledAppLevel() ); snapManager.enableElementSnap( snapManager.elementSnapEnabledAppLevel() ); snapManager.enableGridSnap( snapManager.gridSnapEnabledAppLevel() ); var hitRec = snapManager.snap(x, y, snap3d ); if (hitRec) { // if ((hitRec.getType() !== hitRec.SNAP_TYPE_STAGE) && !hitRec.isSomeGridTypeSnap()) { // hitRec = hitRec.convertToWorkingPlane( snapManager.getDragPlane() ); // } // // if(downHitRec !== null) { // // if we are working off-plane, do a snap to the projected locations of the geometry // var thePlane = workingPlane; // if (snapManager.hasDragPlane()) // { // thePlane = snapManager.getDragPlane(); // } // // // Return the up HitRec // return hitRec; // } else { // return null; // } if(downHitRec) { hitRec = hitRec.convertToWorkingPlane(this.dragPlane || downHitRec.getPlane()); } else if ((hitRec.getType() !== hitRec.SNAP_TYPE_STAGE) && !hitRec.isSomeGridTypeSnap()) { hitRec = hitRec.convertToWorkingPlane( snapManager.getDragPlane() ); } } return hitRec; } }, getUpdatedSnapPointNoAppLevelEnabling: { value: function(x,y, snap3d, downHitRec) { // do the first snap var hitRec = snapManager.snap(x, y, snap3d ); if (hitRec) { if ((hitRec.getType() !== hitRec.SNAP_TYPE_STAGE) && !hitRec.isSomeGridTypeSnap()) { hitRec = hitRec.convertToWorkingPlane( snapManager.getDragPlane() ); } if(downHitRec !== null) { // if we are working off-plane, do a snap to the projected locations of the geometry var thePlane = workingPlane; if (snapManager.hasDragPlane()) { thePlane = snapManager.getDragPlane(); } // Return the up HitRec return hitRec; } else { return null; } } } }, setDownHitRec: { value: function (x, y, do3DSnap) { var hitRec = snapManager.snap(x, y, do3DSnap ); if (hitRec) { if ((hitRec.getType() != hitRec.SNAP_TYPE_STAGE) && !hitRec.isSomeGridTypeSnap()) { //hitRec = hitRec.convertToWorkingPlane( workingPlane ); snapManager.setupDragPlanes(hitRec); hitRec = hitRec.convertToWorkingPlane( snapManager.getDragPlane() ); } return hitRec; } } }, drawSnapLastHit: { value: function() { snapManager.drawLastHit(); } }, getHitRecPos: { value: function (hitRec) { if (!hitRec) return null; // get the hit rec. points in plane space var psPos = hitRec.getLocalPoint(); var stageOffset = viewUtils.getElementOffset(this.application.ninja.currentDocument.model.documentRoot); viewUtils.setViewportObj(this.application.ninja.currentDocument.model.documentRoot); // get the matrix taking the local hit point in plane space // to world space of whatever element it is in. var planeMat = hitRec.getPlaneMatrix(); // get the center of the circle in stage world space var swPos = viewUtils.postViewToStageWorld(MathUtils.transformPoint(psPos, hitRec.getPlaneMatrix()), hitRec.getElt()); //var swPos = hitRec.calculateStageWorldPoint(); todo figure out why we cannot just use this function instead of the above // the stage world space point is now relative to the center of the 3D space. To // calculate the left and top offsets, this must be offset by the stage dimensions swPos[0] += snapManager.getStageWidth() / 2.0; swPos[1] += snapManager.getStageHeight() / 2.0; return swPos; } }, getCompletePoints: { value: function(hitRec0, hitRec1) { if (hitRec0 && hitRec1) { // get the 2 snap points in plane space var p0 = hitRec0.getLocalPoint(), p1 = hitRec1.getLocalPoint(); var stageOffset = viewUtils.getElementOffset(this.application.ninja.currentDocument.model.documentRoot); viewUtils.setViewportObj(this.application.ninja.currentDocument.model.documentRoot); // get the matrix taking the local hit point in plane space // to world space of whatever element it is in. var planeMat = hitRec0.getPlaneMatrix(); // get the center of the circle in stage world space var s0 = viewUtils.postViewToStageWorld( MathUtils.transformPoint(p0,hitRec0.getPlaneMatrix()), hitRec0.getElt() ), s1 = viewUtils.postViewToStageWorld( MathUtils.transformPoint(p1,hitRec1.getPlaneMatrix()), hitRec1.getElt() ); // apply the projected snap points var s0Proj = false, s1Proj = false; // find a "reasonable" plane var thePlane = workingPlane.slice(0); if (snapManager.hasDragPlane()) { thePlane = snapManager.getDragPlane(); } var d0 = vecUtils.vecDot( 3, thePlane, s0 ) + thePlane[3], d1 = vecUtils.vecDot( 3, thePlane, s1 ) + thePlane[3]; var sign0 = MathUtils.fpSign( d0 ), sign1 = MathUtils.fpSign( d1 ); if ((sign0 !== 0) || (sign1 !== 0)) { // we need to pick a different plane if ( MathUtils.fpCmp(d0,d1) === 0 ) { thePlane[3] = -vecUtils.vecDot(3, thePlane, s0); } else { var vec = vecUtils.vecSubtract(3, s1, s0 ); var yAxis = [0,1,0]; var tmp = vecUtils.vecCross( 3, vec, yAxis ); var mag = vecUtils.vecMag(3, tmp); if (MathUtils.fpSign(mag) === 0) { thePlane = [0,0,1]; thePlane[3] = -vecUtils.vecDot(3, thePlane, s0); } else { var xAxis = vecUtils.vecCross( 3, yAxis, tmp ); thePlane = vecUtils.vecCross( 3, xAxis, yAxis ); vecUtils.vecNormalize(3, thePlane, 1.0 ); thePlane[3] = -vecUtils.vecDot(3, thePlane, s0); } } // recompute the plane matrix planeMat = drawUtils.getPlaneToWorldMatrix(thePlane, MathUtils.getPointOnPlane(thePlane)); } // unproject the bounds //var planeMatInv = planeMat.inverse(); var planeMatInv = glmat4.inverse( planeMat, [] ); //var midPt = this.unprojectPoints( s0, s1, planeMat, planeMatInv, s1Proj ); // var midPt = this.unprojectPoints( s0, s1, planeMat, planeMatInv, true ); // get the 4 points of the bounding box in 2D space p0 = MathUtils.transformPoint( s0, planeMatInv ); p1 = MathUtils.transformPoint( s1, planeMatInv ); // determine the midpoint of the oval //midPt = s0.add(s1); //midPt = midPt.multiply(0.5); //midPt = MathUtils.makeDimension3(midPt); var midPt = vec3.add(s0, s1, []); midPt = vecUtils.vecScale( 3, midPt, 0.5 ); // the mid point is now relative to the center of the 3D space. To // calculate the left and top offsets, this must be offset by the stage dimensions midPt[0] += snapManager.getStageWidth() / 2.0; midPt[1] += snapManager.getStageHeight() / 2.0; // calculate the width and height. var left = p0[0]; var top = p0[1]; var right = p1[0]; var bottom = p1[1]; var w = Math.abs(right - left), h = Math.abs(bottom - top); // return ({"width":w, "height":h, "planeMat":planeMat, "midPt":midPt}); var s0Offset = s0.slice(0); var s1Offset = s1.slice(0); s0Offset[0] += snapManager.getStageWidth() / 2.0; s0Offset[1] += snapManager.getStageHeight() / 2.0; s1Offset[0] += snapManager.getStageWidth() / 2.0; s1Offset[1] += snapManager.getStageHeight() / 2.0; return ({ "width": w, "height": h, "planeMat": planeMat, "midPt": midPt, "mouseDownPos": s0Offset, "mouseUpPos": s1Offset }); } else { return null; } } }, cleanupSnap: { value: function() { // set the drag plane to the working plane snapManager.clear2DCache(); snapManager.setupDragPlaneFromPlane( workingPlane ); // update the snap settings snapManager.enableSnapAlign( snapManager.snapAlignEnabledAppLevel() ); snapManager.enableElementSnap( snapManager.elementSnapEnabledAppLevel() ); snapManager.enableGridSnap( snapManager.gridSnapEnabledAppLevel() ); } }, draw2DRectangle: { value: function(x0, y0, x1, y1) { var drawingContext = this.application.ninja.stage.drawingContext, drawingPrefs = this.application.ninja.stage.drawingContextPreferences; this.application.ninja.stage.clearDrawingCanvas(); //TODO Save and restore state drawingContext.strokeStyle = drawingPrefs.color; drawingContext.lineWidth = drawingPrefs.thickness; drawingContext.strokeRect(x0 - 0.5 ,y0 - 0.5 ,x1 ,y1); } }, /** * Feedback drawing functions */ drawRectangle: { value: function(hitRec0, hitRec1) { var p0 = hitRec0.getLocalPoint(), p1 = hitRec1.getLocalPoint(); var stageMat = viewUtils.getMatrixFromElement(this.application.ninja.currentDocument.model.documentRoot); var elt = hitRec0.getElt(); if (!elt) { elt = hitRec1.getElt(); } if (!elt) { elt = this.application.ninja.currentDocument.model.documentRoot; } if (elt) { viewUtils.pushViewportObj(elt); var offset = viewUtils.getElementOffset(elt); offset[2] = 0; // check the plane to draw the rectangle on var s0 = viewUtils.postViewToStageWorld( MathUtils.transformPoint(p0,hitRec0.getPlaneMatrix()), hitRec0.getElt() ), s1 = viewUtils.postViewToStageWorld( MathUtils.transformPoint(p1,hitRec1.getPlaneMatrix()), hitRec1.getElt() ); // find a "reasonable" plane var planeMat = hitRec0.getPlaneMatrix(); var planeMatInv; var thePlane = workingPlane.slice(0); if (snapManager.hasDragPlane()) { thePlane = snapManager.getDragPlane(); } var d0 = vecUtils.vecDot( 3, thePlane, s0 ) + thePlane[3], d1 = vecUtils.vecDot( 3, thePlane, s1 ) + thePlane[3]; var sign0 = MathUtils.fpSign( d0 ), sign1 = MathUtils.fpSign( d1 ); if ((sign0 !== 0) || (sign1 !== 0)) { // we need to pick a different plane if ( MathUtils.fpCmp(d0,d1) === 0 ){ thePlane[3] = -vecUtils.vecDot(3, thePlane, s0); } else { var vec = vecUtils.vecSubtract(3, s1, s0 ); var yAxis = [0,1,0]; var tmp = vecUtils.vecCross( 3, vec, yAxis ); var mag = vecUtils.vecMag(3, tmp); if (MathUtils.fpSign(mag) === 0) { thePlane = [0,0,1]; thePlane[3] = -vecUtils.vecDot(3, thePlane, s0); } else { var xAxis = vecUtils.vecCross( 3, yAxis, tmp ); thePlane = vecUtils.vecCross( 3, xAxis, yAxis ); vecUtils.vecNormalize(3, thePlane, 1.0 ); thePlane[3] = -vecUtils.vecDot(3, thePlane, s0); } } // recompute the plane matrix planeMat = drawUtils.getPlaneToWorldMatrix(thePlane, MathUtils.getPointOnPlane(thePlane)); //planeMatInv = planeMat.inverse(); planeMatInv = glmat4.inverse( planeMat, [] ); p0 = MathUtils.transformPoint(p0,hitRec0.getPlaneMatrix()); p0 = MathUtils.transformPoint(p0, planeMatInv); p1 = MathUtils.transformPoint(p1,hitRec1.getPlaneMatrix()); p1 = MathUtils.transformPoint(p1, planeMatInv); } else { //planeMatInv = planeMat.inverse(); planeMatInv = glmat4.inverse( planeMat, [] ); } // determine if the geometry is going to be projected. If so a second projected rectangle is drawn var isProjected = ((MathUtils.fpCmp(thePlane[2],1.0) !== 0) || (MathUtils.fpSign(thePlane[3]) !== 0)); // TODO - We no longer need to project drawing after perspective fix. Need to clean up this code. // For now, just setting isProjected to false so rest of the drawing still works. isProjected = false; // get and draw the unprojected object points var projPtArr = []; if (isProjected) { this.getProjectedObjectPoints( s0, s1, planeMat, planeMatInv, elt, stageMat, offset, projPtArr ); } var localPt = [p0[0], p0[1], 0.0, 1.0]; s0 = viewUtils.postViewToStageWorld( MathUtils.transformPoint(localPt,planeMat), elt ); s0 = vecUtils.vecAdd(3, viewUtils.viewToScreen( MathUtils.transformPoint(s0, stageMat) ), offset ); localPt[1] = p1[1]; s1 = viewUtils.postViewToStageWorld( MathUtils.transformPoint(localPt,planeMat), elt ); s1 = vecUtils.vecAdd(3, viewUtils.viewToScreen( MathUtils.transformPoint(s1, stageMat) ), offset ); localPt[0] = p1[0]; var s2 = viewUtils.postViewToStageWorld( MathUtils.transformPoint(localPt,planeMat), elt ); s2 = vecUtils.vecAdd(3, viewUtils.viewToScreen( MathUtils.transformPoint(s2, stageMat) ), offset ); localPt[1] = p0[1]; var s3 = viewUtils.postViewToStageWorld( MathUtils.transformPoint(localPt,planeMat), elt ); s3 = vecUtils.vecAdd(3, viewUtils.viewToScreen( MathUtils.transformPoint(s3, stageMat) ), offset ); if ( isProjected) { var unprojPtArr = [s0, s1, s2, s3]; this.application.ninja.stage.draw3DProjectedAndUnprojectedRectangles( unprojPtArr, projPtArr ); } else { this.application.ninja.stage.draw3DSelectionRectangle(s0[0], s0[1], s1[0], s1[1], s2[0], s2[1], s3[0], s3[1], s0[0], s0[1]); } viewUtils.popViewportObj(); } } }, drawLine: { value: function (hitRec0, hitRec1, strokeSize, strokeColor) { var p0 = hitRec0.getScreenPoint(), p1 = hitRec1.getScreenPoint(); this.application.ninja.stage.drawLine(p0[0], p0[1], p1[0], p1[1], strokeSize, strokeColor); } }, /** * Draw Helper Functions */ /** * Returns a perfect square using the top/left/bottom/right values. */ toSquare: { value: function(x0,x1,y0,y1) { var dw = 1; var dh = 1; var w = x1 - x0, h = y1 - y0; if(w < 0) dw = -1; if(h < 0) dh = -1; if(Math.abs(w) >= Math.abs(h)) { h = (Math.abs(w) * dh); } else { w = (Math.abs(h) * dw); } return [x0,y0,w,h]; } }, toCenterRectangle: { value: function(x0,x1,y0,y1) { var x,y,w,h = 0; x = x0 - (x1 - x0); y = y0 - (y1 - y0); w = x1 - x; h = y1 - y; return [x,y,w,h]; } }, /** * Helper Functions */ unprojectPoints: { value: function( s0In, s1In, planeMat, planeMatInv, fixedS1 ) { var s0 = s0In.slice(0); var s2 = s1In.slice(0); var pt0, pt1; // calculate the mid point of the rectangle var midPt = vecUtils.vecAdd(3, s0, s2); vecUtils.vecScale(3, midPt, 0.5); s0[0] -= midPt[0]; s0[1] -= midPt[1]; s2[0] -= midPt[0]; s2[1] -= midPt[1]; // convert the 2 world space points to plane space var p0 = MathUtils.transformPoint( s0, planeMatInv ), p2 = MathUtils.transformPoint( s2, planeMatInv ); var z = p0[2]; // fill in the other 2 points on the plane to complete the 4 points var p1 = [p0[0], p2[1], z], p3 = [p2[0], p0[1], z]; // convert back to 3D space s0 = MathUtils.transformPoint( p0, planeMat ); var s1 = MathUtils.transformPoint( p1, planeMat ); s2 = MathUtils.transformPoint( p2, planeMat ); var s3 = MathUtils.transformPoint( p3, planeMat ); // unproject the 4 points var i; var p = 1400; var ptArr = [ s0, s1, s2, s3 ]; for (i=0; i<4; i++) { pt = ptArr[i]; if (MathUtils.fpCmp(p,-pt[2]) !== 0){ z = pt[2]*p/(p + pt[2]); var x = pt[0]*(p - z)/p, y = pt[1]*(p - z)/p; pt[0] = x; pt[1] = y; pt[2] = z; } } // back to 2D space... for (i=0; i<4; i++) ptArr[i] = MathUtils.transformPoint( ptArr[i], planeMatInv ); // find the 2 diagonal points to use if (fixedS1) { p0 = ptArr[0]; p1 = ptArr[1]; p2 = ptArr[2]; p3 = ptArr[3]; pt0 = p0.slice(0); pt1 = p2.slice(0); z = pt0[2]; } else { p0 = ptArr[0]; p1 = ptArr[1]; p2 = ptArr[2]; p3 = ptArr[3]; z = p0[2]; pt0 = p0.slice(0); pt1 = p2.slice(0); if (p0[0] < p2[0]){ pt0[0] = Math.max(p0[0],p1[0]); pt1[0] = Math.min(p2[0],p3[0]); } else { pt0[0] = Math.min(p0[0],p1[0]); pt1[0] = Math.max(p2[0],p3[0]); } if (p0[1] < p2[1]){ pt0[1] = Math.max(p0[1],p3[1]); pt1[1] = Math.min(p1[1],p2[1]); } else { pt0[1] = Math.min(p0[1],p3[1]); pt1[1] = Math.max(p1[1],p2[1]); } } pt0[2] = z; pt1[2] = z; var ctr = vecUtils.vecAdd(3, pt0, pt1); vecUtils.vecScale( 3, ctr, 0.5 ); // put the diagonal points back in 3D space s0 = MathUtils.transformPoint( pt0, planeMat ); s1 = MathUtils.transformPoint( pt1, planeMat ); ctr = MathUtils.transformPoint( ctr, planeMat ); // add the translation back in s0[0] += midPt[0]; s0[1] += midPt[1]; s1[0] += midPt[0]; s1[1] += midPt[1]; ctr[0] += midPt[0]; ctr[1] += midPt[1]; // set the returned values s0In[0] = s0[0]; s0In[1] = s0[1]; s0In[2] = s0[2]; s1In[0] = s1[0]; s1In[1] = s1[1]; s1In[2] = s1[2]; return ctr; } }, getProjectedObjectPoints: { value: function( u0, u1, planeMat, planeMatInv, elt, stageMat, offset, rtnScrPts ){ var s0 = u0.slice(0); var s2 = u1.slice(0); var i, z; // calculate the mid point of the rectangle var midPt = vecUtils.vecAdd(3, s0, s2); vecUtils.vecScale(3, midPt, 0.5); s0[0] -= midPt[0]; s0[1] -= midPt[1]; s2[0] -= midPt[0]; s2[1] -= midPt[1]; // unproject the 2 points var p = 1400; var ptArr = [ s0, s2 ]; for (i=0; i<2; i++) { pt = ptArr[i]; if (MathUtils.fpCmp(p,-pt[2]) !== 0) { z = pt[2]*p/(p + pt[2]); var x = pt[0]*(p - z)/p, y = pt[1]*(p - z)/p; y = pt[1]*(p - z)/p; pt[0] = x; pt[1] = y; pt[2] = z; } } // back to 2D space... for (i=0; i<2; i++) ptArr[i] = MathUtils.transformPoint( ptArr[i], planeMatInv ); // fill in the other 2 points on the plane to complete the 4 points var pt0 = ptArr[0], pt2 = ptArr[1]; z = pt0[2]; pt0[2] = z; pt2[2] = z; var pt1 = [pt0[0], pt2[1], z], pt3 = [pt2[0], pt0[1], z]; // ptArr = [pt0, pt1, pt2, pt3]; var pt; var dist, scale, pDist = 1400; // elt.webkitTransform is not always defined. for (i=0; i<4; i++) { // put the point back in 3D space pt = MathUtils.transformPoint( ptArr[i], planeMat ); // apply the perspective dist = pDist - pt[2]; if (MathUtils.fpSign(dist) !== 0) { scale = pDist / dist; pt[0] *= scale; pt[1] *= scale; pt[2] *= scale; } // add the translation back in pt[0] += midPt[0]; pt[1] += midPt[1]; // to screen coordinates pt = viewUtils.postViewToStageWorld( pt, elt ); pt = vecUtils.vecAdd(3, viewUtils.viewToScreen( MathUtils.transformPoint(pt, stageMat) ), offset ); // save the final result rtnScrPts[i] = pt; } } } });