From 107f79288ed87a282dd52075640297cc02bdf318 Mon Sep 17 00:00:00 2001 From: Pushkar Joshi Date: Wed, 1 Feb 2012 12:00:44 -0800 Subject: performance improvement: add most of the GLSubpath functions to its prototype --- js/helper-classes/RDGE/GLSubpath.js | 2331 +++++++++++++---------------------- 1 file changed, 848 insertions(+), 1483 deletions(-) (limited to 'js/helper-classes/RDGE/GLSubpath.js') diff --git a/js/helper-classes/RDGE/GLSubpath.js b/js/helper-classes/RDGE/GLSubpath.js index 2e4c1631..c4ca1b20 100644 --- a/js/helper-classes/RDGE/GLSubpath.js +++ b/js/helper-classes/RDGE/GLSubpath.js @@ -34,6 +34,7 @@ function SegmentIntersections(){ // representation a sequence of cubic bezier curves. // Derived from class GLGeomObj /////////////////////////////////////////////////////////////////////// + function GLSubpath() { /////////////////////////////////////////////////// // Instance variables @@ -49,14 +50,6 @@ function GLSubpath() { this._UnprojectedAnchors = []; - //offset path samples and the points on the input path they map to - this._offsetPointsLeft = []; - this._offsetPointsRight = []; - - //triangles determined by the offset points - this._offsetTrianglesLeft = []; - this._offsetTrianglesRight = []; - //initially set the _dirty bit so we will construct samples this._dirty = true; @@ -87,7 +80,6 @@ function GLSubpath() { this._planeMatInv = null; this._planeCenter = null; - // initialize the inherited members this.inheritedFrom = GLGeomObj; this.inheritedFrom(); @@ -104,728 +96,438 @@ function GLSubpath() { this._DEFAULT_STROKE_WIDTH = 20; //use only if stroke width not specified this._MAX_OFFSET_ANGLE = 10; //max angle (in degrees) between consecutive vectors from curve to offset path - ///////////////////////////////////////////////////////// - // Property Accessors/Setters - ///////////////////////////////////////////////////////// - this.setWorld = function (world) { this._world = world; } - this.getWorld = function () { return this._world; } - this.makeDirty = function () {this._dirty = true;} - this.geomType = function () { return this.GEOM_TYPE_CUBIC_BEZIER; } - this.setDrawingTool = function (tool) {this._drawingTool = tool;} - this.getDrawingTool = function () {return this._drawingTool;} - this.setPlaneMatrix = function(planeMat){this._planeMat = planeMat;} - this.setPlaneMatrixInverse = function(planeMatInv){this._planeMatInv = planeMatInv;} - this.setPlaneCenter = function(pc){this._planeCenter = pc;} - - this.getCanvasX = function(){return this._canvasX;} - this.getCanvasY = function(){return this._canvasY;} - this.setCanvasX = function(cx){this._canvasX=cx;} - this.setCanvasY = function(cy){this._canvasY=cy;} - - this.getIsClosed = function () {return this._isClosed;} - this.setIsClosed = function (isClosed) { - if (this._isClosed !== isClosed) { - this._isClosed = isClosed; - this._dirty = true; - } - } - this.getNumAnchors = function () { return this._Anchors.length; } - this.getAnchor = function (index) { return this._Anchors[index]; } - this.addAnchor = function (anchorPt) { - this._Anchors.push(anchorPt); - this._selectedAnchorIndex = this._Anchors.length-1; - this._dirty = true; - } + // (current GLGeomObj complains if buildBuffers/render is added to GLSubpath prototype) + //buildBuffers + // Build the stroke vertices, normals, textures and colors + // Add that array data to the GPU using OpenGL data binding + this.buildBuffers = function () { + return; //no need to do anything for now + }//buildBuffers() - this.insertAnchor = function(anchorPt, index){ - this._Anchors.splice(index, 0, anchorPt); - } + //render + // specify how to render the subpath in Canvas2D + this.render = function () { + // get the world + var world = this.getWorld(); + if (!world) throw( "null world in subpath render" ); - //remove and return anchor at specified index, return null on error - this.removeAnchor = function (index) { - var retAnchor = null; - if (index < this._Anchors.length) { - retAnchor = this._Anchors.splice(index, 1); - this._dirty = true; - } - //deselect the removed anchor if necessary - if (this._selectedAnchorIndex === index){ - this._selectedAnchorIndex = -1; - } - return retAnchor; - } + // get the context + var ctx = world.get2DContext(); + if (!ctx) throw ("null context in subpath render") - this.deselectAnchorPoint = function(){ - this._selectedAnchorIndex = -1; - } - - this.reversePath = function() { - var revAnchors = []; - var numAnchors = this._Anchors.length; - var lastIndex = numAnchors-1; - if (lastIndex<0){ - return; //cannot reverse empty path + var numAnchors = this.getNumAnchors(); + if (numAnchors === 0) + return; //nothing to do for empty paths + + ctx.save(); + + this.createSamples(); //dirty bit checked in this function...will generate a polyline representation + var bboxMin = this.getBBoxMin(); + var bboxMax = this.getBBoxMax(); + var bboxWidth = bboxMax[0] - bboxMin[0]; + var bboxHeight = bboxMax[1] - bboxMin[1]; + var bboxMid = Vector.create([0.5 * (bboxMax[0] + bboxMin[0]), 0.5 * (bboxMax[1] + bboxMin[1]), 0.5 * (bboxMax[2] + bboxMin[2])]); + + ctx.clearRect(0, 0, bboxWidth, bboxHeight); + + + ctx.lineWidth = this._strokeWidth; + ctx.strokeStyle = "black"; + if (this._strokeColor) + ctx.strokeStyle = MathUtils.colorToHex( this._strokeColor ); + ctx.fillStyle = "white"; + if (this._fillColor) + ctx.fillStyle = MathUtils.colorToHex( this._fillColor ); + var lineCap = ['butt','round','square']; + ctx.lineCap = lineCap[1]; + ctx.beginPath(); + var prevAnchor = this.getAnchor(0); + ctx.moveTo(prevAnchor.getPosX()-bboxMin[0],prevAnchor.getPosY()-bboxMin[1]); + for (var i = 1; i < numAnchors; i++) { + var currAnchor = this.getAnchor(i); + ctx.bezierCurveTo(prevAnchor.getNextX()-bboxMin[0],prevAnchor.getNextY()-bboxMin[1], currAnchor.getPrevX()-bboxMin[0], currAnchor.getPrevY()-bboxMin[1], currAnchor.getPosX()-bboxMin[0], currAnchor.getPosY()-bboxMin[1]); + prevAnchor = currAnchor; } - for (var i=lastIndex;i>=0;i--) { - var newAnchor = new GLAnchorPoint(); - 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._isClosed === true) { + var currAnchor = this.getAnchor(0); + ctx.bezierCurveTo(prevAnchor.getNextX()-bboxMin[0],prevAnchor.getNextY()-bboxMin[1], currAnchor.getPrevX()-bboxMin[0], currAnchor.getPrevY()-bboxMin[1], currAnchor.getPosX()-bboxMin[0], currAnchor.getPosY()-bboxMin[1]); + prevAnchor = currAnchor; } - if (this._selectedAnchorIndex >= 0){ - this._selectedAnchorIndex = (numAnchors-1) - this._selectedAnchorIndex; + if (this._isClosed){ + ctx.fill(); } - this._Anchors = revAnchors; - this._dirty=true; - } + ctx.stroke(); - //remove all the anchor points - this.clearAllAnchors = function () { - this._Anchors = []; - this._isClosed = false; - this._dirty = true; - } + ctx.restore(); + } //render() - this.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; + this.geomType = function () { return this.GEOM_TYPE_CUBIC_BEZIER; } +} //function GLSubpath ...class definition - //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 GLAnchorPoint(); + ///////////////////////////////////////////////////////// + // Property Accessors/Setters + ///////////////////////////////////////////////////////// +GLSubpath.prototype.setWorld = function (world) { this._world = world; } +GLSubpath.prototype.getWorld = function () { return this._world; } +GLSubpath.prototype.makeDirty = function () {this._dirty = true;} +GLSubpath.prototype.setDrawingTool = function (tool) {this._drawingTool = tool;} +GLSubpath.prototype.getDrawingTool = function () {return this._drawingTool;} +GLSubpath.prototype.setPlaneMatrix = function(planeMat){this._planeMat = planeMat;} +GLSubpath.prototype.setPlaneMatrixInverse = function(planeMatInv){this._planeMatInv = planeMatInv;} +GLSubpath.prototype.setPlaneCenter = function(pc){this._planeCenter = pc;} + +GLSubpath.prototype.getCanvasX = function(){return this._canvasX;} +GLSubpath.prototype.getCanvasY = function(){return this._canvasY;} +GLSubpath.prototype.setCanvasX = function(cx){this._canvasX=cx;} +GLSubpath.prototype.setCanvasY = function(cy){this._canvasY=cy;} + +GLSubpath.prototype.getIsClosed = function () {return this._isClosed;} +GLSubpath.prototype.setIsClosed = function (isClosed) { + if (this._isClosed !== isClosed) { + this._isClosed = isClosed; + this._dirty = true; + } +} +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._dirty = true; +} - 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]); - } +GLSubpath.prototype.insertAnchor = function(anchorPt, index){ + this._Anchors.splice(index, 0, anchorPt); +} - //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; +//remove and return anchor at specified index, return null on error +GLSubpath.prototype.removeAnchor = function (index) { + var retAnchor = null; + if (index < this._Anchors.length) { + retAnchor = this._Anchors.splice(index, 1); this._dirty = true; } - - this._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 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; - } - } + return retAnchor; +} - //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); +GLSubpath.prototype.deselectAnchorPoint = function(){ + this._selectedAnchorIndex = -1; +} - var C0__ = VecUtils.vecInterpolate(3, C0_, C1_, 0.5); - var C1__ = VecUtils.vecInterpolate(3, C1_, C2_, 0.5); +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 GLAnchorPoint(); + 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._dirty=true; +} - var C0___ = VecUtils.vecInterpolate(3, C0__, C1__, 0.5); +//remove all the anchor points +GLSubpath.prototype.clearAllAnchors = function () { + this._Anchors = []; + this._isClosed = false; + this._dirty = true; +} - //recursively sample the first half of the curve - var midParam = (endParam+beginParam)*0.5; - var param1 = this._checkIntersection(Vector.create([controlPts[0],C0_,C0__,C0___]), beginParam, midParam, point, radius); - if (param1!==null){ - return param1; - } +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]); + } - //recursively sample the second half of the curve - var param2 = this._checkIntersection(Vector.create([C0___,C1__,C2_,controlPts[3]]), midParam, endParam, point, radius); - if (param2!==null){ - return param2; - } + 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]); + } - //no intersection, so return null - return null; + //create a new anchor point + var newAnchor = new GLAnchorPoint(); + + 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]); } - //whether the point lies within the bbox given by the four control points - this._isWithinBoundingBox = function(point, ctrlPts, radius) { - var bboxMin = Vector.create([Infinity, Infinity, Infinity]); - var bboxMax = Vector.create([-Infinity,-Infinity,-Infinity]); - for (var i=0;i bboxMax[d]){ - bboxMax[d] = ctrlPts[i][d]; - } - } + //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._dirty = 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 (bboxMax[d]+radius)){ - return false; + if (controlPts[i][d] > bboxMax[d]){ + bboxMax[d] = controlPts[i][d]; } } - return true; - } - - this.pickAnchor = function (pickX, pickY, pickZ, radius) { - var numAnchors = this._Anchors.length; - var selAnchorIndex = -1; - var retCode = this.SEL_NONE; - var radSq = radius * radius; - var minDistance = Infinity; - for (var i = 0; i < numAnchors; i++) { - var distSq = this._Anchors[i].getDistanceSq(pickX, pickY, pickZ); - //check the anchor point - if (distSq < minDistance && distSq < radSq) { - selAnchorIndex = i; - minDistance = distSq; - } - }//for every anchor i - return selAnchorIndex; } - - //pick the path point closest to the specified location, return null if some anchor point (or its handles) is within radius, else return the parameter distance - this.pickPath = function (pickX, pickY, pickZ, radius) { - var numAnchors = this._Anchors.length; - var selAnchorIndex = -1; - var retCode = this.SEL_NONE; - var radSq = radius * radius; - var minDistance = Infinity; - for (var i = 0; i < numAnchors; i++) { - var distSq = this._Anchors[i].getDistanceSq(pickX, pickY, pickZ); - //check the anchor point - if (distSq < minDistance && distSq < radSq) { - selAnchorIndex = i; - minDistance = distSq; - retCode = retCode | this.SEL_ANCHOR; - } - }//for every anchor i - - //check the prev and next of the selected anchor if the above did not register a hit - if (this._selectedAnchorIndex>=0 && selAnchorIndex === -1) { - var distSq = this._Anchors[this._selectedAnchorIndex].getPrevDistanceSq(pickX, pickY, pickZ); - if (distSq < minDistance && distSq < radSq){ - selAnchorIndex = this._selectedAnchorIndex; - minDistance = distSq; - retCode = retCode | this.SEL_PREV; - } else { - //check the next for this anchor point - distSq = this._Anchors[this._selectedAnchorIndex].getNextDistanceSq(pickX, pickY, pickZ); - if (distSq (bboxMax[d]+radius)){ + return null; } - this.setIsClosed(subpath.getIsClosed()); - this.setStrokeWidth(subpath.getStrokeWidth()); } - this.translate = function (tx, ty, tz) { - for (var i=0;iradius) + 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; } - this._dirty = true; } - // _cleanupOffsetSamples (offSamples) - // removes retrograde segments of the offset path samples - // returns the cleaned up offset samples - this._cleanupOffsetSamples = function (offSamples, width) { - var retOffSamples = []; - var numSamples = offSamples.length; - var keepOffSample = []; - for (var i = 0; i < numSamples; i++) { - keepOffSample.push(true); - } + //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); - //NOTE: this is very slow O(n^2) for now...testing only - //remove any sample that's less than 'width' far from any other boundary segment - for (var i = 0; i < numSamples; i++) { - //build the current offset point - var O = Vector.create([offSamples[i].Pos[0], offSamples[i].Pos[1], offSamples[i].Pos[2]]); - - //iterate over all the path segments - var numPoints = this._samples.length / 3; - for (var j = 0; j < numPoints - 1; j++) { - var C0 = Vector.create([this._samples[3 * j], this._samples[3 * j + 1], this._samples[3 * j + 2]]); //segment startpoint - var C1 = Vector.create([this._samples[3 * (j + 1)], this._samples[3 * (j + 1) + 1], this._samples[3 * (j + 1) + 2]]); //segment endpoint - var distToSeg = MathUtils.distPointToSegment(O, C0, C1); - if (width - distToSeg > 1) { //if the distance is smaller than the width - keepOffSample[i] = false; - break; - } //if (width - distToC > 1 ) { //if the distance is substantially smaller than the width - } //for (var j=0;jstartIndex+1 intersects the segment from endIndex-1->endIndex - var seg0Start = Vector.create([offSamples[startIndex].Pos[0],offSamples[startIndex].Pos[1],offSamples[startIndex].Pos[2]]); - var seg0End = Vector.create([offSamples[startIndex + 1].Pos[0],offSamples[startIndex + 1].Pos[1],offSamples[startIndex + 1].Pos[2]]); - var seg1Start = Vector.create([offSamples[endIndex - 1].Pos[0],offSamples[endIndex - 1].Pos[1],offSamples[endIndex - 1].Pos[2]]); - var seg1End = Vector.create([offSamples[endIndex].Pos[0],offSamples[endIndex].Pos[1],offSamples[endIndex].Pos[2]]); - - - if (seg0Start.length===0 || seg0End.length ===0 || seg1Start.length===0 ||seg1End.length ===0){ - alert("empty offset point"); - } - - var intersection = MathUtils.segSegIntersection2D(seg0Start, seg0End, seg1Start, seg1End, 0); - - - if (intersection) { - intersection = MathUtils.makeDimension3(intersection); - //add two points for the intersection (one after to the startIndex, another before the endIndex) - var newOffsetPoint1 = new SubpathOffsetPoint(intersection, Vector.create([offSamples[startIndex + 1].CurveMapPos[0],offSamples[startIndex + 1].CurveMapPos[1],offSamples[startIndex + 1].CurveMapPos[2]])); - retOffSamples.push(newOffsetPoint1); - var newOffsetPoint2 = new SubpathOffsetPoint(intersection, Vector.create([offSamples[endIndex - 1].CurveMapPos[0], offSamples[endIndex - 1].CurveMapPos[1], offSamples[endIndex - 1].CurveMapPos[2]])); - retOffSamples.push(newOffsetPoint2); - //also add the end point - retOffSamples.push(offSamples[endIndex]); - } - - } //for (var i = 0; i < numSamples; i++) { - - - - return retOffSamples; + //recursively sample the first half of the curve + var midParam = (endParam+beginParam)*0.5; + var param1 = this._checkIntersection(Vector.create([controlPts[0],C0_,C0__,C0___]), beginParam, midParam, point, radius); + if (param1!==null){ + return param1; } - // _triangulateOffset - // generate triangles from offset points and add them to the offsetTriangles array - this._triangulateOffset = function (offsetPoints, offsetTriangles) { - if (offsetPoints.length === 0) - return; - - // triangulate using the fan method (trivial) for every consecutive pair of offset points - for (var i = 1; i < offsetPoints.length; i++) { - var tri1 = new SubpathOffsetTriangle(offsetPoints[i - 1].CurveMapPos, offsetPoints[i - 1].Pos, offsetPoints[i].CurveMapPos); - var tri2 = new SubpathOffsetTriangle(offsetPoints[i].CurveMapPos, offsetPoints[i - 1].Pos, offsetPoints[i].Pos); - offsetTriangles.push(tri1); - offsetTriangles.push(tri2); - } + //recursively sample the second half of the curve + var param2 = this._checkIntersection(Vector.create([C0___,C1__,C2_,controlPts[3]]), midParam, endParam, point, radius); + if (param2!==null){ + return param2; } - // _addOffsetSamples - // Adds samples to the offset path in places it is not sampled densely enough - //TODO: as an optimization, don't add any samples at a concave corners, since those extra points will be removed anyway - this._addOffsetSamples = function (offsetPoints) { - if (offsetPoints.length === 0) - return; - - var retOffsetPoints = []; - retOffsetPoints.push(offsetPoints[0]); - //compute angle between consecutive vectors from curve to offset - for (var i = 1; i < offsetPoints.length; i++) { - var prev = offsetPoints[i - 1]; - var curr = offsetPoints[i]; - var vec1 = VecUtils.vecSubtract(3, prev.Pos, prev.CurveMapPos); - var vec2 = VecUtils.vecSubtract(3, curr.Pos, curr.CurveMapPos); - var width = VecUtils.vecMag(3, vec1); - //NOTE: the following angle computation works only in 2D - var angle = Math.atan2(vec2[1], vec2[0]) - Math.atan2(vec1[1], vec1[0]); - //if (angle < 0) angle = angle + Math.PI + Math.PI; - angle = angle * 180 / Math.PI; - if (angle > 180) { - angle = angle - 360; - } - if (angle < -180) { - angle = 360 + angle; - } - var numNewSamples = Math.floor(Math.abs(angle) / this._MAX_OFFSET_ANGLE); - if (numNewSamples > 0) { - numNewSamples -= 1; //decrementing gives the correct number of new samples - } - if (numNewSamples > 10) { - numNewSamples = 10; //limit the number of inserted offset samples to 10 - } + //no intersection, so return null + return null; +} - //build the rotation matrix - var rotAngle = angle / (numNewSamples + 1) * Math.PI / 180; //angle to rotate (in radians) - var rotMat = Matrix.RotationZ(rotAngle); - - //build the vector to be transformed - var rotVec = VecUtils.vecNormalize(3, vec1, 1); - - for (var s = 0; s < numNewSamples; s++) { - //build curve position as a linear combination of prev. and curr - var weight = (s + 1) / (numNewSamples + 1); - var scaledPos1 = Vector.create([prev.CurveMapPos[0], prev.CurveMapPos[1], prev.CurveMapPos[2]]); - VecUtils.vecScale(3, scaledPos1, 1 - weight); - var scaledPos2 = Vector.create([curr.CurveMapPos[0], curr.CurveMapPos[1], curr.CurveMapPos[2]]); - VecUtils.vecScale(3, scaledPos2, weight); - var curvePos = VecUtils.vecAdd(3, scaledPos1, scaledPos2); - - rotVec = MathUtils.transformVector(rotVec, rotMat); - var offsetPos = VecUtils.vecAdd(3, curvePos, VecUtils.vecNormalize(3, rotVec, width)); - var newOffsetPoint = new SubpathOffsetPoint(offsetPos, curvePos); - retOffsetPoints.push(newOffsetPoint); +//whether the point lies within the bbox given by the four control points +GLSubpath.prototype._isWithinBoundingBox = function(point, ctrlPts, radius) { + var bboxMin = Vector.create([Infinity, Infinity, Infinity]); + var bboxMax = Vector.create([-Infinity,-Infinity,-Infinity]); + for (var i=0;i bboxMax[d]){ + bboxMax[d] = ctrlPts[i][d]; + } + } } - - - this._addOffsetIntersectionPoints = function (offSamples, isClosed) - { - if (offSamples.length === 0) - return; - - //TODO: implement the O(nlogn) algorithm from the Dutch book instead of the O(n^2) algorithm below - var numOrigPoints = offSamples.length; - var numOrigSegments = numOrigPoints; - if (!isClosed) - numOrigSegments--; - - //make an empty list of intersection points for every segment (assuming the segment id is the index of its start point) - var segmentIntersectionArray = []; - for (var i=0;istartIndex+1 intersects the segment from endIndex-1->endIndex - var seg0Start = Vector.create([offSamples[i].Pos[0],offSamples[i].Pos[1],offSamples[i].Pos[2]]); - var nextI = (i+1)%numOrigPoints; - var seg0End = Vector.create([offSamples[nextI].Pos[0],offSamples[nextI].Pos[1],offSamples[nextI].Pos[2]]); - - for (var j=0;j 0.01 && Math.abs(param) > 0.01) //if the intersection is not at the endpoint - segmentIntersectionArray[i].paramArray.push(param); - } - }//for every point j - }//for every point i - - var retOffSamples = [] ;//what is built and returned - //sort all the intersection points based on the parameter value - //AND add the intersection points to the new offset samples - - for (var i=0;i (bboxMax[d]+radius)){ + return false; } - return retOffSamples; } + return true; +} - - // _offsetFromSamples - // generates the offset curves (left and right) of width w from the samples (polyline) currently in this._samples - this._offsetFromSamples = function (width) { - this._offsetPointsLeft = []; - this._offsetPointsRight = []; - this._offsetTrianglesLeft = []; - this._offsetTrianglesRight = []; - - var numPoints = this._samples.length / 3; - if (numPoints < 2) { - return; //do nothing for +GLSubpath.prototype.pickAnchor = function (pickX, pickY, pickZ, radius) { + var numAnchors = this._Anchors.length; + var selAnchorIndex = -1; + var retCode = this.SEL_NONE; + var radSq = radius * radius; + var minDistance = Infinity; + for (var i = 0; i < numAnchors; i++) { + var distSq = this._Anchors[i].getDistanceSq(pickX, pickY, pickZ); + //check the anchor point + if (distSq < minDistance && distSq < radSq) { + selAnchorIndex = i; + minDistance = distSq; } + }//for every anchor i + return selAnchorIndex; +} - for (var i = 0; i < numPoints; i++) { - var C = Vector.create([this._samples[3 * i], this._samples[3 * i + 1], this._samples[3 * i + 2]]); //current point - var N = null; //next point - if (i < (numPoints - 1)) { - N = Vector.create([this._samples[3 * (i + 1)], this._samples[3 * (i + 1) + 1], this._samples[3 * (i + 1) + 2]]); - } - var P = null; //previous point - if (i > 0) { - P = Vector.create([this._samples[3 * (i - 1)], this._samples[3 * (i - 1) + 1], this._samples[3 * (i - 1) + 2]]); - } +GLSubpath.prototype.getSelectedAnchorIndex = function () { return this._selectedAnchorIndex; } +GLSubpath.prototype.getSelectedMode = function () { return this._selectMode; } - //compute the direction at C (based on averaging across prev. and next if available) - var D = null; - if (N) { - D = VecUtils.vecSubtract(3, N, C); - if (P) { - var Dprev = VecUtils.vecSubtract(3, C, P); - D = VecUtils.vecAdd(3, D, Dprev); - D = VecUtils.vecScale(3, D, 0.5); - } - } - else { - D = VecUtils.vecSubtract(3, C, P); - } +GLSubpath.prototype.getNumPoints = function () { return this._samples.length; } - if (!D) { - throw ("null direction in _offsetFromSamples"); - return; - } - //ignore this point if the D is not significant - var dirLen = VecUtils.vecMag(3, D); - if (dirLen < this._SAMPLING_EPSILON) { - continue; - } +GLSubpath.prototype.getBBoxMin = function () { return this._BBoxMin; } +GLSubpath.prototype.getBBoxMax = function () { return this._BBoxMax; } - D = VecUtils.vecNormalize(3, D, 1.0); - - //compute the perpendicular to D to the left - //TODO: for now assume we're only considering D in XY plane - var OL = Vector.create([D[1], -1*D[0], D[2]]); - OL = VecUtils.vecNormalize(3, OL, width); - var OR = Vector.create([-1 * OL[0], -1 * OL[1], -1 * OL[2]]); - OR = VecUtils.vecNormalize(3, OR, width); - var leftC = VecUtils.vecAdd(3, C, OL); - var rightC = VecUtils.vecAdd(3, C, OR); - - var sopl = new SubpathOffsetPoint(leftC, C); - this._offsetPointsLeft.push(sopl); - if (sopl.Pos.length===0 || sopl.CurveMapPos.length ===0){ - alert("empty offset point"); - } - var sopr = new SubpathOffsetPoint(rightC, C); - this._offsetPointsRight.push(sopr); - } //for (var i = 0; i < numPoints; i++) { +GLSubpath.prototype.getStrokeWidth = function () { return this._strokeWidth; } +GLSubpath.prototype.setStrokeWidth = function (w) { this._strokeWidth = w; } +GLSubpath.prototype.getStrokeMaterial = function () { return this._strokeMaterial; } +GLSubpath.prototype.setStrokeMaterial = function (m) { this._strokeMaterial = m; } +GLSubpath.prototype.getStrokeColor = function () { return this._strokeColor; } +GLSubpath.prototype.setStrokeColor = function (c) { this._strokeColor = c; } +GLSubpath.prototype.getStrokeStyle = function () { return this._strokeStyle; } +GLSubpath.prototype.setStrokeStyle = function (s) { this._strokeStyle = s; } +GLSubpath.prototype.getFillMaterial = function() {return this._fillMaterial;} +GLSubpath.prototype.setFillMaterial = function(m){ this._fillMaterial = m;} +GLSubpath.prototype.getFillColor = function() {return this._fillColor;} +GLSubpath.prototype.setFillColor = function(c){this._fillColor = c;} +GLSubpath.prototype.setWidth = function () {//NO-OP for now +} +GLSubpath.prototype.setHeight = function () {//NO-OP for now +} - //add offset samples near cusps or corners - this._offsetPointsLeft = this._addOffsetSamples(this._offsetPointsLeft); - this._offsetPointsRight = this._addOffsetSamples(this._offsetPointsRight); +GLSubpath.prototype.copyFromSubpath = function (subpath) { + this.clearAllAnchors(); + for (var i = 0; i < subpath.getNumAnchors(); i++) { + var oldAnchor = subpath.getAnchor(i); + var newAnchor = new GLAnchorPoint(); + newAnchor.setPos(oldAnchor.getPosX(), oldAnchor.getPosY(), oldAnchor.getPosZ()); + newAnchor.setPrevPos(oldAnchor.getPrevX(), oldAnchor.getPrevY(), oldAnchor.getPrevZ()); + newAnchor.setNextPos(oldAnchor.getNextX(), oldAnchor.getNextY(), oldAnchor.getNextZ()); + this.addAnchor(newAnchor); + } + this.setIsClosed(subpath.getIsClosed()); + this.setStrokeWidth(subpath.getStrokeWidth()); +} + +GLSubpath.prototype.translate = function (tx, ty, tz) { + for (var i=0;i 1) { - //start with the first anchor position (since the Bezier curve start point is not added in the sample function below) - //this._samples.push(this._Anchors[0].getPosX()); - //this._samples.push(this._Anchors[0].getPosY()); - //this._samples.push(this._Anchors[0].getPosZ()); - - 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/3); //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 poi