From b89a7ee8b956c96a1dcee995ea840feddc5d4b27 Mon Sep 17 00:00:00 2001 From: Pierre Frisch Date: Thu, 22 Dec 2011 07:25:50 -0800 Subject: First commit of Ninja to ninja-internal Signed-off-by: Valerio Virgillito --- js/helper-classes/3D/snap-manager.js | 2247 ++++++++++++++++++++++++++++++++++ 1 file changed, 2247 insertions(+) create mode 100644 js/helper-classes/3D/snap-manager.js (limited to 'js/helper-classes/3D/snap-manager.js') diff --git a/js/helper-classes/3D/snap-manager.js b/js/helper-classes/3D/snap-manager.js new file mode 100644 index 00000000..3ed96082 --- /dev/null +++ b/js/helper-classes/3D/snap-manager.js @@ -0,0 +1,2247 @@ +/* +This file contains proprietary software owned by Motorola Mobility, Inc.
+No rights, expressed or implied, whatsoever to this software are provided by Motorola Mobility, Inc. hereunder.
+(c) Copyright 2011 Motorola Mobility, Inc. All Rights Reserved. +
*/ + +/////////////////////////////////////////////////////////////////////// +// Class SnapManager +// Class to do hit testing of objects in the html page +/////////////////////////////////////////////////////////////////////// +var Montage = require("montage/core/core").Montage, + Component = require("montage/ui/component").Component; + +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; +var HitRecord = require("js/helper-classes/3D/hit-record").HitRecord; +var Snap2DRecord = require("js/helper-classes/3D/snap-2d-record").Snap2DRecord; +var NJUtils = require("js/lib/NJUtils").NJUtils; + +var SnapManager = exports.SnapManager = Montage.create(Component, { + /////////////////////////////////////////////////////////////////////// + // Instance variables + /////////////////////////////////////////////////////////////////////// + currentStage: { value: null, writable: true }, + drawingCanvas: { value: null, writable: true}, + + // we keep a stack of working planes to facilitate working on other planes temporarily + _workingPlaneStack : { value: [], writable: true }, + + // snapping radii relative to a 25 pixel grid + GRID_VERTEX_HIT_RAD : { value: 10, writable: true }, + GRID_EDGE_HIT_RAD : { value: 6, writable: true}, + + // these are the grid snapping tolerances scaled to the current grid spacing + _gridVertexHitRad : { value: this.GRID_VERTEX_HIT_RAD, writable: true }, + _gridEdgeHitRad : { value: this.GRID_EDGE_HIT_RAD, writable: true }, + + ELEMENT_VERTEX_HIT_RAD : { value: 18, writable: true }, + ELEMENT_EDGE_HIT_RAD : { value: 14, writable: true }, + + // keep a reference to the most recent hitRecord. Used for drawing feedback on the stage + _lastHit : { value: null, writable: true }, + + // keep a list of objects to avoid snapping to + _avoidList : { value: [], writable: true }, + + // keep a cache of 2D elements to snap to + _elementCache : { value: null, writable: true }, + _isCacheInvalid : { value: false, writable: true }, + + // the snap manager can handle a 2D plane for dragging. + // A call to initDragPlane sets these variables. + // a call to clearDragPlane MUST be called on the completion of a drag + _hasDragPlane : {value: false, writable: true }, + _dragPlane : { value: null, writable: true }, + _dragPlaneToWorld : { value: Matrix.I(4), writable: true }, + _worldToDragPlane : { value: Matrix.I(4), writable: true }, + _dragPlaneActive : {value: false, writable: true }, + + // cache the matrix linking stage world and global spaces + _stageWorldToGlobalMat : { value: Matrix.I(4), writable: true }, + _globalToStageWorldMat : { value: Matrix.I(4), writable: true }, + + // various flags to enable snapping + _snapAlignEnabled : {value: true, writable: true }, + _elementSnapEnabled : {value: true, writable: true }, + _gridSnapEnabled : {value: true, writable: true }, + + // these represent the app level snap settings as set by the end user through + // the menus. These should be stored somewhere else and serialized. Putting them here for now... + _snapAlignEnabledAppLevel : {value: true, writable: true }, + _elementSnapEnabledAppLevel : {value: true, writable: true }, + _gridSnapEnabledAppLevel : {value: true, writable: true }, + + // App Model pointer + appModel: { value: null }, + + + /////////////////////////////////////////////////////////////////////// + // Property accessors + /////////////////////////////////////////////////////////////////////// + pushWorkingPlane : { value: function (p) { this._workingPlaneStack.push(workingPlane.slice(0)); workingPlane = p.slice(0); }}, + popWorkingPlane : { value: function () { workingPlane = this._workingPlaneStack.pop(); return workingPlane; }}, + + getStageWidth : { value: function () { + return parseInt(this.currentStage.offsetWidth); + }}, + + getStageHeight : { value: function () { + return parseInt(this.currentStage.offsetHeight); + }}, + + getStage : { value: function() { return this.currentStage; }}, + + getGridVertexHitRad : { value: function() { return this._gridVertexHitRad; }}, + getGridEdgeHitRad : { value: function() { return this._gridEdgeHitRad; }}, + + getLastHit : { value: function() { return this._lastHit; }}, + setLastHit : { value: function(h) { this._lastHit = h; }}, + + hasDragPlane : { value: function() { return this._hasDragPlane; }}, + getDragPlane : { value: function() { return this._dragPlane.slice(0); }}, + + has2DCache : { value: function() { return (this._elementCache && !this._isCacheInvalid); }}, + + enableSnapAlign : { value: function(e) { this._snapAlignEnabled = e; }}, + snapAlignEnabled : { value: function() { return this._snapAlignEnabled; }}, + enableElementSnap : { value: function(e) { this._elementSnapEnabled = e; }}, + elementSnapEnabled : { value: function() { return this._elementSnapEnabled; }}, + enableGridSnap : { value: function(e) { this._gridSnapEnabled = e; }}, + gridSnapEnabled : { value: function() { return this._gridSnapEnabled; }}, + + enableSnapAlignAppLevel : { value: function(e) { this._snapAlignEnabledAppLevel = e; }}, + snapAlignEnabledAppLevel : { value: function() { return this._snapAlignEnabledAppLevel; }}, + enableElementSnapAppLevel : { value: function(e) { this._elementSnapEnabledAppLevel = e; }}, + elementSnapEnabledAppLevel : { value: function() { return this._elementSnapEnabledAppLevel; }}, + enableGridSnapAppLevel : { value: function(e) { this._gridSnapEnabledAppLevel = e; }}, + gridSnapEnabledAppLevel : { value: function() { return this._gridSnapEnabledAppLevel; }}, + + /////////////////////////////////////////////////////////////////////// + // Methods + /////////////////////////////////////////////////////////////////////// + initialize: { + value: function() { + this.eventManager.addEventListener("elementDeleted", this, false); + } + }, + + bindSnap: { + value: function() { + this.addEventListener("change@appModel.snap", this.toggleSnap, false); + this.addEventListener("change@appModel.snapGrid", this.toggleSnapGrid, false); + this.addEventListener("change@appModel.snapObjects", this.toggleSnapObjects, false); + this.addEventListener("change@appModel.snapAlign", this.toggleSnapAlign, false); + } + }, + + toggleSnap: { + value: function() { + this.enableSnapAlignAppLevel(this.appModel.snap); + this.enableElementSnapAppLevel(this.appModel.snap); + this.enableGridSnapAppLevel(this.appModel.snap); + } + }, + + toggleSnapGrid: { + value: function() { + this.enableGridSnapAppLevel(this.appModel.snapGrid); + } + }, + + toggleSnapObjects: { + value: function() { + this.enableElementSnapAppLevel(this.appModel.snapObjects); + } + }, + + toggleSnapAlign: { + value: function() { + this.enableSnapAlignAppLevel(this.appModel.snapAlign); + } + }, + + + handleElementDeleted: { + value: function(event) { + this.removeElementFrom2DCache(event.detail); + } + }, + + + setCurrentStage: { + value: function(stage) { + this.currentStage = stage; + } + }, + + snap : { + value: function (xScreen, yScreen, snap3D, quadPt) + { + // force a 3D snap if a 2D snap is requested but the 2D cache has not been initialized + if (!snap3D && !this._elementCache) snap3D = true; + + // clear out the last hit record + this.setLastHit( null ); + + // snap to elements first, then the working plane + var screenPt = [xScreen, yScreen]; + var hitRecArray = new Array(); + if (this.elementSnapEnabled()) + { + if (snap3D) + this.snapToElements( screenPt, hitRecArray ); + + // now always doing a 2D snap + this.snapToCached2DElements( screenPt, hitRecArray ); + } + + // if we did not hit anything, and we are in 2D mode, try a snap align + if (this.snapAlignEnabled()) + { + //if (hitRecArray.length == 0) + this.snapAlign( screenPt, hitRecArray ); + } + + + // if we did not find any objects to snap to, snap to the working plane and/or grid + //if (hitRecArray.length == 0) + { + var stage = this.getStage(); + var parentPt; + if (quadPt) + parentPt = Vector.create([quadPt[0], quadPt[1], 0.0]); + else + parentPt = Vector.create([xScreen, yScreen, 0.0]); + var vec = viewUtils.parentToChildVec(parentPt, stage); + if (vec) + { + // activate the drag working plane + if (!snap3D && this.hasDragPlane()) + this.activateDragPlane(); + + // project to the working plane + var currentWorkingPlane = workingPlane.slice(0); + var wp = currentWorkingPlane.slice(0); + var mat = viewUtils.getMatrixFromElement(stage); + wp = MathUtils.transformPlane(wp, mat); + var eyePt = viewUtils.getEyePoint(); + var projPt = MathUtils.vecIntersectPlane(eyePt, vec, wp); + if (projPt) + { + // the local point gets stored in the coordinate space of the plane + var wpMat = drawUtils.getPlaneToWorldMatrix(currentWorkingPlane, MathUtils.getPointOnPlane(currentWorkingPlane)); + projPt[3] = 1.0; + //var planeToViewMat = mat.multiply(wpMat); + var planeToViewMat = glmat4.multiply(mat, wpMat, []); + //var viewToPlaneMat = planeToViewMat.inverse(); + var viewToPlaneMat = glmat4.inverse( planeToViewMat, [] ); + var planePt = projPt.slice(0); + planePt[3] = 1.0; + //planePt = viewToPlaneMat.multiply(planePt); + planePt = glmat4.multiplyVec3( viewToPlaneMat, planePt ); + + // get the screen position of the projected point + viewUtils.setViewportObj(stage); + var offset = viewUtils.getElementOffset(stage); + offset[2] = 0; + var scrPt = viewUtils.viewToScreen(projPt); + //scrPt = scrPt.add(offset); + scrPt = vecUtils.vecAdd(3, scrPt, offset); + + // create the hit record + var hitRec = Object.create(HitRecord);//new HitRecord(); + hitRec.setLocalPoint(planePt); + hitRec.setPlaneMatrix( wpMat ); + hitRec.setScreenPoint(scrPt); + hitRec.setPlane(currentWorkingPlane); + hitRec.setType( hitRec.SNAP_TYPE_STAGE ); + hitRec.setElt( stage ); + if (quadPt) hitRec.setUseQuadPoint( true ); + + // try snapping to the 3D grid, or to the stage boundaries if the grid is not displayed + if (this.gridSnapEnabled()) + this.snapToGrid( hitRec ); + + // save the hit record + hitRecArray.push( hitRec ); + + // restore the original working plane + if (!snap3D && this.hasDragPlane()) + this.deactivateDragPlane(); + } + } + } //if (hitRecArray.length == 0) + + var rtnHit; + if (hitRecArray.length > 0) + { + this.sortHitRecords( hitRecArray ); + rtnHit = hitRecArray[0]; + } + + // catch-all to turn off drag plane snapping + this.deactivateDragPlane(); + + this.setLastHit( rtnHit ); + return rtnHit; + } + }, + + snapToGrid : { + value: function( hitRec ) + { + this.calculateGridHitRadii(); + + var stage = this.getStage(); + var stageMat = viewUtils.getMatrixFromElement( stage ); + var offset = viewUtils.getElementOffset( stage ); + MathUtils.makeDimension3( offset ); + viewUtils.setViewportObj( stage ); + var scrPt = hitRec.getScreenPoint(); + + // get the grid spacing + var dx = drawUtils.getGridVerticalSpacing(), + dy = drawUtils.getGridHorizontalSpacing(); + var verticalLineCount = drawUtils.getGridVerticalLineCount(), + horizontalLineCount = drawUtils.getGridHorizontalLineCount(); + if (!drawUtils.isDrawingGrid()) + { + dx = this.getStageWidth(); + dy = this.getStageHeight(); + verticalLineCount = 2; + horizontalLineCount = 2; + } + + // get the point to the lower left of the plane point and + // see if it falls within the snap distance + var origin = Vector.create( [-0.5*this.getStageWidth(), -0.5*this.getStageHeight()] ); + var planePt = hitRec.getLocalPoint(); + var dToOrigin = MathUtils.vecSubtract(planePt, origin); + var nx = Math.floor( dToOrigin[0]/dx), + ny = Math.floor( dToOrigin[1]/dy ); + + if ((nx < 0) || (nx >= (verticalLineCount-1))) + { + //console.log( "off the vertical end" ); + return false; + } + if ((ny < 0) || (ny >= (horizontalLineCount-1))) + { + //console.log( "off the horizontal end" ); + return false; + } + + var pt00 = Vector.create( [ + origin[0] + nx*dx, + origin[1] + ny*dy, + 0.0 + ] ); + var planeMat = hitRec.getPlaneMatrix(); + var scrPt2 = viewUtils.postViewToStageWorld( MathUtils.transformPoint(pt00,planeMat), stage ); + scrPt2 = MathUtils.makeDimension3( scrPt2 ); + scrPt2 = vecUtils.vecAdd(3, viewUtils.viewToScreen( MathUtils.transformPoint(scrPt2, stageMat) ), offset ); + + var dist = MathUtils.vecDist( scrPt, scrPt2 ); + if (dist <= this.getGridVertexHitRad() ) + { + hitRec.setLocalPoint( pt00 ); + hitRec.setScreenPoint( scrPt2 ); + hitRec.setType( hitRec.SNAP_TYPE_GRID_VERTEX ); + return true; + } + + // check the next point and the edge connecting them + if (this.snapToGridEdge( planePt, scrPt, pt00, scrPt2, origin, nx+1, ny, hitRec, hitRec.SNAP_TYPE_GRID_HORIZONTAL )) + return true; + + // check the other edge + if (this.snapToGridEdge( planePt, scrPt, pt00, scrPt2, origin, nx, ny+1, hitRec, hitRec.SNAP_TYPE_GRID_VERTICAL )) + return true; + + // check the far corner point and 2 edges out from it + var pt11 = Vector.create( [ + origin[0] + (nx+1)*dx, + origin[1] + (ny+1)*dy, + 0.0 + ] ); + var scrPt4 = viewUtils.postViewToStageWorld( MathUtils.transformPoint(pt11,planeMat), stage ); + scrPt4 = MathUtils.makeDimension3( scrPt4 ); + scrPt4 = vecUtils.vecAdd(3, viewUtils.viewToScreen( MathUtils.transformPoint(scrPt4, stageMat) ), offset ); + var dist = MathUtils.vecDist( scrPt, scrPt4 ); + nx++; ny++; + if (dist <= this.getGridVertexHitRad() ) + { + hitRec.setLocalPoint( pt11 ); + hitRec.setScreenPoint( scrPt4 ); + hitRec.setType( hitRec.SNAP_TYPE_GRID_VERTEX ); + return true; + } + + // check the next point and the edge connecting them + if (this.snapToGridEdge( planePt, scrPt, pt11, scrPt4, origin, nx-1, ny, hitRec, hitRec.SNAP_TYPE_GRID_HORIZONTAL )) + return true; + + // check the other edge + if (this.snapToGridEdge( planePt, scrPt, pt11, scrPt4, origin, nx, ny-1, hitRec, hitRec.SNAP_TYPE_GRID_VERTICAL )) + return true; + + return false; + } + }, + + + snapToGridEdge : + { + value : function( pt, scrPt, gridPt, gridPtScr, gridOrigin, nx, ny, hitRec, hitType ) + { + var stage = this.getStage(); + var stageMat = viewUtils.getMatrixFromElement( stage ); + var offset = viewUtils.getElementOffset( stage ); + MathUtils.makeDimension3( offset ); + var planePt = hitRec.getLocalPoint(); + var planeMat = hitRec.getPlaneMatrix(); + + var dx = drawUtils.getGridVerticalSpacing(), + dy = drawUtils.getGridHorizontalSpacing(); + var verticalLineCount = drawUtils.getGridVerticalLineCount(), + horizontalLineCount = drawUtils.getGridHorizontalLineCount(); + if (!drawUtils.isDrawingGrid()) + { + dx = this.getStageWidth(); + dy = this.getStageHeight(); + verticalLineCount = 2; + horizontalLineCount = 2; + } + + var edgePt = Vector.create( [ + gridOrigin[0] + nx*dx, + gridOrigin[1] + ny*dy, + 0.0 + ] ); + var scrPt2 = viewUtils.postViewToStageWorld( MathUtils.transformPoint(edgePt,planeMat), stage ); + scrPt2 = MathUtils.makeDimension3( scrPt2 ); + scrPt2 = vecUtils.vecAdd(3, viewUtils.viewToScreen( MathUtils.transformPoint(scrPt2, stageMat) ), offset ); + var dist = MathUtils.vecDist( scrPt, scrPt2 ); + if (dist <= this.getGridVertexHitRad() ) + { + hitRec.setLocalPoint( edgePt ); + hitRec.setScreenPoint( scrPt2 ); + hitRec.setType( hitRec.SNAP_TYPE_GRID_VERTEX ); + return true; + } + + // check the line between the 2 previous points + var nearPt = MathUtils.nearestPointOnLine2D( gridPt, MathUtils.vecSubtract(edgePt,gridPt), pt ); + MathUtils.makeDimension3( nearPt ); + var tmpPt = MathUtils.transformPoint(nearPt,planeMat); + var scrPt3 = viewUtils.postViewToStageWorld( tmpPt, stage ); + scrPt3 = MathUtils.makeDimension3( scrPt3 ); + scrPt3 = vecUtils.vecAdd(3, viewUtils.viewToScreen( MathUtils.transformPoint(scrPt3, stageMat) ), offset ); + var edgeDist = MathUtils.vecDist( scrPt, scrPt3 ); + if (edgeDist <= this.getGridEdgeHitRad() ) + { + hitRec.setLocalPoint( nearPt ); + hitRec.setScreenPoint( scrPt3 ); + hitRec.setType( hitType ); + return true; + } + + return false; + } + }, + + clear2DCache : { + value : function() { + // clear the 2D cache flags in the objects + if (this._elementCache) + { + var n = this._elementCache.length; + for (var i=0; i= 0) + { + var n = this._elementCache.length; + this._elementCache[index] = this._elementCache[n-1]; + this._elementCache.pop(); + found = true; + } + + return found; + } + }, + + addElementTo2DCache : { + value: function( elt ) { + var added = false; + if (this.hasDragPlane()) + { + // make sure the element is not already in there + if (this.findElementIn2DCache( elt ) == -1) + { + var plane = this.getDragPlane(); + var onPlane = this.elementIsOnPlane( elt, plane ); + if (onPlane) + { + added = true; + + var snapRec = new Snap2DRecord(); + snapRec.init( elt ); + this._elementCache.push( snapRec ); + + if (!elt.elementModel) + { + NJUtils.makeElementModel2(elt); + } + elt.elementModel.isIn2DSnapCache = true; + } + else if (elt.elementModel) + elt.elementModel.isIn2DSnapCache = false; + } + } + + return added; + } + }, + + elementIsIn2DCache : { + value: function( target ) { + var found = false; + var index = this.findElementIn2DCache( target ); + if (index >= 0) + found = true; + + return found; + } + }, + + findElementIn2DCache : { + value: function( target ) { + var rtnIndex = -1; + if (this._elementCache) + { + var n = this._elementCache.length; + for (var i=0; i 0) + { + // check if the element is on the specified plane + var onPlane = this.elementIsOnPlane( elt, plane ); + if (onPlane) + { + var snapRec = Object.create(Snap2DRecord);//new Snap2DRecord(); + snapRec.init( elt ); + this._elementCache.push( snapRec ); + + if (!elt.elementModel) + { + NJUtils.makeElementModel2(elt); + } + elt.elementModel.isIn2DSnapCache = true; + } + else if (elt.elementModel) + elt.elementModel.isIn2DSnapCache = false; + } + + // TODO - Don't traverse components' children + if(elt.elementModel && elt.elementModel.isComponent) + { + return; + } + var n = elt.childElementCount; + if (n > 0) + { + for (var i=0; i 1) + { + var tmpArray = new Array; + var both; + for (var i=0; i 0) + this.setLastHit( hitRecArray[hitRecArray.length-1] ); + } + }, + + getPlaneToViewMat : + { + value : function() + { + var wasActive = this._dragPlaneActive; + if (!wasActive) this.activateDragPlane(); + var stage = this.getStage(); + var wp = workingPlane.slice(0); + var mat = viewUtils.getMatrixFromElement(stage); + var wpMat = drawUtils.getPlaneToWorldMatrix(wp, MathUtils.getPointOnPlane(wp)); + //var planeToViewMat = mat.multiply(wpMat); + var planeToViewMat = glmat4.multiply( mat, wpMat, []); + if (!wasActive) this.deactivateDragPlane(); + + return planeToViewMat; + } + }, + + snapAlign : { + value: function( scrPt, hitRecArray ) { + var didHit = false; + + if (!this._elementCache) return false; + var n = this._elementCache.length; + if (n > 0) + { + // project the screen point to the working plane + this.activateDragPlane(); + var gPt = scrPt.slice(0); + var stage = this.getStage(); + //var stageOffset = viewUtils.getElementOffset( stage ); + //MathUtils.makeDimension3( stageOffset ); + var currentWorkingPlane = workingPlane.slice(0); + var wp = currentWorkingPlane.slice(0); + var mat = viewUtils.getMatrixFromElement(stage); + wp = MathUtils.transformPlane(wp, mat); + var eyePt = viewUtils.getEyePoint(); + var vec = viewUtils.parentToChildVec(gPt, stage); + var projPt = MathUtils.vecIntersectPlane(eyePt, vec, wp); + var wpMat = drawUtils.getPlaneToWorldMatrix(currentWorkingPlane, MathUtils.getPointOnPlane(currentWorkingPlane)); + projPt[3] = 1.0; + //var planeToViewMat = mat.multiply(wpMat); + var planeToViewMat = glmat4.multiply( mat, wpMat, []); + //var viewToPlaneMat = planeToViewMat.inverse(); + var viewToPlaneMat = glmat4.inverse( planeToViewMat, [] ); + var planePt = projPt.slice(0); + planePt[3] = 1.0; + planePt = MathUtils.transformPoint( planePt, viewToPlaneMat ); + this.deactivateDragPlane(); + + var hitRec; + var nHits = hitRecArray.length; + var hr = Object.create(HitRecord);//new HitRecord(); + for (var i=0; i 0); + + if (nHits > 1) + { + var tmpArray = new Array; + for (var i=0; i dist) assocPt = ap2; + } + hitRec.setAssociatedScreenPoint( assocPt ); + } + } + + return hitRec; + } + }, + + coalesceSnapAlignHits : { + value: function( hitArray, planeToViewMat ) { + var nHits = hitArray.length; + if (nHits < 2) return; + + var hSnap, vSnap, hitRec; + for (var i=0; i 0) // don't snap to the root + { + // if the element is in the 2D cache snapping is done there + if (elt.elementModel && !elt.elementModel.isIn2DSnapCache) + { + var scrPt = viewUtils.parentToChild( parentPt, elt, false ); + hit = this.snapToElement( elt, scrPt, globalScrPt ); + if (hit) + { + //hitRecs.push( hit ); + if (!hit.checkType()) + { + console.log( "invalid hit record: " + hit.getTypeString() ); + hit.checkType() + } + else + hitRecs.push( hit ); + } + } + } + + // TODO - Don't traverse components' children + if(elt.elementModel && elt.elementModel.isComponent) + { + return; + } + // test the rest of the tree + var n = elt.childElementCount; + var eltPt = viewUtils.parentToChild( parentPt, elt, true ); + if (n > 0) + { + for (var i=0; i= 0)) + { + localPt = vecUtils.vecInterpolate( 3, bounds[iLast], bounds[i], t ); + hitType = hitRec.SNAP_TYPE_ELEMENT_EDGE; + globalPt = vecUtils.vecInterpolate( 3, pt0, pt1, t ); + break; + } + + // create a vector from pt0 to the screen point + var vec1 = vecUtils.vecSubtract(2, globalPt, pt0); + + // take the cross product of the 2 vectors. positive sign indicates the + // point is outside the bounds. + var cross = vec0[0]*vec1[1] - vec0[1]*vec1[0]; + if (((zNrm < 0) && (MathUtils.fpSign(cross) > 0)) || + ((zNrm > 0) && (MathUtils.fpSign(cross) < 0))) + { + hit = false; + break; + } + + // advance to the next edge + pt0 = pt1; + iLast = i; + } + + + if (hit && (hitType == hitRec.SNAP_TYPE_ELEMENT)) + { + // check the center point + var ctr2D = MathUtils.getCenterFromBounds( 2, bounds ); + var ctr3D = viewUtils.localToGlobal( ctr2D, elt ); + dist = vecUtils.vecDist( 2, globalPt, ctr3D ); + if ( dist <= this.ELEMENT_VERTEX_HIT_RAD) + { + MathUtils.makeDimension3( ctr2D ); + globalPt = ctr3D; + localPt = ctr2D; + hitType = hitRec.SNAP_TYPE_ELEMENT_CENTER; + } + } + + if (hit) + { + // if the interior of the element was hit, the local point is null at this point. + // Calculate the local point + var planeMat; + var mat = viewUtils.getMatrixFromElement( elt ); + var wp = Vector.create([0,0,1,0]); + wp = MathUtils.transformPlane( wp, mat ); + var wpMat = drawUtils.getPlaneToWorldMatrix(wp, MathUtils.getPointOnPlane(wp)); + //var wpMatInv = wpMat.inverse(); + var wpMatInv = glmat4.inverse(wpMat, []); + var recalcGlobalPt = false; + if (!localPt) + { + localPt = viewUtils.globalScreenToLocalWorld( globalPt, elt ); + if (!localPt) return null; + recalcGlobalPt = true; + } + else + { + MathUtils.makeDimension3( localPt ); + viewUtils.setViewportObj( elt ); + localPt = viewUtils.screenToView( localPt[0], localPt[1], localPt[2] ); + localPt = MathUtils.transformPoint( localPt, mat ); + } + + // transform the point from local world space to working plane space + localPt = MathUtils.transformPoint( localPt, wpMatInv ); + + // fill out the hit record + hitRec.setElt( elt ); + hitRec.setLocalPoint( localPt ); + hitRec.setPlaneMatrix( wpMat ); + hitRec.setScreenPoint(globalPt); + hitRec.setPlane(wp); + hitRec.setType( hitType ); + hitRec.setZIndex( viewUtils.getZIndex(elt) ); + if (recalcGlobalPt) + { + globalPt = hitRec.calculateScreenPoint(); + hitRec.setScreenPoint(globalPt); + } + } + else + hitRec = null; + } + catch(e) + { + //console.trace(); + console.log( "***** Exception in snapToElement: " + e + " *****" ); + } + + viewUtils.popViewportObj(); + + return hitRec; + } + }, + + isARectangle: + { + value: function( elt ) + { + var rtnVal = false; + var world = this.getGLWorld(elt); + if (world) + { + var obj = world.getGeomRoot(); + if (!obj.getChild() && !obj.getNext()) // just a single object + { + if (obj.geomType() == obj.GEOM_TYPE_RECTANGLE) + { + // FIXME - need to check that the rectangle fits the element bounds + //if ((obj.getWidth() == elt. + rtnVal = true; + } + } + } + + return rtnVal; + } + }, + + + snapToContainedElements : + { + value: function( hitRec, viewPt, targetScrPt ) + { + var rtnVal = false; + var elt = hitRec.getElement(); + if (elt) + { + if (elt.elementModel && elt.elementModel.shapeModel) + { + var world = elt.elementModel.shapeModel.GLWorld; + if ( world ) + { + // convert to GL coordinates + var glPt = this.globalScreenToWebGL( targetScrPt, elt ); + var eyePt = Vector.create( [0, 0, world.getViewDistance()] ); + var dir = vecUtils.vecSubtract(3, glPt, eyePt); + + // recursively go through the tree testing all objects + var root = world.getGeomRoot(); + rtnVal = this.hSnapToContainedElements( eyePt, dir, root, hitRec, targetScrPt ); + } + } + } + + return rtnVal; + } + }, + + hSnapToContainedElements : + { + value: function( eyePt, dir, glObj, hitRec, targetScrPt ) + { + if (!glObj) return false; + + var rtnVal = this.snapToContainedElement( eyePt, dir, glObj, hitRec, targetScrPt ); + + rtnVal |= this.hSnapToContainedElements( eyePt, dir, glObj.getChild(), hitRec, targetScrPt ); + rtnVal |= this.hSnapToContainedElements( eyePt, dir, glObj.getNext(), hitRec, targetScrPt ); + + return rtnVal; + } + }, + + snapToContainedElement : + { + value: function( eyePt, dir, glObj, hitRec, targetScrPt ) + { + var rtnVal = false; + var elt = hitRec.getElement(); + + var world = glObj.getWorld(); + switch (glObj.geomType()) + { + case glObj.GEOM_TYPE_RECTANGLE: + break; + + case glObj.GEOM_TYPE_CIRCLE: + { + var nearVrt = glObj.getNearVertex( eyePt, dir ); + if (nearVrt) + { + var viewPt = this.GLToView(nearVrt, world ); + var mat = viewUtils.getMatrixFromElement( elt ); + var worldPt = MathUtils.transformPoint( viewPt, mat ); + + viewUtils.pushViewportObj( elt ); + var scrPt = viewUtils.viewToScreen( worldPt ); + var offset = viewUtils.getElementOffset( elt ); + MathUtils.makeDimension3( offset ); + var parentPt = vecUtils.vecAdd(3, scrPt, offset ); + var globalPt = viewUtils.localToGlobal( parentPt, elt.parentElement ); + + var dist = vecUtils.vecDist(2, globalPt, targetScrPt ); + if (dist < this.ELEMENT_VERTEX_HIT_RAD) + { + // check if the distance is less than + // the distance on the current hit record + if (dist <= vecUtils.vecDist(2, targetScrPt, hitRec.getScreenPoint() )) + { + hitRec.setScreenPoint( globalPt ); + //var localMatInv = hitRec.getPlaneMatrix().inverse(); + var localMatInv = glmat4.inverse( hitRec.getPlaneMatrix(), []); + viewUtils.pushViewportObj( hitRec.getElement() ); + var localPt = viewUtils.screenToView( scrPt[0], scrPt[1], scrPt[2] ); + viewUtils.popViewportObj(); + localPt = MathUtils.transformPoint( localPt, localMatInv ); + hitRec.setLocalPoint( localPt ); + hitRec.setType( hitRec.SNAP_TYPE_CONTAINED_ELEMENT ); + + rtnVal = true; + } + } + } // if (nearVrt) + + if (!rtnVal) + { + var nearPt = glObj.getNearPoint( eyePt, dir ); + if (nearPt) + { + var viewPt = this.GLToView(nearPt, world ); + var mat = viewUtils.getMatrixFromElement( elt ); + var worldPt = MathUtils.transformPoint( viewPt, mat ); + + viewUtils.pushViewportObj( elt ); + var scrPt = viewUtils.viewToScreen( worldPt ); + var offset = viewUtils.getElementOffset( elt ); + MathUtils.makeDimension3( offset ); + var parentPt = vecUtils.vecAdd(3, scrPt, offset ); + var globalPt = viewUtils.localToGlobal( parentPt, elt.parentElement ); + + var dist = vecUtils.vecDist(2, globalPt, targetScrPt ); + if (dist < this.ELEMENT_EDGE_HIT_RAD) + { + // check if the distance is less than + // the distance on the current hit record + //var dist2 = vecUtils.vecDist(2, targetScrPt, hitRec.getScreenPoint() ); + //if (dist <= dist2+1 ) + { + hitRec.setScreenPoint( globalPt ); + //var localMatInv = hitRec.getPlaneMatrix().inverse(); + var localMatInv = glmat4.inverse( hitRec.getPlaneMatrix(), []); + viewUtils.pushViewportObj( hitRec.getElement() ); + var localPt = viewUtils.screenToView( scrPt[0], scrPt[1], scrPt[2] ); + viewUtils.popViewportObj(); + localPt = MathUtils.transformPoint( localPt, localMatInv ); + hitRec.setLocalPoint( localPt ); + hitRec.setType( hitRec.SNAP_TYPE_CONTAINED_ELEMENT ); + + rtnVal = true; + } + } + } + } // if (!rtnVal) + + if (!rtnVal && glObj.containsPoint( eyePt, dir )) + { + rtnVal = true; + } + } + break; + + case glObj.GEOM_TYPE_LINE: + case glObj.GEOM_TYPE_PATH: + // Snapping not implemented for these type, but don't throw an error... + break; + case glObj.GEOM_TYPE_CUBIC_BEZIER: + { + var nearVrt = glObj.getNearVertex( eyePt, dir ); + if (nearVrt) + { + var viewPt = this.GLToView(nearVrt, world ); + var mat = viewUtils.getMatrixFromElement( elt ); + var worldPt = MathUtils.transformPoint( viewPt, mat ); + + viewUtils.pushViewportObj( elt ); + var scrPt = viewUtils.viewToScreen( worldPt ); + var offset = viewUtils.getElementOffset( elt ); + MathUtils.makeDimension3( offset ); + var parentPt = vecUtils.vecAdd(3, scrPt, offset ); + var globalPt = viewUtils.localToGlobal( parentPt, elt.parentElement ); + + var dist = vecUtils.vecDist(2, globalPt, targetScrPt ); + if (dist < this.ELEMENT_VERTEX_HIT_RAD) + { + // check if the distance is less than + // the distance on the current hit record + if (dist <= vecUtils.vecDist(2, targetScrPt, hitRec.getScreenPoint() )) + { + hitRec.setScreenPoint( globalPt ); + //var localMatInv = hitRec.getPlaneMatrix().inverse(); + var localMatInv = glmat4.inverse( hitRec.getPlaneMatrix(), []); + viewUtils.pushViewportObj( hitRec.getElement() ); + var localPt = viewUtils.screenToView( scrPt[0], scrPt[1], scrPt[2] ); + viewUtils.popViewportObj(); + localPt = MathUtils.transformPoint( localPt, localMatInv ); + hitRec.setLocalPoint( localPt ); + hitRec.setType( hitRec.SNAP_TYPE_CONTAINED_ELEMENT ); + + rtnVal = true; + } + } + } // if (nearVrt) + + if (!rtnVal) + { + var nearPt = glObj.getNearPoint( eyePt, dir ); + if (nearPt) + { + var viewPt = this.GLToView(nearPt, world ); + var mat = viewUtils.getMatrixFromElement( elt ); + var worldPt = MathUtils.transformPoint( viewPt, mat ); + + viewUtils.pushViewportObj( elt ); + var scrPt = viewUtils.viewToScreen( worldPt ); + var offset = viewUtils.getElementOffset( elt ); + MathUtils.makeDimension3( offset ); + var parentPt = vecUtils.vecAdd(3, scrPt, offset ); + var globalPt = viewUtils.localToGlobal( parentPt, elt.parentElement ); + + var dist = vecUtils.vecDist(2, globalPt, targetScrPt ); + if (dist < this.ELEMENT_EDGE_HIT_RAD) + { + // check if the distance is less than + // the distance on the current hit record + //var dist2 = vecUtils.vecDist(2, targetScrPt, hitRec.getScreenPoint() ); + //if (dist <= dist2+1 ) + { + hitRec.setScreenPoint( globalPt ); + //var localMatInv = hitRec.getPlaneMatrix().inverse(); + var localMatInv = glmat4.inverse( hitRec.getPlaneMatrix(), []); + viewUtils.pushViewportObj( hitRec.getElement() ); + var localPt = viewUtils.screenToView( scrPt[0], scrPt[1], scrPt[2] ); + viewUtils.popViewportObj(); + localPt = MathUtils.transformPoint( localPt, localMatInv ); + hitRec.setLocalPoint( localPt ); + hitRec.setType( hitRec.SNAP_TYPE_CONTAINED_ELEMENT ); + + rtnVal = true; + } + } + } + } // if (!rtnVal) + + if (!rtnVal && glObj.containsPoint( eyePt, dir )) + { + rtnVal = true; + } + } + break; + default: + throw new Error( "invalid GL geometry type: " + glObj.geomType() ); + break; + } + + return rtnVal; + } + }, + + getGLWorld : { + value: function( elt ) { + var world; + if (elt.elementModel && elt.elementModel.shapeModel) + world = elt.elementModel.shapeModel.GLWorld; + + return world; + } + }, + + globalScreenToWebGL : + { + value: function( targetScrPt, elt ) + { + var glPt; + if (elt.elementModel && elt.elementModel.shapeModel) + { + var world = elt.elementModel.shapeModel.GLWorld; + if ( world ) + { + // create a matrix going all the way from GL space to screen space + var o2w = viewUtils.getLocalToGlobalMatrix( elt ); + //var w2o = o2w.inverse(); + var w2o = glmat4.inverse(o2w, []); + viewUtils.pushViewportObj( elt ); + var cop = viewUtils.getCenterOfProjection(); + viewUtils.popViewportObj(); + var s2v = Matrix.Translation(Vector.create([-cop[0], -cop[1], 0])); + var vToNDC = Matrix.I(4); + vToNDC[0] = 1.0/(0.5*world.getViewportWidth()); + vToNDC[5] = 1.0/(0.5*world.getViewportHeight()); + //var sToNDC = vToNDC.multiply( s2v ); + var sToNDC = glmat4.multiply( vToNDC, s2v, [] ); + + // add the projection matrix + var projMat = world.makePerspectiveMatrix(); + //var projInv = projMat.inverse(); + var projInv = glmat4.inverse(projMat, []); + var camInv = world.getCameraMatInverse(); + //var glToNDC = projMat.multiply( camInv ); + var glToNDC = glmat4.multiply( projMat, camInv, [] ); + //var ndcToGL = glToNDC.inverse(); + var ndcToGL = glmat4.inverse(glToNDC, []); + //var sToGL = projInv.multiply( sToNDC ); + var sToGL = glmat4.multiply( projInv, sToNDC, [] ); + + // add the camera matrix to produce the matrix going from + // object local screen space to GL space + var camMat = world.getCameraMat(); + //sToGL = camMat.multiply( sToGL ); + sToGL = glmat4.multiply( camMat, sToGL, [] ); + + // transform the input point in screen space to object space + var tmpInPt = targetScrPt.slice(0); + tmpInPt = MathUtils.makeDimension4( tmpInPt ); + tmpInPt[2] = 0.0; // z == 0 + var tmpPt1 = MathUtils.transformHomogeneousPoint( tmpInPt, w2o); + tmpPt1 = MathUtils.applyHomogeneousCoordinate( tmpPt1 ); + + // get a second point in object space starting from the input point plus an (arbitrary) z offset + tmpInPt[2] = 100.0; + var tmpPt2 = MathUtils.transformHomogeneousPoint( tmpInPt, w2o); + tmpPt2 = MathUtils.applyHomogeneousCoordinate( tmpPt2 ); + + // project the 2 object space points onto the original plane of the object + var tmpPt3 = MathUtils.vecIntersectPlane( tmpPt1, vecUtils.vecSubtract(3, tmpPt2, tmpPt1), Vector.create([0,0,1,0]) ); + //console.log( "object space pt: " + tmpPt3 ); + + // get the z value in NDC space of the projection plane + var ndcPt = MathUtils.transformHomogeneousPoint( Vector.create( [0, 0, 0] ), glToNDC ); + ndcPt = MathUtils.applyHomogeneousCoordinate( ndcPt ); + var zNDC = ndcPt[2]; + + // transform the ndc point into gl space + ndcPt = tmpPt3.slice(0); + ndcPt[2] = zNDC; + glPt = MathUtils.transformHomogeneousPoint( ndcPt, sToGL ); + glPt = MathUtils.applyHomogeneousCoordinate( glPt ); + glPt[1] = -glPt[1]; + + //console.log( "object space pt: " + tmpPt3.elements + ", GL pt: " + glPt.elements ); + } + } + return glPt; + } + }, + + GLToView : + { + value: function( glPt, world ) + { + var projMat = world.makePerspectiveMatrix(); + //var mat = projMat.multiply( world.getCameraMatInverse() ); + var mat = glmat4.multiply( projMat, world.getCameraMatInverse(), [] ); + var viewPt = MathUtils.transformHomogeneousPoint( glPt, mat ); + viewPt = MathUtils.applyHomogeneousCoordinate( viewPt ); + viewPt[0] *= 0.5*world.getViewportWidth(); + viewPt[1] *= -0.5*world.getViewportHeight(); + viewPt[2] = 0.0; + + return viewPt; + } + }, + + + addToAvoidList : { + value: function( elt ) { + this._avoidList.push( elt ); + } + }, + + clearAvoidList : { + value: function() { + this._avoidList = new Array; + } + }, + + isAvoidedElement : { + value: function( elt ) { + var n = this._avoidList.length; + for (var i=0; i y) { + if (x > z) { + plane[0] = 1; + plane[3] = this.getStageWidth() / 2.0; + if (dir[0] > 0) plane[3] = -plane[3]; + change = !drawUtils.drawYZ; + drawUtils.drawXY = drawUtils.drawXZ = false; + if (drawingGrid) drawUtils.drawYZ = true; + id = "showSide"; + } + else { + plane[2] = 1; + change = !drawUtils.drawXY; + drawUtils.drawYZ = drawUtils.drawXZ = false; + if (drawingGrid) drawUtils.drawXY = true; + id = "showFront"; + } + } + else { + if (y > z) { + plane[1] = 1; + plane[3] = this.getStageHeight() / 2.0; + if (dir[1] > 0) plane[3] = -plane[3]; + change = !drawUtils.drawXZ; + drawUtils.drawXY = drawUtils.drawYZ = false; + if (drawingGrid) drawUtils.drawXZ = true; + id = "showTop"; + } + else { + plane[2] = 1; + change = !drawUtils.drawXY; + drawUtils.drawYZ = drawUtils.drawXZ = false; + if (drawingGrid) drawUtils.drawXY = true; + id = "showFront"; + } + } + + if (change) { + //console.log( "change working plane: " + drawUtils.drawXY + ", " + drawUtils.drawYZ + ", " + drawUtils.drawXZ ); + workingPlane = plane; + drawUtils.setWorkingPlane(plane); + //window.stageManager.drawSelectionRec(true); + //window.stageManager.layoutModule.redrawDocument(); + this.application.ninja.stage.updateStage = true; + } + } + } + } + }, + + sortHitRecords : { + value: function( hitRecs ) { + if (!hitRecs) return; + var nHits = hitRecs.length; + if (nHits < 2) + { + // if (nHits > 0) + // console.log( "single hit, type: " + hitRecs[0].getTypeString() + ", screen point z: " + hitRecs[0].getScreenPoint()[2] ); + return; + } + + // find the hit record with the largest Z value in global screen space + for (var i=0; i pi[2]) + { + var tmp = hitRecs[i]; + hitRecs[i] = null; + hi = null; + hitRecs[i] = hj; + hitRecs[j] = tmp; + hi = hj; + } + } + } + + // stage hits are lowest priority. Move them to the end of the array + var nm1 = nHits - 1; + var flag = false; + for (var i=nHits-1; i>=0; i--) + { + var pi = hitRecs[i]; + + // we favor snapping to object vertices. + // if we find one, put it first and return + if (pi.getType() == pi.SNAP_TYPE_ELEMENT_VERTEX) + { + if (i != 0) + { + var tmp = hitRecs[0]; + hitRecs[0] = pi; + hitRecs[i] = tmp; + } + return; + } + + if (pi.getType() == pi.SNAP_TYPE_STAGE) + { + if (flag) // don't start moving until we find something other than a stage snap + { + var pn = hitRecs[nm1]; + hitRecs[nm1] = pi; + hitRecs[i] = pn; + flag = true; + } + nm1--; + } + else + flag = true; + } + + var mergedSnap = this.mergeHitRecords( hitRecs ); + if (mergedSnap) + { + while (hitRecs.length > 0) hitRecs.pop(); + hitRecs.push( mergedSnap ); + //console.log( "merged snaps" ); + } + + //this.checkZValues( hitRecs ); + } + }, + + mergeHitRecords : + { + value: function( hitRecs ) + { + var nHits = hitRecs.length; + //console.log( "merging " + nHits + " hits" ); + var i = 0; + for (i=0; i