/* Copyright (c) 2012, Motorola Mobility LLC. All Rights Reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Motorola Mobility LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ var 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=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] 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 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=0 && this._selectedAnchorIndex 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 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 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; }