/* <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> */ var VecUtils = require("js/helper-classes/3D/vec-utils").VecUtils; var CanvasController = require("js/controllers/elements/canvas-controller").CanvasController; var GeomObj = require("js/lib/geom/geom-obj").GeomObj; var AnchorPoint = require("js/lib/geom/anchor-point").AnchorPoint; var MaterialsModel = require("js/models/materials-model").MaterialsModel; /////////////////////////////////////////////////////////////////////// // Class GLSubpath // representation a sequence of cubic bezier curves. // Derived from class GeomObj /////////////////////////////////////////////////////////////////////// var GLSubpath = function GLSubpath() { /////////////////////////////////////////////////// // Instance variables /////////////////////////////////////////////////// // NOTE: // This class contains functionality to store piecewise cubic bezier paths. // The coordinates of the paths are always in local, canvas space. // That is, the Z coordinate can be ignored (for now), and the paths are essentially in 2D. // All coordinates of the '_Samples' should lie within [0,0] and [width, height], // where width and height refer to the dimensions of the canvas for this path. // Whenever the the canvas dimensions change, the coordinates of the anchor points // and _Samples must be re-computed. this._Anchors = []; this._BBoxMin = [0, 0, 0]; this._BBoxMax = [0, 0, 0]; this._canvasCenterLocalCoord = [0,0,0]; this._isClosed = false; this._Samples = []; //polyline representation of this curve in canvas space this._sampleParam = []; //parametric distance of samples, within [0, N], where N is # of Bezier curves (=# of anchor points if closed, =#anchor pts -1 if open) this._anchorSampleIndex = []; //index within _Samples corresponding to anchor points //initially set the _dirty bit so we will re-construct _Anchors and _Samples this._dirty = true; //stroke information this._strokeWidth = 1.0; this._strokeColor = [0.4, 0.4, 0.4, 1.0]; this._fillColor = [1.0, 1.0, 1.0, 0.0]; this._DISPLAY_ANCHOR_RADIUS = 5; //drawing context this._world = null; this._canvas = null; //todo this might be unnecessary (but faster) since we can get it from the world //tool that owns this subpath this._drawingTool = null; //used to query what the user selected, OR-able for future extensions this.SEL_NONE = 0; //nothing was selected this.SEL_ANCHOR = 1; //anchor point was selected this.SEL_PREV = 2; //previous handle of anchor point was selected this.SEL_NEXT = 4; //next handle of anchor point was selected this.SEL_PATH = 8; //the path itself was selected this._selectMode = this.SEL_NONE; this._selectedAnchorIndex = -1; this._SAMPLING_EPSILON = 0.5; //epsilon used for sampling the curve }; //function GLSubpath ...class definition GLSubpath.prototype = Object.create(GeomObj, {}); //buildBuffers GLSubpath.prototype.buildBuffers = function () { //no need to do anything for now (no WebGL) }; //buildColor returns the fillStyle or strokeStyle for the Canvas 2D context GLSubpath.prototype.buildColor = function(ctx, //the 2D rendering context (for creating gradients if necessary) ipColor, //color string, also encodes whether there's a gradient and of what type w, //width of the region of color h, //height of the region of color lw) //linewidth (i.e. stroke width/size) { if (ipColor.gradientMode){ var position, gradient, cs, inset; //vars used in gradient calculations inset = Math.ceil( lw ) - 0.5; inset=0; if(ipColor.gradientMode === "radial") { var ww = w - 2*lw, hh = h - 2*lw; gradient = ctx.createRadialGradient(w/2, h/2, 0, w/2, h/2, Math.max(ww, hh)/2); } else { gradient = ctx.createLinearGradient(inset, h/2, w-inset, h/2); } var colors = ipColor.color; var len = colors.length; for(n=0; n<len; n++) { position = colors[n].position/100; cs = colors[n].value; gradient.addColorStop(position, "rgba(" + cs.r + "," + cs.g + "," + cs.b + "," + cs.a + ")"); } return gradient; } else { var c = "rgba(" + 255*ipColor[0] + "," + 255*ipColor[1] + "," + 255*ipColor[2] + "," + ipColor[3] + ")"; return c; } }; //render // specify how to render the subpath in Canvas2D GLSubpath.prototype.render = function () { // get the world var world = this.getWorld(); if (!world) throw( "null world in subpath render" ); if (!this._canvas){ //set the canvas by querying the world this._canvas = this.getWorld().getCanvas(); } // get the context var ctx = world.get2DContext(); if (!ctx) throw ("null context in subpath render"); var numAnchors = this.getNumAnchors(); if (numAnchors === 0) { return; //nothing to do for empty paths } this.createSamples(false); //dirty bit checked in this function...will generate a polyline representation var numPoints = this._Samples.length; if (numPoints === 0){ return; //nothing to do for empty paths } //figure the size of the area we will draw into var bboxWidth=0, bboxHeight=0; bboxWidth = this._BBoxMax[0] - this._BBoxMin[0]; bboxHeight = this._BBoxMax[1] - this._BBoxMin[1]; ctx.save(); ctx.clearRect(0, 0, bboxWidth, bboxHeight); ctx.lineWidth = this._strokeWidth; ctx.strokeStyle = "black"; //vars used for the gradient computation in buildColor var w = world.getViewportWidth(), h = world.getViewportHeight(); if (this._strokeColor) { ctx.strokeStyle = this.buildColor(ctx, this._strokeColor, w,h, this._strokeWidth); } ctx.fillStyle = "white"; if (this._fillColor){ ctx.fillStyle = this.buildColor(ctx, this._fillColor, w, h, this._strokeWidth); } var lineCap = ['butt','round','square']; ctx.lineCap = lineCap[1]; var lineJoin = ['round','bevel','miter']; ctx.lineJoin = lineJoin[0]; //commenting this out for now because of Chrome bug where coincident endpoints of bezier curve cause the curve to not be rendered /* ctx.beginPath(); var prevAnchor = this.getAnchor(0); ctx.moveTo(prevAnchor.getPosX(),prevAnchor.getPosY()); for (var i = 1; i < numAnchors; i++) { var currAnchor = this.getAnchor(i); ctx.bezierCurveTo(prevAnchor.getNextX(),prevAnchor.getNextY(), currAnchor.getPrevX(), currAnchor.getPrevY(), currAnchor.getPosX(), currAnchor.getPosY()); prevAnchor = currAnchor; } if (this._isClosed === true) { var currAnchor = this.getAnchor(0); ctx.bezierCurveTo(prevAnchor.getNextX(),prevAnchor.getNextY(), currAnchor.getPrevX(), currAnchor.getPrevY(), currAnchor.getPosX(), currAnchor.getPosY()); prevAnchor = currAnchor; } ctx.fill(); ctx.stroke(); */ ctx.beginPath(); ctx.moveTo(this._Samples[0][0],this._Samples[0][1]); for (var i=0;i<numPoints;i++){ ctx.lineTo(this._Samples[i][0],this._Samples[i][1]); } if (this._isClosed === true) { ctx.lineTo(this._Samples[0][0],this._Samples[0][1]); } ctx.fill(); ctx.stroke(); ctx.restore(); }; //render() ///////////////////////////////////////////////////////// // Property Accessors/Setters ///////////////////////////////////////////////////////// GLSubpath.prototype.geomType = function () { return this.GEOM_TYPE_CUBIC_BEZIER; }; GLSubpath.prototype.setWidth = function (newW) { var strokeWidth = this._strokeWidth; var halfStrokeWidth = strokeWidth*0.5; var minWidth = 1+strokeWidth; if (newW<minWidth) { newW=minWidth; } //scale the contents of this subpath to lie within this width //determine the scale factor by comparing with the old width var oldCanvasWidth = this._BBoxMax[0]-this._BBoxMin[0]; if (oldCanvasWidth<minWidth) { oldCanvasWidth=minWidth; } var scaleX = (newW-strokeWidth)/(oldCanvasWidth-strokeWidth); if (scaleX===1) { return; //no need to do anything } //scale the anchor point positions such that the width of the bbox is the newW var origX = halfStrokeWidth;//this is the left edge //this._BBoxMin[0]; //this should always be zero since we only deal with local coordinates var numAnchors = this._Anchors.length; for (var i=0;i<numAnchors;i++){ //compute the distance from the bboxMin var oldW = this._Anchors[i].getPosX() - origX; var prevW = this._Anchors[i].getPrevX() - origX; var nextW = this._Anchors[i].getNextX() - origX; this._Anchors[i].setPos(origX + oldW*scaleX,this._Anchors[i].getPosY(),this._Anchors[i].getPosZ()); this._Anchors[i].setPrevPos(origX + prevW*scaleX,this._Anchors[i].getPrevY(),this._Anchors[i].getPrevZ()); this._Anchors[i].setNextPos(origX + nextW*scaleX,this._Anchors[i].getNextY(),this._Anchors[i].getNextZ()); } this.makeDirty(); this.computeBoundingBox(true, false); }; GLSubpath.prototype.setHeight = function (newH) { var strokeWidth = this._strokeWidth; var halfStrokeWidth = strokeWidth*0.5; var minHeight = 1+strokeWidth; if (newH<minHeight) { newH=minHeight; //clamp minimum width to 1 } //scale the contents of this subpath to lie within this height //determine the scale factor by comparing with the old height var oldHeight = this._BBoxMax[1]-this._BBoxMin[1]; if (oldHeight<minHeight){ oldHeight=minHeight; } var scaleY = (newH-strokeWidth)/(oldHeight-strokeWidth); if (scaleY===1){ return; //no need to do anything } //scale the anchor point positions such that the height of the bbox is the newH var origY = halfStrokeWidth;// this._BBoxMin[1];//this is the top edge var numAnchors = this._Anchors.length; for (var i=0;i<numAnchors;i++){ //compute the distance from the bboxMin var oldW = this._Anchors[i].getPosY() - origY; var prevW = this._Anchors[i].getPrevY() - origY; var nextW = this._Anchors[i].getNextY() - origY; this._Anchors[i].setPos(this._Anchors[i].getPosX(), origY + oldW*scaleY,this._Anchors[i].getPosZ()); this._Anchors[i].setPrevPos(this._Anchors[i].getPrevX(), origY + prevW*scaleY,this._Anchors[i].getPrevZ()); this._Anchors[i].setNextPos(this._Anchors[i].getNextX(), origY + nextW*scaleY,this._Anchors[i].getNextZ()); } this.makeDirty(); this.computeBoundingBox(true, false); }; GLSubpath.prototype.setWorld = function (world) { this._world = world; }; GLSubpath.prototype.setCanvas = function (canvas){ this._canvas = canvas; }; GLSubpath.prototype.getWorld = function () { return this._world; }; GLSubpath.prototype.getCanvas = function() { return this._canvas; }; GLSubpath.prototype.makeDirty = function () { this._dirty = true; }; GLSubpath.prototype.setDrawingTool = function (tool) { this._drawingTool = tool; }; GLSubpath.prototype.getDrawingTool = function () { return this._drawingTool; }; GLSubpath.prototype.getIsClosed = function () { return this._isClosed; }; GLSubpath.prototype.setIsClosed = function (isClosed) { if (this._isClosed !== isClosed) { this._isClosed = isClosed; this.makeDirty(); } }; GLSubpath.prototype.setCanvasCenterLocalCoord = function(center){ this._canvasCenterLocalCoord = center; }; GLSubpath.prototype.getCanvasCenterLocalCoord = function(){ return [this._canvasCenterLocalCoord[0],this._canvasCenterLocalCoord[1],this._canvasCenterLocalCoord[2]]; }; GLSubpath.prototype.getNumAnchors = function () { return this._Anchors.length; }; GLSubpath.prototype.getAnchor = function (index) { return this._Anchors[index]; }; GLSubpath.prototype.addAnchor = function (anchorPt) { this._Anchors.push(anchorPt); this._selectedAnchorIndex = this._Anchors.length-1; this.makeDirty(); }; GLSubpath.prototype.insertAnchor = function(anchorPt, index){ this._Anchors.splice(index, 0, anchorPt); this.makeDirty(); }; //remove and return anchor at specified index, return null on error GLSubpath.prototype.removeAnchor = function (index) { var retAnchor = null; if (index < this._Anchors.length) { //if this is a closed path and we've removed either endpoint, we simply mark it as open if (this._isClosed && (index===this._Anchors.length-1 || index===0)){ this.setIsClosed(false); } else { retAnchor = this._Anchors.splice(index, 1); } this.makeDirty(); } //deselect any selected anchor point this.deselectAnchorPoint(); return retAnchor; }; GLSubpath.prototype.deselectAnchorPoint = function(){ this._selectedAnchorIndex = -1; }; GLSubpath.prototype.reversePath = function() { var revAnchors = []; var numAnchors = this._Anchors.length; var lastIndex = numAnchors-1; if (lastIndex<0){ return; //cannot reverse empty path } for (var i=lastIndex;i>=0;i--) { var newAnchor = new AnchorPoint(); var oldAnchor = this._Anchors[i]; newAnchor.setPos(oldAnchor.getPosX(),oldAnchor.getPosY(),oldAnchor.getPosZ()); newAnchor.setPrevPos(oldAnchor.getNextX(),oldAnchor.getNextY(),oldAnchor.getNextZ()); newAnchor.setNextPos(oldAnchor.getPrevX(),oldAnchor.getPrevY(),oldAnchor.getPrevZ()); revAnchors.push(newAnchor); } if (this._selectedAnchorIndex >= 0){ this._selectedAnchorIndex = (numAnchors-1) - this._selectedAnchorIndex; } this._Anchors = revAnchors; this.makeDirty(); }; //remove all the anchor points GLSubpath.prototype.clearAllAnchors = function () { this._Anchors = []; this.deselectAnchorPoint(); this.setIsClosed(false); this.makeDirty(); }; GLSubpath.prototype.insertAnchorAtParameter = function(index, param) { if (index+1 >= this._Anchors.length && !this._isClosed) { return; } //insert an anchor after the specified index using the parameter, using De Casteljau subdivision var nextIndex = (index+1)%this._Anchors.length; //build the De Casteljau points var P0P1 = VecUtils.vecInterpolate(3, this._Anchors[index].getPos(), this._Anchors[index].getNext(), param); var P1P2 = VecUtils.vecInterpolate(3, this._Anchors[index].getNext(), this._Anchors[nextIndex].getPrev(), param); var P2P3 = VecUtils.vecInterpolate(3, this._Anchors[nextIndex].getPrev(), this._Anchors[nextIndex].getPos(), param); var P0P1P2 = VecUtils.vecInterpolate(3, P0P1, P1P2, param); var P1P2P3 = VecUtils.vecInterpolate(3, P1P2, P2P3, param); var anchorPos = VecUtils.vecInterpolate(3, P0P1P2, P1P2P3, param); //update the next of the anchor at index and prev of anchor at nextIndex var isPrevCoincident = false; var isNextCoincident = false; if (VecUtils.vecDist( 3, P0P1, this._Anchors[index].getNext()) < this._SAMPLING_EPSILON) { //no change to the next point isPrevCoincident = true; } else { this._Anchors[index].setNextPos(P0P1[0], P0P1[1], P0P1[2]); } if (VecUtils.vecDist( 3, P2P3, this._Anchors[nextIndex].getPrev()) < this._SAMPLING_EPSILON) { //no change to the prev point isNextCoincident = true; } else { this._Anchors[nextIndex].setPrevPos(P2P3[0], P2P3[1], P2P3[2]); } //create a new anchor point var newAnchor = new AnchorPoint(); if (isPrevCoincident && isNextCoincident){ anchorPos[0]=P1P2[0];anchorPos[1]=P1P2[1];anchorPos[2]=P1P2[2]; newAnchor.setPos(anchorPos[0],anchorPos[1],anchorPos[2]); newAnchor.setPrevPos(anchorPos[0],anchorPos[1],anchorPos[2]); newAnchor.setNextPos(anchorPos[0],anchorPos[1],anchorPos[2]); } else { newAnchor.setPrevPos(P0P1P2[0], P0P1P2[1], P0P1P2[2]); newAnchor.setNextPos(P1P2P3[0], P1P2P3[1], P1P2P3[2]); newAnchor.setPos(anchorPos[0], anchorPos[1], anchorPos[2]); } //insert the new anchor point at the correct index and set it as the selected anchor this._Anchors.splice(nextIndex, 0, newAnchor); this._selectedAnchorIndex = nextIndex; this.makeDirty(); }; GLSubpath.prototype.isWithinPathBBox = function(x,y,z) { if (this._BBoxMin[0]>x || this._BBoxMin[1]>y || this._BBoxMin[2]>z){ return false; } if (this._BBoxMax[0]<x || this._BBoxMax[1]<y || this._BBoxMax[2]<z){ return false; } return true; } GLSubpath.prototype._checkIntersectionWithSamples = function(startIndex, endIndex, point, radius){ //check whether the point is within the radius distance from the curve represented as a polyline in _Samples //return the parametric distance along the curve if there is an intersection, else return null //will assume that the BBox test is performed outside this function if (endIndex<startIndex){ //go from startIndex to the end of the samples endIndex = this._Samples.length-1; } var retParam = null; for (var i=startIndex; i<endIndex; i++){ var seg0 = this._Samples[i].slice(0); var j=i+1; var seg1 = this._Samples[j].slice(0); var distToSegment = MathUtils.distPointToSegment(point, seg0, seg1); if (distToSegment<=radius){ var paramDistance = MathUtils.paramPointProjectionOnSegment(point, seg0, seg1); //TODO Optimize! this function was called in distPointToSegment above retParam = this._sampleParam[i] + (this._sampleParam[j] - this._sampleParam[i])*paramDistance; break; } } return retParam; }; GLSubpath.prototype._checkIntersection = function(controlPts, beginParam, endParam, point, radius) { //check whether the point is within radius distance from the curve // if there is an intersection, return the parameter value (between beginParam and endParam) of the intersection point, else return null var bboxMin = [Infinity, Infinity, Infinity]; var bboxMax = [-Infinity,-Infinity,-Infinity]; for (var i=0;i<controlPts.length;i++) { for (var d=0;d<3;d++){ if (controlPts[i][d] < bboxMin[d]){ bboxMin[d] = controlPts[i][d]; } if (controlPts[i][d] > bboxMax[d]){ bboxMax[d] = controlPts[i][d]; } } } //check whether the bbox of the control points contains the point within the specified radius for (var d=0;d<3;d++){ if (point[d] < (bboxMin[d]-radius)){ return null; } if (point[d] > (bboxMax[d]+radius)){ return null; } } //check if the curve is already flat, and if so, check the distance from the segment C0C3 to the point //measure distance of C1 and C2 to segment C0-C3 var distC1 = MathUtils.distPointToSegment(controlPts[1], controlPts[0], controlPts[3]); var distC2 = MathUtils.distPointToSegment(controlPts[2], controlPts[0], controlPts[3]); var maxDist = Math.max(distC1, distC2); var threshold = this._SAMPLING_EPSILON; //this should be set outside this function //TODO if (maxDist < threshold) { //if the curve is flat var distP = MathUtils.distPointToSegment(point, controlPts[0], controlPts[3]); //TODO we may need to neglect cases where the non-perpendicular distance is used... if (distP>radius) { return null; } else { var param = MathUtils.paramPointProjectionOnSegment(point, controlPts[0], controlPts[3]); //TODO this function is already called in distPointToSegment...optimize by removing redundant call //var param = VecUtils.vecDist(3, point, controlPts[0])/VecUtils.vecDist(3, controlPts[3], controlPts[0]); if (param<0) param=0; if (param>1) param=1; return beginParam + (endParam-beginParam)*param; } } //subdivide this curve using De Casteljau interpolation var C0_ = VecUtils.vecInterpolate(3, controlPts[0], controlPts[1], 0.5); var C1_ = VecUtils.vecInterpolate(3, controlPts[1], controlPts[2], 0.5); var C2_ = VecUtils.vecInterpolate(3, controlPts[2], controlPts[3], 0.5); var C0__ = VecUtils.vecInterpolate(3, C0_, C1_, 0.5); var C1__ = VecUtils.vecInterpolate(3, C1_, C2_, 0.5); var C0___ = VecUtils.vecInterpolate(3, C0__, C1__, 0.5); //recursively sample the first half of the curve var midParam = (endParam+beginParam)*0.5; var param1 = this._checkIntersection([controlPts[0],C0_,C0__,C0___], beginParam, midParam, point, radius); if (param1!==null){ return param1; } //recursively sample the second half of the curve var param2 = this._checkIntersection([C0___,C1__,C2_,controlPts[3]], midParam, endParam, point, radius); if (param2!==null){ return param2; } //no intersection, so return null return null; }; //whether the point lies within the bbox given by the four control points GLSubpath.prototype._isWithinGivenBoundingBox = function(point, ctrlPts, radius) { var bboxMin = [Infinity, Infinity, Infinity]; var bboxMax = [-Infinity,-Infinity,-Infinity]; for (var i=0;i<ctrlPts.length;i++) { for (var d=0;d<3;d++){ if (ctrlPts[i][d] < bboxMin[d]){ bboxMin[d] = ctrlPts[i][d]; } if (ctrlPts[i][d] > bboxMax[d]){ bboxMax[d] = ctrlPts[i][d]; } } } //check whether the bbox of the control points contains the point within the specified radius for (var d=0;d<3;d++){ if (point[d] < (bboxMin[d]-radius)){ return false; } if (point[d] > (bboxMax[d]+radius)){ return false; } } return true; }; GLSubpath.prototype._checkAnchorIntersection = function(pickX, pickY, pickZ, radSq, anchorIndex, minDistance) { if ( anchorIndex >= this._Anchors.length) { return this.SEL_NONE; } var distSq = this._Anchors[anchorIndex].getDistanceSq(pickX, pickY, pickZ); //check the anchor point if (distSq < radSq && distSq<minDistance) { return this.SEL_ANCHOR; } //check the prev. and next of the selected anchor point distSq = this._Anchors[anchorIndex].getPrevDistanceSq(pickX, pickY, pickZ); if (distSq<radSq && distSq<minDistance){ return this.SEL_PREV; } distSq = this._Anchors[anchorIndex].getNextDistanceSq(pickX, pickY, pickZ); if (distSq<radSq && distSq<minDistance){ return this.SEL_NEXT; } return this.SEL_NONE; }; GLSubpath.prototype.pickAnchor = function (pickX, pickY, pickZ, radius) { var numAnchors = this._Anchors.length; var selAnchorIndex = -1; var retCode = this.SEL_NONE; var minDistance = Infinity; var radSq = radius * radius; //check if the clicked location is close to the currently selected anchor position if (this._selectedAnchorIndex>=0 && this._selectedAnchorIndex<this._Anchors.length){ retCode = this._checkAnchorIntersection(pickX, pickY, pickZ, radSq, this._selectedAnchorIndex, minDistance); if (retCode!==this.SEL_NONE){ return [this._selectedAnchorIndex, retCode]; } } //now check if the click location is close to any anchor position for (var i = 0; i < numAnchors; i++) { retCode = this._checkAnchorIntersection(pickX, pickY, pickZ, radSq, i, minDistance); if (retCode===this.SEL_ANCHOR){ //we only care if the anchor was hit for the non-selected anchors (not the prev. or next handles) selAnchorIndex=i; break; } }//for every anchor i return [selAnchorIndex, retCode]; }; GLSubpath.prototype.pickPath = function (pickX, pickY, pickZ, radius, testOnly) { var numAnchors = this._Anchors.length; var selAnchorIndex = -1; var retParam = null; var retCode = this.SEL_NONE; //check if the location is close to any of the anchors var anchorAndRetCode = this.pickAnchor(pickX, pickY, pickZ, radius); selAnchorIndex = anchorAndRetCode[0]; retCode = anchorAndRetCode[1]; if (retCode!== this.SEL_NONE){ retCode = retCode | this.SEL_PATH; //ensure that path is also selected if anything else is selected } //if the location is not close any of the anchors, check if it is close to the curve itself if (selAnchorIndex===-1) { //first check if the input location is within the bounding box if (this.isWithinPathBBox(pickX,pickY,pickZ)){ var numSegments = this._isClosed ? numAnchors : numAnchors-1; for (var i = 0; i < numSegments; i++) { var nextIndex = (i+1)%numAnchors; //check if the point is close to the bezier segment between anchor i and anchor nextIndex var controlPoints = [[this._Anchors[i].getPosX(),this._Anchors[i].getPosY(),this._Anchors[i].getPosZ()], [this._Anchors[i].getNextX(),this._Anchors[i].getNextY(),this._Anchors[i].getNextZ()], [this._Anchors[nextIndex].getPrevX(),this._Anchors[nextIndex].getPrevY(),this._Anchors[nextIndex].getPrevZ()], [this._Anchors[nextIndex].getPosX(),this._Anchors[nextIndex].getPosY(),this._Anchors[nextIndex].getPosZ()]]; var point = [pickX, pickY, pickZ]; if (this._isWithinGivenBoundingBox(point, controlPoints, radius)) { //var intersectParam = this._checkIntersection(controlPoints, 0.0, 1.0, point, radius); var intersectParam = this._checkIntersectionWithSamples(this._anchorSampleIndex[i], this._anchorSampleIndex[nextIndex], point, radius); if (intersectParam!==null){ selAnchorIndex=i; retCode = retCode | this.SEL_PATH; retParam = intersectParam-i; //make the retParam go from 0 to 1 break; } } }//for every anchor i }//if is within bbox } if (!testOnly){ this._selectMode = retCode; this._selectedAnchorIndex = selAnchorIndex; } return [selAnchorIndex,retParam, retCode]; }; //GLSubpath.pickPath function GLSubpath.prototype.getSelectedAnchorIndex = function () { return this._selectedAnchorIndex; }; GLSubpath.prototype.getSelectedMode = function () { return this._selectMode; }; GLSubpath.prototype.getNumPoints = function () { return this._Samples.length; }; GLSubpath.prototype.getBBoxMin = function () { return this._BBoxMin; }; GLSubpath.prototype.getBBoxMax = function () { return this._BBoxMax; }; GLSubpath.prototype.getStrokeWidth = function () { return this._strokeWidth; }; GLSubpath.prototype.setStrokeWidth = function (w) { var diffStrokeWidth = w-this._strokeWidth;//if positive, then stroke width grew, else shrunk if (diffStrokeWidth === 0){ return;//nothing to do } //only allow to increase the stroke width with even increments (this avoids floating point round-off problems) diffStrokeWidth = Math.round(diffStrokeWidth); if (diffStrokeWidth%2){ diffStrokeWidth+=1; } //update the stroke width by the delta this._strokeWidth += diffStrokeWidth; //if we have no path yet, return if (this.getNumAnchors()<2 || this._canvas===null){ return; } if (this._dirty){ this.createSamples(false); //this will also update the bounding box } else{ this.computeBoundingBox(true,false); } this.offsetPerBBoxMin(); //this will shift the local coordinates such that the bbox min point is at (0,0) //figure out the adjustment to the canvas position and size var delta = Math.round(diffStrokeWidth*0.5); //update the canvas center (it's simply the center of the new bbox in this case) this._canvasCenterLocalCoord = [0.5*(this._BBoxMax[0]+this._BBoxMin[0]),0.5*(this._BBoxMax[1]+this._BBoxMin[1]),0.5*(this._BBoxMax[2]+this._BBoxMin[2])]; //update the width, height, left and top var ElementMediator = require("js/mediators/element-mediator").ElementMediator; var penCanvasCurrentWidth = parseInt(ElementMediator.getProperty(this._canvas, "width")); var penCanvasNewWidth = penCanvasCurrentWidth + diffStrokeWidth; var penCanvasCurrentHeight = parseInt(ElementMediator.getProperty(this._canvas, "height")); var penCanvasNewHeight = penCanvasCurrentHeight + diffStrokeWidth; var penCanvasCurrentLeft = parseInt(ElementMediator.getProperty(this._canvas, "left")); var penCanvasNewLeft = penCanvasCurrentLeft - delta; var penCanvasCurrentTop = parseInt(ElementMediator.getProperty(this._canvas, "top")); var penCanvasNewTop = penCanvasCurrentTop - delta; var canvasArray=[this._canvas]; ElementMediator.setProperty(canvasArray, "width", [penCanvasNewWidth+"px"], "Changing", "penTool"); ElementMediator.setProperty(canvasArray, "height", [penCanvasNewHeight+"px"], "Changing", "penTool"); ElementMediator.setProperty(canvasArray, "left", [penCanvasNewLeft+"px"],"Changing", "penTool"); ElementMediator.setProperty(canvasArray, "top", [penCanvasNewTop+ "px"],"Changing", "penTool"); }; GLSubpath.prototype.getStrokeColor = function () { return this._strokeColor; }; GLSubpath.prototype.setStrokeColor = function (c) { this._strokeColor = c; }; GLSubpath.prototype.getFillColor = function() { return this._fillColor; }; GLSubpath.prototype.setFillColor = function(c){ this._fillColor = c; }; GLSubpath.prototype.translateAnchors = function (tx, ty, tz) { for (var i=0;i<this._Anchors.length;i++){ this._Anchors[i].translateAll(tx,ty,tz); } this._dirty = true; }; GLSubpath.prototype.translateSamples = function (tx, ty, tz) { for (var i=0;i<this._Samples.length;i++){ this._Samples[i][0]+=tx; this._Samples[i][1]+=ty; this._Samples[i][2]+=tz; } }; GLSubpath.prototype._getCubicBezierPoint = function(C0X, C0Y, C0Z, C1X, C1Y, C1Z, C2X, C2Y, C2Z, C3X, C3Y, C3Z, param) { var t = param; var t2 = t * t; var t3 = t * t2; var s = 1 - t; var s2 = s * s; var s3 = s * s2; var Px = s3 * C0X + 3 * s2 * t * C1X + 3 * s * t2 * C2X + t3 * C3X; var Py = s3 * C0Y + 3 * s2 * t * C1Y + 3 * s * t2 * C2Y + t3 * C3Y; var Pz = s3 * C0Z + 3 * s2 * t * C1Z + 3 * s * t2 * C2Z + t3 * C3Z; return [Px,Py, Pz]; }; GLSubpath.prototype.getCubicBezierPoint = function(startIndex, param){ var C0X = this._Anchors[startIndex].getPosX(); var C0Y = this._Anchors[startIndex].getPosY(); var C0Z = this._Anchors[startIndex].getPosZ(); var C1X = this._Anchors[startIndex].getNextX(); var C1Y = this._Anchors[startIndex].getNextY(); var C1Z = this._Anchors[startIndex].getNextZ(); var nextIndex = (startIndex +1)% this._Anchors.length; var C2X = this._Anchors[nextIndex].getPrevX(); var C2Y = this._Anchors[nextIndex].getPrevY(); var C2Z = this._Anchors[nextIndex].getPrevZ(); var C3X = this._Anchors[nextIndex].getPosX(); var C3Y = this._Anchors[nextIndex].getPosY(); var C3Z = this._Anchors[nextIndex].getPosZ(); return this._getCubicBezierPoint(C0X, C0Y, C0Z, C1X, C1Y, C1Z, C2X, C2Y, C2Z, C3X, C3Y, C3Z, param); }; GLSubpath.prototype._sampleCubicBezierUniform = function (C0X, C0Y, C0Z, C1X, C1Y, C1Z, C2X, C2Y, C2Z, C3X, C3Y, C3Z, beginParam, endParam) { //for now, sample using regularly spaced parameter values var numSteps = 11; //hard-coded for now var stepSize = 0.1; // = 1/(numSteps-1) for (var i = 0; i < numSteps; i++) { var t = i * stepSize; var t2 = t * t; var t3 = t * t2; var s = 1 - t; var s2 = s * s; var s3 = s * s2; var Px = s3 * C0X + 3 * s2 * t * C1X + 3 * s * t2 * C2X + t3 * C3X; var Py = s3 * C0Y + 3 * s2 * t * C1Y + 3 * s * t2 * C2Y + t3 * C3Y; var Pz = s3 * C0Z + 3 * s2 * t * C1Z + 3 * s * t2 * C2Z + t3 * C3Z; this._Samples.push([Px, Py, Pz]); if (beginParam && endParam) { this._sampleParam.push(beginParam + (endParam-beginParam)*t); } } }; // _sampleCubicBezier // queries the Bezier curve and adaptively samples it, adds samples in this._samples GLSubpath.prototype._sampleCubicBezier = function (C0X, C0Y, C0Z, C1X, C1Y, C1Z, C2X, C2Y, C2Z, C3X, C3Y, C3Z, beginParam, endParam) { var C0 = [C0X, C0Y, C0Z]; var C1 = [C1X, C1Y, C1Z]; var C2 = [C2X, C2Y, C2Z]; var C3 = [C3X, C3Y, C3Z]; //measure distance of C1 and C2 to segment C0-C3 var distC1 = MathUtils.distPointToSegment(C1, C0, C3); var distC2 = MathUtils.distPointToSegment(C2, C0, C3); var maxDist = Math.max(distC1, distC2); //if max. distance is smaller than threshold, early exit var threshold = this._SAMPLING_EPSILON; //this should be set outside this function if (maxDist < threshold) { //push the endpoints and return this._Samples.push([C0X, C0Y, C0Z]); this._Samples.push([C3X, C3Y, C3Z]); this._sampleParam.push(beginParam); this._sampleParam.push(endParam); return; } //subdivide this curve var C0_ = VecUtils.vecAdd(3, C0, C1); C0_ = VecUtils.vecScale(3, C0_, 0.5); var C1_ = VecUtils.vecAdd(3, C1, C2); C1_ = VecUtils.vecScale(3, C1_, 0.5); var C2_ = VecUtils.vecAdd(3, C2, C3); C2_ = VecUtils.vecScale(3, C2_, 0.5); var C0__ = VecUtils.vecAdd(3, C0_, C1_); C0__ = VecUtils.vecScale(3, C0__, 0.5); var C1__ = VecUtils.vecAdd(3, C1_, C2_); C1__ = VecUtils.vecScale(3, C1__, 0.5); var C0___ = VecUtils.vecAdd(3, C0__, C1__); C0___ = VecUtils.vecScale(3, C0___, 0.5); var midParam = beginParam + (endParam-beginParam)*0.5; //recursively sample the first half of the curve this._sampleCubicBezier(C0X, C0Y, C0Z, C0_[0], C0_[1], C0_[2], C0__[0], C0__[1], C0__[2], C0___[0], C0___[1], C0___[2], beginParam, midParam); //recursively sample the second half of the curve this._sampleCubicBezier(C0___[0], C0___[1], C0___[2], C1__[0], C1__[1], C1__[2], C2_[0], C2_[1], C2_[2], C3X, C3Y, C3Z, midParam, endParam); }; /////////////////////////////////////////////////////////// // Methods /////////////////////////////////////////////////////////// GLSubpath.prototype._unprojectPt = function(pt, pespectiveDist) { var retPt = pt.slice(0); if (MathUtils.fpCmp(pespectiveDist,-pt[2]) !== 0){ z = pt[2]*pespectiveDist/(pespectiveDist + pt[2]); var x = pt[0]*(pespectiveDist - z)/pespectiveDist, y = pt[1]*(pespectiveDist - z)/pespectiveDist; retPt[0] = x; retPt[1] = y; retPt[2] = z; } return retPt; }; // createSamples // stores samples of the subpath in _samples GLSubpath.prototype.createSamples = function (isStageWorldCoord) { if (this._dirty) { //clear any previously computed samples this._Samples = []; this._sampleParam = []; this._anchorSampleIndex = []; var numAnchors = this._Anchors.length; if (numAnchors > 1) { for (var i = 0; i < numAnchors - 1; i++) { //get the control points var C0X = this._Anchors[i].getPosX(); var C0Y = this._Anchors[i].getPosY(); var C0Z = this._Anchors[i].getPosZ(); var C1X = this._Anchors[i].getNextX(); var C1Y = this._Anchors[i].getNextY(); var C1Z = this._Anchors[i].getNextZ(); var C2X = this._Anchors[i + 1].getPrevX(); var C2Y = this._Anchors[i + 1].getPrevY(); var C2Z = this._Anchors[i + 1].getPrevZ(); var C3X = this._Anchors[i + 1].getPosX(); var C3Y = this._Anchors[i + 1].getPosY(); var C3Z = this._Anchors[i + 1].getPosZ(); var beginParam = i; var endParam = i+1; this._anchorSampleIndex.push(this._Samples.length); //index of sample corresponding to anchor i this._sampleCubicBezier(C0X, C0Y, C0Z, C1X, C1Y, C1Z, C2X, C2Y, C2Z, C3X, C3Y, C3Z, beginParam, endParam); } //for every anchor point i, except last if (this._isClosed) { var i = numAnchors - 1; //get the control points var C0X = this._Anchors[i].getPosX(); var C0Y = this._Anchors[i].getPosY(); var C0Z = this._Anchors[i].getPosZ(); var C1X = this._Anchors[i].getNextX(); var C1Y = this._Anchors[i].getNextY(); var C1Z = this._Anchors[i].getNextZ(); var C2X = this._Anchors[0].getPrevX(); var C2Y = this._Anchors[0].getPrevY(); var C2Z = this._Anchors[0].getPrevZ(); var C3X = this._Anchors[0].getPosX(); var C3Y = this._Anchors[0].getPosY(); var C3Z = this._Anchors[0].getPosZ(); var beginParam = i; var endParam = i+1; this._anchorSampleIndex.push(this._Samples.length); //index of sample corresponding to anchor i this._sampleCubicBezier(C0X, C0Y, C0Z, C1X, C1Y, C1Z, C2X, C2Y, C2Z, C3X, C3Y, C3Z, beginParam, endParam); } else { this._anchorSampleIndex.push((this._Samples.length) - 1); //index of sample corresponding to last anchor } } //if (numAnchors >== 2) { //re-compute the bounding box (this also accounts for stroke width, so assume the stroke width is set) this.computeBoundingBox(true, isStageWorldCoord); } //if (this._dirty) this._dirty = false; }; GLSubpath.prototype.offsetPerBBoxMin = function() { //offset the anchor and sample coordinates such that the min point of the bbox is at [0,0,0] this.translateAnchors(-this._BBoxMin[0], -this._BBoxMin[1], -this._BBoxMin[2]); this.translateSamples(-this._BBoxMin[0], -this._BBoxMin[1], -this._BBoxMin[2]); this._BBoxMax[0]-= this._BBoxMin[0]; this._BBoxMax[1]-= this._BBoxMin[1]; this._BBoxMax[2]-= this._BBoxMin[2]; this._BBoxMin[0] = this._BBoxMin[1] = this._BBoxMin[2] = 0; }; GLSubpath.prototype.computeBoundingBox = function(useSamples, isStageWorldCoord){ this._BBoxMin = [Infinity, Infinity, Infinity]; this._BBoxMax = [-Infinity, -Infinity, -Infinity]; if (useSamples) { var numPoints = this._Samples.length; if (numPoints === 0) { this._BBoxMin = [0, 0, 0]; this._BBoxMax = [0, 0, 0]; } else { for (var i=0;i<numPoints;i++){ var pt = this._Samples[i]; for (var d = 0; d < 3; d++) { if (this._BBoxMin[d] > pt[d]) { this._BBoxMin[d] = pt[d]; } if (this._BBoxMax[d] < pt[d]) { this._BBoxMax[d] = pt[d]; } }//for every dimension d from 0 to 2 } } } else{ //build a bbox of the anchor points, not the path itself var numAnchors = this._Anchors.length; var anchorPts = [[0,0,0], [0,0,0], [0,0,0]]; if (numAnchors === 0) { this._BBoxMin = [0, 0, 0]; this._BBoxMax = [0, 0, 0]; } else { for (var i = 0; i < numAnchors; i++) { anchorPts[0] = ([this._Anchors[i].getPosX(),this._Anchors[i].getPosY(),this._Anchors[i].getPosZ()]); anchorPts[1] = ([this._Anchors[i].getPrevX(),this._Anchors[i].getPrevY(),this._Anchors[i].getPrevZ()]); anchorPts[2] = ([this._Anchors[i].getNextX(),this._Anchors[i].getNextY(),this._Anchors[i].getNextZ()]); for (var p=0;p<3;p++){ for (var d = 0; d < 3; d++) { if (this._BBoxMin[d] > anchorPts[p][d]) { this._BBoxMin[d] = anchorPts[p][d]; } if (this._BBoxMax[d] < anchorPts[p][d]) { this._BBoxMax[d] = anchorPts[p][d]; } }//for every dimension d from 0 to 2 } //for every anchorPts p from 0 to 2 } //for every anchor point i } //else of if (numSamples === 0) { }//else of if useSamples //increase the bbox given the stroke width var dim = 2; if (isStageWorldCoord){ dim=3; } else { this._BBoxMax[2]=this._BBoxMin[2]=0;//zero out the Z coord since in local coord everything is flat } for (var d = 0; d < dim; d++) { this._BBoxMin[d]-= this._strokeWidth*0.5; this._BBoxMax[d]+= this._strokeWidth*0.5; }//for every dimension d from 0 to 3 }; //returns v such that it is in [min,max] GLSubpath.prototype._clamp = function (v, min, max) { if (v < min) { return min; } if (v > max) { return max; } return v; }; GLSubpath.prototype.getNearVertex = function( eyePt, dir ){ //todo fill in this function return null; }; GLSubpath.prototype.getNearPoint = function( eyePt, dir ){ return null; }; //returns true if P is left of line through l0 and l1 or on it GLSubpath.prototype.isLeft = function(l0, l1, P){ var signedArea = (l1[0]-l0[0])*(P[1] - l0[1]) - (P[0]-l0[0])*(l1[1]-l0[1]); if (signedArea>=0) { return true; } else { return false; } }; //returns true if 2D point p is contained within 2D quad given by r0,r1,r2,r3 (need not be axis-aligned) GLSubpath.prototype.isPointInQuad2D = function(r0,r1,r2,r3,p){ //returns true if the point is on the same side of the segments r0r1, r1r2, r2r3, r3r0 var isLeft0 = this.isLeft(r0,r1,p); var isLeft1 = this.isLeft(r1,r2,p); var isLeft2 = this.isLeft(r2,r3,p); var isLeft3 = this.isLeft(r3,r0,p); var andAll = isLeft0 & isLeft1 & isLeft2 & isLeft3; if (andAll) return true; var orAll = isLeft0 | isLeft1 | isLeft2 | isLeft3; if (!orAll) { return true; } return false; }; GLSubpath.prototype.exportJSON = function() { var retObject= new Object(); //the type of this object retObject.type = this.geomType(); retObject.geomType = retObject.type; //the geometry for this object (anchor points in stage world space) retObject.anchors = this._Anchors.slice(0); retObject.isClosed = this._isClosed; //stroke appearance properties retObject.strokeWidth = this._strokeWidth; retObject.strokeColor = this._strokeColor; retObject.fillColor = this._fillColor; return retObject; }; GLSubpath.prototype.export = function() { var jsonObject = this.exportJSON(); var stringified = JSON.stringify(jsonObject); return "type: " + this.geomType() + "\n" + stringified; }; GLSubpath.prototype.importJSON = function(jo) { if (this.geomType()!== jo.geomType){ return; } //the geometry for this object this._Anchors = []; var i=0; for (i=0;i<jo.anchors.length;i++){ this.addAnchor(new AnchorPoint()); var newAnchor = this.getAnchor(this.getSelectedAnchorIndex()); var ipAnchor = jo.anchors[i]; newAnchor.setPos(ipAnchor._x, ipAnchor._y, ipAnchor._z); newAnchor.setPrevPos(ipAnchor._prevX, ipAnchor._prevY, ipAnchor._prevZ); newAnchor.setNextPos(ipAnchor._nextX, ipAnchor._nextY, ipAnchor._nextZ); } this._isClosed = jo.isClosed; //stroke appearance properties this._strokeWidth = jo.strokeWidth; this._strokeColor = jo.strokeColor; this._fillColor = jo.fillColor; this._dirty = true; this.createSamples(false); this.offsetPerBBoxMin(); //compute and store the center of the bbox in local space var bboxMin = this.getBBoxMin(); var bboxMax = this.getBBoxMax(); var bboxMid = [0.5*(bboxMin[0]+bboxMax[0]),0.5*(bboxMin[1]+bboxMax[1]),0.5*(bboxMin[2]+bboxMax[2])]; this.setCanvasCenterLocalCoord(bboxMid); }; GLSubpath.prototype.import = function( importStr ) { var jsonObject = JSON.parse(importStr); this.importJSON(jsonObject); }; GLSubpath.prototype.collidesWithPoint = function (x, y, z) { if (x < this._BBoxMin[0]) return false; if (x > this._BBoxMax[0]) return false; if (y < this._BBoxMin[1]) return false; if (y > this._BBoxMax[1]) return false; if (z < this._BBoxMin[2]) return false; if (z > this._BBoxMax[2]) return false; return true; }; GLSubpath.prototype.collidesWithPoint = function (x, y) { if (x < this._BBoxMin[0]) return false; if (x > this._BBoxMax[0]) return false; if (y < this._BBoxMin[1]) return false; if (y > this._BBoxMax[1]) return false; return true; }; //GLSubpath.prototype = new GeomObj(); if (typeof exports === "object") { exports.SubPath = GLSubpath; }