/** *Handles are created on-the-fly for a figure. They are completelly managed by the HandleManager *Each handle is responsable for an action. The {Handle} does not need to keep a reference to the parent shape *as the HandleManager will do that. *@constructor *@this {Handle} *@param {String} type - the type of handle **/ function Handle(type){ /**Type of Handle*/ this.type = type; /*These are stupidly initialized to 0 but they should not be present at all... *anyway they got set to the proper values in HandleManager::handleGetAll() function*/ /**The center of the circle (x coordinates)*/ this.x = 0; /**The center of the circle (y coordinates)*/ this.y = 0; /**Used by Connector handles, to not display redundant handles (i.e. when they are on the same line)*/ this.visible = true; } /**It's a (static) vector of handle types * Note: R - stand for rotation * Note: More handles might be added in the future : like handles to increase the number of edges for a hexagone * Those handles will be specific for a figure **/ Handle.types = ['n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw', 'r' ]; //DO NOT CHANGE THE ORDER OF THESE VALUES /**It's a (static) vector of connector types*/ Handle.connectorTypes = ['ns', 'ew']; /**Creates a {Handle} out of JSON parsed object *@param {JSONObject} o - the JSON parsed object *@return {Handle} a newly constructed Handle *@author Alex Gheorghiu **/ Handle.load = function(o){ var newHandle = new Handle(o.type); newHandle.x = o.x; newHandle.y = o.y; newHandle.visible = o.visible; return newHandle; } /**Creates an array of handles from an array of {JSONObject}s *@param {Array} v - the array of JSONObjects *@return an {Array} of {Handle}s *@author Alex Gheorghiu **/ Handle.loadArray = function(v){ var newHandles = []; for(var i=0; i< v.length; i++){ newHandles.push(Handle.load(v[i])); } return newHandles; } /**Default handle radius*/ Handle.RADIUS = 3; Handle.prototype = { constructor : Handle, /**Compares to another Handle *@param {Handle} group - - the other glue *@return {Boolean} - true if equals, false otherwise **/ equals : function(anotherHandle){ if(!anotherHandle instanceof Handle){ return false; } return this.type == anotherHandle.type && this.x == anotherHandle.x && this.y == anotherHandle.y && this.visible == anotherHandle.visible; }, actionFigure : function(lastMove, newX, newY){ var m = this.actionShape(lastMove, newX, newY); if(m[0] == 'rotate'){ var cmdRotate = new FigureRotateCommand(HandleManager.shape.id, m[1], m[2]); cmdRotate.execute(); History.addUndo(cmdRotate); } else if(m[0] == 'scale'){ var cmdScale = new FigureScaleCommand(HandleManager.shape.id, m[1], m[2]); cmdScale.execute(); History.addUndo(cmdScale); } }, actionContainer : function(lastMove, newX, newY){ var m = this.actionShape(lastMove, newX, newY); if(m[0] == 'rotate'){ //simply ingnore rotate throw "Handles.js -> actionContainer -> rotate should be disabled for Container" // var cmdRotate = new ContainerRotateCommand(HandleManager.shape.id, m[1], m[2]); // cmdRotate.execute(); // History.addUndo(cmdRotate); } else if(m[0] == 'scale'){ var cmdScale = new ContainerScaleCommand(HandleManager.shape.id, m[1], m[2]); cmdScale.execute(); History.addUndo(cmdScale); } }, actionGroup : function(lastMove, newX, newY){ var m = this.actionShape(lastMove, newX, newY); if(m[0] == 'rotate'){ var cmdRotate = new GroupRotateCommand(HandleManager.shape.id, m[1], m[2]); cmdRotate.execute(); History.addUndo(cmdRotate); } else if(m[0] == 'scale'){ var cmdScale = new GroupScaleCommand(HandleManager.shape.id, m[1], m[2]); cmdScale.execute(); History.addUndo(cmdScale); } }, /**Handle actions for Figure * *@param {Array } lastMove - an array that will hold [x,y] of last x & y coordinates *@param {Number} newX - new X coordinate *@param {Number} newY - new Y coordinate *@return {Array} of two matrixes direct and revers **/ actionShape: function(lastMove, newX, newY){ var matrixes = []; var figBounds = HandleManager.shape.getBounds(); var transX = 0; //the amount of translation on Ox var transY = 0; //the amount of translation on Oy var scaleX = 1; //the scale percentage on Ox var scaleY = 1; //the scale percentage on Oy var arc = false; //see if we have a resize and prepare the figure by moving it back to Origin and "unrotate" it if(this.type == 'r'){ //rotationCoords[0] is always the center of the shape, we clone it as when we do -rotationCoords[0].x, it is set to 0. var center = HandleManager.shape.rotationCoords[0].clone(); var endAngle = Util.getAngle(HandleManager.shape.rotationCoords[0],new Point(newX,newY)); var startAngle = Util.getAngle(HandleManager.shape.rotationCoords[0],HandleManager.shape.rotationCoords[1]);//new Point(lastMove[0],lastMove[1]) var rotAngle = endAngle - startAngle; // HandleManager.shape.transform(Matrix.translationMatrix(-center.x, -center.y)) // HandleManager.shape.transform(Matrix.rotationMatrix(rotAngle)) // HandleManager.shape.transform(Matrix.translationMatrix(center.x, center.y)); var equivTransfMatrix = Matrix.mergeTransformations( Matrix.translationMatrix(-center.x, -center.y), Matrix.rotationMatrix(rotAngle), Matrix.translationMatrix(center.x,center.y) ); //TODO: make somehow to compute the inverse of it. //@see http://en.wikipedia.org/wiki/Transformation_matrix#Rotation to find inverses var inverseTransfMatrix = Matrix.mergeTransformations( Matrix.translationMatrix(-center.x, -center.y), Matrix.rotationMatrix(-rotAngle), Matrix.translationMatrix(center.x,center.y) ); matrixes = ['rotate', equivTransfMatrix, inverseTransfMatrix]; } else{ //if not "rotate" (figure), "updown", "leftright" (connector) //find the angle by which the figure was rotated (for any figure this is initally 0 so if != than 0 we have a rotation) var angle = Util.getAngle(HandleManager.shape.rotationCoords[0], HandleManager.shape.rotationCoords[1]); //save initial figure's center var oldCenter = HandleManager.shape.rotationCoords[0].clone(); //move the new [x,y] to the "un-rotated" and "un-translated" space var p = new Point(newX,newY); p.transform(Matrix.translationMatrix(-oldCenter.x,-oldCenter.y)); p.transform(Matrix.rotationMatrix(-angle)); p.transform(Matrix.translationMatrix(oldCenter.x,oldCenter.y)); newX = p.x; newY = p.y; var handlerPoint=new Point(this.x,this.y) //Handler's center point (used to draw it's circle) //rotate that as well. handlerPoint.transform(Matrix.translationMatrix(-oldCenter.x,-oldCenter.y)); handlerPoint.transform(Matrix.rotationMatrix(-angle)); handlerPoint.transform(Matrix.translationMatrix(oldCenter.x,oldCenter.y)); switch(this.type){ case 'n': /*move the xOy coodinates at the bottom of the figure and then scale*/ transY = figBounds[3]; if(newY < figBounds[3]-5){ //North must not get too close to South scaleY = (figBounds[3]-newY)/(figBounds[3] - handlerPoint.y); } break; case 's': /*move the xOy coodinates at the top of the figure (superfluous as we are there already) and then scale*/ transY = figBounds[1]; if(newY > figBounds[1]+5){ //South must not get too close to North scaleY = (newY-figBounds[1])/(handlerPoint.y-figBounds[1]); } break; case 'w': /*move the xOy coordinates at the right of the figure and then scale*/ transX = figBounds[2]; if(newX < figBounds[2]-5){ //West(newX) must not get too close to East(figBounds[2]) scaleX = (figBounds[2]-newX)/(figBounds[2]-handlerPoint.x); } break; case 'e': /*move the xOy coodinates at the left of the figure (superfluous as we are there already) and then scale*/ transX = figBounds[0]; if(newX > figBounds[0]+5){ scaleX = (newX-figBounds[0])/(handlerPoint.x-figBounds[0]); } break; case 'nw': /*You can think as a combined North and West action*/ transX = figBounds[2]; transY = figBounds[3]; if(newXfigBounds[0]+5 && newYfigBounds[1]+5){ scaleX = (figBounds[2]-newX)/((figBounds[2]-handlerPoint.x)); scaleY = (newY-figBounds[1])/(handlerPoint.y-figBounds[1]); } break; case 'se': transX = figBounds[0]; transY = figBounds[1]; if(newX>figBounds[0]+5 && newY>figBounds[1]+5){ scaleY= (newY-figBounds[1]) / (handlerPoint.y-figBounds[1]); scaleX= (newX-figBounds[0]) / (handlerPoint.x-figBounds[0]); } break; }//end switch /*By default the NW, NE, SW and SE are scalling keeping the ratio *but you can use SHIFT to cause a free (no keep ratio) change *So, if no SHIFT pressed we force a "keep ration" resize **/ if(!SHIFT_PRESSED && transX != 0 && transY != 0){//keep ratios, only affects ne/nw resize //if we are scaling along the x axis (West or East resize), with an arc(behaves like corner) then scale relative to x movement //TODO: what's the reason for this? if(this.getCursor()=="w-resize" || this.getCursor()=="e-resize"){ scaleY = scaleX; } else { //for everything else, scale based on y scaleX = scaleY; } } //move the figure to origine and "unrotate" it var matrixToOrigin = Matrix.mergeTransformations( Matrix.translationMatrix(-oldCenter.x,-oldCenter.y), Matrix.rotationMatrix(-angle), Matrix.translationMatrix(oldCenter.x,oldCenter.y) ); // HandleManager.shape.transform(Matrix.translationMatrix(-oldCenter.x,-oldCenter.y)); // HandleManager.shape.transform(Matrix.rotationMatrix(-angle)); // HandleManager.shape.transform(Matrix.translationMatrix(oldCenter.x,oldCenter.y)); // // //scale matrix // HandleManager.shape.transform(Matrix.translationMatrix(-transX, -transY)); // HandleManager.shape.transform(Matrix.scaleMatrix(scaleX, scaleY)) // HandleManager.shape.transform(Matrix.translationMatrix(transX, transY)); // // // //move and rotate the figure back to its original coordinates // HandleManager.shape.transform(Matrix.translationMatrix(-oldCenter.x,-oldCenter.y)); // HandleManager.shape.transform(Matrix.rotationMatrix(angle)); // HandleManager.shape.transform(Matrix.translationMatrix(oldCenter.x,oldCenter.y)); // HandleManager.shape.transform(directMatrix); //scale matrix var scaleMatrix = Matrix.mergeTransformations( Matrix.translationMatrix(-transX, -transY), Matrix.scaleMatrix(scaleX, scaleY), Matrix.translationMatrix(transX, transY) ); var unscaleMatrix = Matrix.mergeTransformations( Matrix.translationMatrix(-transX, -transY), Matrix.scaleMatrix(1/scaleX, 1/scaleY), Matrix.translationMatrix(transX, transY) ); //move and rotate the figure back to its original coordinates var matrixBackFromOrigin = Matrix.mergeTransformations( Matrix.translationMatrix(-oldCenter.x,-oldCenter.y), Matrix.rotationMatrix(angle), Matrix.translationMatrix(oldCenter.x,oldCenter.y) ); var directMatrix = Matrix.mergeTransformations(matrixToOrigin, scaleMatrix, matrixBackFromOrigin); var reverseMatrix = Matrix.mergeTransformations(matrixToOrigin, unscaleMatrix, matrixBackFromOrigin); matrixes = ['scale', directMatrix, reverseMatrix]; } //end else return matrixes; }, /** *Handle actions for Connector * *@param {Array } lastMove - an array that will hold [x,y] of last x & y coordinates *@param {Number} newX - new X coordinate *@param {Number} newY - new Y coordinate **/ actionConnector: function(lastMove, newX, newY){ switch(this.type){ case 'v': var index; //find the two turning points this handle is in between for(var i = 1; i < HandleManager.shape.turningPoints.length-1; i++){ if(HandleManager.shape.turningPoints[i-1].y == HandleManager.shape.turningPoints[i].y && HandleManager.shape.turningPoints[i].y == this.y && Math.min(HandleManager.shape.turningPoints[i].x, HandleManager.shape.turningPoints[i-1].x) <= this.x && Math.max(HandleManager.shape.turningPoints[i].x, HandleManager.shape.turningPoints[i-1].x) >= this.x) { index = i; } } var deltaY = newY - lastMove[1]; //Take changes on Oy var translationMatrix = Matrix.translationMatrix(0, deltaY); //Generate translation matrix /*TODO: make changes to DIAGRAMO.debugSolutions here * because, otherwise, those changes are not reflected in debug painting of Connector */ //Pick turning points neighbours and translate them HandleManager.shape.turningPoints[index-1].transform(translationMatrix); HandleManager.shape.turningPoints[index].transform(translationMatrix); // add new changes in {Connector} HandleManager.shape.addUserChange({ align: Connector.USER_CHANGE_VERTICAL_ALIGN, delta: deltaY, index: index - 1 }); HandleManager.shape.addUserChange({ align: Connector.USER_CHANGE_VERTICAL_ALIGN, delta: deltaY, index: index }); break; case 'h': var index; //find the two turning points this handle is in between for(var i = 1; i < HandleManager.shape.turningPoints.length-1; i++){ if(HandleManager.shape.turningPoints[i-1].x == HandleManager.shape.turningPoints[i].x && HandleManager.shape.turningPoints[i].x == this.x && Math.min(HandleManager.shape.turningPoints[i].y, HandleManager.shape.turningPoints[i-1].y) <= this.y && Math.max(HandleManager.shape.turningPoints[i].y, HandleManager.shape.turningPoints[i-1].y) >= this.y) { index = i; } } var deltaX = newX-lastMove[0]; //Take changes on Ox var translationMatrix = Matrix.translationMatrix(deltaX, 0); //Generate translation matrix /*TODO: make changes to DIAGRAMO.debugSolution here * because, otherwise, those changes are not reflected in debug painting of Connector */ //Pick turning points neighbours and translate them HandleManager.shape.turningPoints[index-1].transform(translationMatrix); HandleManager.shape.turningPoints[index].transform(translationMatrix); // add new changes in {Connector} HandleManager.shape.addUserChange({ align: Connector.USER_CHANGE_HORIZONTAL_ALIGN, delta: deltaX, index: index - 1 }); HandleManager.shape.addUserChange({ align: Connector.USER_CHANGE_HORIZONTAL_ALIGN, delta: deltaX, index: index }); break; } HandleManager.shape.updateMiddleText(); }, /**Handle an action. Simply dispatch to the correct handler *@param {Array } lastMove - an array that will hold [x,y] of last x & y coordinates *@param {Number} newX - new X coordinate *@param {Number} newY - new Y coordinate **/ action: function(lastMove, newX, newY){ if(lastMove == null || lastMove.length != 2){ throw 'Handle:action() Last move is wrong'; } if(HandleManager.shape instanceof Connector){ this.actionConnector(lastMove, newX, newY); } else if(HandleManager.shape instanceof Figure){ this.actionFigure(lastMove, newX, newY); } else if(HandleManager.shape instanceof Group){ this.actionGroup(lastMove, newX, newY); } else if(HandleManager.shape instanceof Container){ this.actionContainer(lastMove, newX, newY); } }, /**This is the method you have to call to paint a handler * All handles will be circles...so we avoid to much of the computing for rectangle handles * They will have a filling color (green) and a stoke (black) * @param {Context} context - the 2D context **/ paint : function(context){ context.save(); //fill the handler context.beginPath(); context.arc(this.x, this.y, Handle.RADIUS, 0, Math.PI*2, false); context.fillStyle = "rgb(0,255,0)"; context.closePath(); context.fill(); //stroke the handler context.beginPath(); context.arc(this.x, this.y, Handle.RADIUS, 0, Math.PI*2, false); context.strokeStyle = "rgb(0,0,0)"; context.lineWidth = defaultThinLineWidth; context.closePath(); context.stroke(); // if(this.type == 'r'){ // var line = new Line(new Point(this.x,this.y), new Point(HandleManager.handles[1].x,HandleManager.handles[1].y)); // line.style.dashLength = 3; // line.style.strokeStyle = "grey"; // line.style.lineWidth = defaultThinLineWidth; // line.paint(context); // } // context.restore(); }, /**See if the handle contains a point *@param {Number} x - the x coordinate of the point *@param {Number} y - the y coordinate of the point **/ contains:function(x,y){ var p=new Point(this.x,this.y); return p.near(x,y, Handle.RADIUS); }, /** *Get a handle bounds **/ getBounds : function(){ return [this.x - Handle.RADIUS, this.y - Handle.RADIUS, this.x + Handle.RADIUS,this.y + Handle.RADIUS]; }, /** *Transform the Handle through a matrix *@param {Matrix} matrix - the matrix that will perform the transformation **/ transform: function(matrix){ var p=new Point(this.x,this.y) p.transform(matrix); this.x=p.x; this.y=p.y; }, /**Get the specific cursor for this handle. Cursor is ONLY a visual clue for * the user to know how to move his mouse. * *Behaviour: * If North handle is in the North we have 'Up/Down arrow" cursor * If North handle is in the West (so it has "Left/Right arrow") (or anything different that North) * we have 'Left/Right arrow' cursor but the figure will expand as follows: * - rotate back to initial position * - expand North * - rotate back to current position * - repaint * @see http://www.w3schools.com/css/pr_class_cursor.asp for cusor values * @author Zack Newsham * @author Alex Gheorghiu **/ getCursor:function(){ if(HandleManager.shape instanceof Connector){ if(this.visible == false){ return ""; } if(this.type == 'v'){ return 'ns-resize'; } else{ return 'ew-resize'; } } //end if Connector else if(HandleManager.shape instanceof Figure ){ if(this.visible == false){ return ""; } if(this.type == 'r'){ return 'move'; } var figureBounds = HandleManager.shape.getBounds(); //get figure's bounds var figureCenter = new Point(figureBounds[0] + ((figureBounds[2]-figureBounds[0])/2), (figureBounds[1] + ((figureBounds[3] - figureBounds[1])/2)) ); //get figure's center //find north var closestToNorthIndex = -1; //keeps the index of closest handle to North var minAngleToNorth = 2 * Math.PI; //keeps the smallest (angular) distante to North var myIndex = -1; for(var i=0; i