/* <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> */ // Todo: This entire class should be converted to a module var VecUtils = require("js/helper-classes/3D/vec-utils").VecUtils; function SubpathOffsetPoint(pos, mapPos) { this.Pos = Vector.create([pos[0],pos[1],pos[2]]); this.CurveMapPos = Vector.create([mapPos[0], mapPos[1], mapPos[2]]); } function SubpathOffsetTriangle(v0, v1, v2) { this.v0 = v0; this.v1 = v1; this.v2 = v2; this.n = Vector.create([0,0,1]); //replace with the actual cross product later } function sortNumberAscending(a,b){ return a-b; } function sortNumberDescending(a,b){ return b-a; } function SegmentIntersections(){ this.paramArray = []; } /////////////////////////////////////////////////////////////////////// // Class GLSubpath // representation a sequence of cubic bezier curves. // Derived from class GLGeomObj /////////////////////////////////////////////////////////////////////// function GLSubpath() { /////////////////////////////////////////////////// // Instance variables /////////////////////////////////////////////////// this._Anchors = []; this._BBoxMin = [0, 0, 0]; this._BBoxMax = [0, 0, 0]; this._isClosed = false; this._samples = []; //polyline representation of this curve 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 this._UnprojectedAnchors = []; //initially set the _dirty bit so we will construct samples this._dirty = true; //whether or not to use the canvas drawing to stroke/fill this._useCanvasDrawing = true; //the X and Y location of this subpath's canvas in stage world space of Ninja this._canvasX = 0; this._canvasY = 0; //stroke information this._strokeWidth = 0.0; this._strokeColor = [0.4, 0.4, 0.4, 1.0]; this._strokeMaterial; this._strokeStyle = "Solid"; this._materialAmbient = [0.2, 0.2, 0.2, 1.0]; this._materialDiffuse = [0.4, 0.4, 0.4, 1.0]; this._materialSpecular = [0.4, 0.4, 0.4, 1.0]; this._fillColor = [0.4, 0.4, 0.4, 1.0]; this._fillMaterial; this._DISPLAY_ANCHOR_RADIUS = 5; //drawing context this._world = null; //tool that owns this subpath this._drawingTool = null; this._planeMat = null; this._planeMatInv = null; this._planeCenter = null; this.inheritedFrom = GLGeomObj; this.inheritedFrom(); //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 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 // (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() //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" ); // 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 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 fillColorStr = "rgba("+parseInt(255*this._fillColor[0])+","+parseInt(255*this._fillColor[1])+","+parseInt(255*this._fillColor[2])+","+this._fillColor[3]+")"; ctx.fillStyle = fillColorStr; } var lineCap = ['butt','round','square']; ctx.lineCap = lineCap[1]; ctx.beginPath(); /* commenting this out for now because of Chrome bug where coincident endpoints of bezier curve cause the curve to not be rendered 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; } 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; ctx.fill(); } */ var numPoints = this._samples.length/3; ctx.moveTo(this._samples[0]-bboxMin[0],this._samples[1]-bboxMin[1]); for (var i=0;i<numPoints;i++){ ctx.lineTo(this._samples[3*i]-bboxMin[0],this._samples[3*i + 1]-bboxMin[1]); } if (this._isClosed === true) { ctx.lineTo(this._samples[0]-bboxMin[0],this._samples[1]-bboxMin[1]); } ctx.fill(); ctx.stroke(); ctx.restore(); } //render() this.geomType = function () { return this.GEOM_TYPE_CUBIC_BEZIER; } } //function GLSubpath ...class definition ///////////////////////////////////////////////////////// // 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; } GLSubpath.prototype.insertAnchor = function(anchorPt, index){ this._Anchors.splice(index, 0, anchorPt); } //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; } //deselect the removed anchor this._selectedAnchorIndex = -1; 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 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; } //remove all the anchor points GLSubpath.prototype.clearAllAnchors = function () { this._Anchors = []; this._isClosed = false; this._dirty = true; } 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 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]); } //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<startIndex){ //go from startIndex to the end of the samples endIndex = this._samples.length/3; } for (var i=startIndex; i<endIndex; i++){ var seg0 = Vector.create([this._samples[3*i], this._samples[3*i + 1], this._samples[3*i + 2]]); var j=i+1; var seg1 = Vector.create([this._samples[3*j], this._samples[3*j + 1], this._samples[3*j + 2]]); 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 var retParam = this._sampleParam[i] + (this._sampleParam[j] - this._sampleParam[i])*paramDistance; return retParam; } } return null; } 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 = Vector.create([Infinity, Infinity, Infinity]); var bboxMax = Vector.create([-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(Vector.create([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(Vector.create([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._isWithinBoundingBox = function(point, ctrlPts, radius) { var bboxMin = Vector.create([Infinity, Infinity, Infinity]); var bboxMax = Vector.create([-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.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; //check if the clicked location is close to the currently selected anchor position if (this._selectedAnchorIndex>=0 && this._selectedAnchorIndex<this._Anchors.length){ var distSq = this._Anchors[this._selectedAnchorIndex].getDistanceSq(pickX, pickY, pickZ); //check the anchor point if (distSq < minDistance && distSq < radSq) { selAnchorIndex = this._selectedAnchorIndex; minDistance = distSq; retCode = retCode | this.SEL_ANCHOR; } } //now check if the click location is close to any anchor position if (selAnchorIndex===-1) { 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 GLSubpath.prototype.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; //check if the clicked location is close to the currently selected anchor position if (this._selectedAnchorIndex>=0 && this._selectedAnchorIndex<this._Anchors.length){ var distSq = this._Anchors[this._selectedAnchorIndex].getDistanceSq(pickX, pickY, pickZ); //check the anchor point if (distSq < minDistance && distSq < radSq) { selAnchorIndex = this._selectedAnchorIndex; minDistance = distSq; retCode = retCode | this.SEL_ANCHOR; } } //now check if the click location is close to any anchor position if (selAnchorIndex===-1) { 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<minDistance && distSq<radSq){ selAnchorIndex = this._selectedAnchorIndex; minDistance = distSq; retCode = retCode | this.SEL_NEXT; } } } var retParam = null; if (retCode !== this.SEL_NONE) { retCode = retCode | this.SEL_PATH; //ensure that path is also selected if anything else is selected this._selectedAnchorIndex = selAnchorIndex; } else { this._selectedAnchorIndex = -1; 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 = Vector.create([Vector.create([this._Anchors[i].getPosX(),this._Anchors[i].getPosY(),this._Anchors[i].getPosZ()]), Vector.create([this._Anchors[i].getNextX(),this._Anchors[i].getNextY(),this._Anchors[i].getNextZ()]), Vector.create([this._Anchors[nextIndex].getPrevX(),this._Anchors[nextIndex].getPrevY(),this._Anchors[nextIndex].getPrevZ()]), Vector.create([this._Anchors[nextIndex].getPosX(),this._Anchors[nextIndex].getPosY(),this._Anchors[nextIndex].getPosZ()])]); var point = Vector.create([pickX, pickY, pickZ]); if (this._isWithinBoundingBox(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); console.log("intersectParam:"+intersectParam); if (intersectParam){ retCode = retCode | this.SEL_PATH; retParam = intersectParam-i; //make the retParam go from 0 to 1 this._selectedAnchorIndex = i; break; } } }//for every anchor i } this._selectMode = retCode; return retParam; } //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) { 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 } 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.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._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 Vector.create([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); this._samples.push(Py); this._samples.push(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 = Vector.create([C0X, C0Y, C0Z]); var C1 = Vector.create([C1X, C1Y, C1Z]); var C2 = Vector.create([C2X, C2Y, C2Z]); var C3 = Vector.create([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); this._samples.push(C0Y); this._samples.push(C0Z); this._samples.push(C3X); this._samples.push(C3Y); this._samples.push(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 /////////////////////////////////////////////////////////// // createSamples // stores samples of the subpath in _samples GLSubpath.prototype.createSamples = function () { if (this._dirty) { //clear any previously computed samples this._samples = []; this._sampleParam = []; this._anchorSampleIndex = []; var numAnchors = this._Anchors.length; if (numAnchors > 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 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/3); //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/3) - 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); } //if (this._dirty) this._dirty = false; } GLSubpath.prototype.computeBoundingBox = function(useSamples){ this._BBoxMin = [Infinity, Infinity, Infinity]; this._BBoxMax = [-Infinity, -Infinity, -Infinity]; if (useSamples) { var numPoints = this._samples.length/3; if (numPoints === 0) { this._BBoxMin = [0, 0, 0]; this._BBoxMax = [0, 0, 0]; } else { for (var i=0;i<numPoints;i++){ var pt = Vector.create([this._samples[3*i],this._samples[3*i + 1],this._samples[3*i + 2]]); 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 = [Vector.create([0,0,0]), Vector.create([0,0,0]), Vector.create([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] = (Vector.create([this._Anchors[i].getPosX(),this._Anchors[i].getPosY(),this._Anchors[i].getPosZ()])); anchorPts[1] = (Vector.create([this._Anchors[i].getPrevX(),this._Anchors[i].getPrevY(),this._Anchors[i].getPrevZ()])); anchorPts[2] = (Vector.create([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 for (var d = 0; d < 3; d++) { this._BBoxMin[d]-= this._strokeWidth/2; this._BBoxMax[d]+= this._strokeWidth/2; }//for every dimension d from 0 to 2 } //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; }, //input: point sIn in stage-world space, planeMidPt in stage-world space, matrix planeMat that rotates plane into XY (parallel to view plane), inverse of planeMat //returns: sIn 'unprojected' GLSubpath.prototype.unprojectPoint = function ( sIn, planeMidPt, planeMat, planeMatInv) { var s = sIn.slice(0); s[0] -= planeMidPt[0]; s[1] -= planeMidPt[1]; //bring s to the center of the plane // unproject the point s var i; var viewZ = 1400; if (MathUtils.fpCmp(viewZ,-s[2]) !== 0){ z = s[2]*viewZ/(viewZ + s[2]); var x = s[0]*(viewZ - z)/viewZ, y = s[1]*(viewZ - z)/viewZ; s[0] = x; s[1] = y; s[2] = z; } // add the translation back in s[0] += planeMidPt[0]; s[1] += planeMidPt[1]; return s; }, GLSubpath.prototype.computeUnprojectedNDC = function (pos, bboxMid, bboxDim, r, l, t, b, z, zn) { //unproject pos from stageworld to unprojected state var ppos = this.unprojectPoint(pos, this._planeCenter, this._planeMat, this._planeMatInv); //make the coordinates lie in [-1,1] var x = (ppos[0] - bboxMid[0]) / bboxDim[0]; var y = -(ppos[1] - bboxMid[1]) / bboxDim[1]; //x and y should never be outside the [-1,1] range x = this._clamp(x, -1, 1); y = this._clamp(y, -1, 1); //apply the perspective transform x *= -z * (r - l) / (2.0 * zn); y *= -z * (t - b) / (2.0 * zn); ppos[0] = x; ppos[1] = y; ppos[2] = 0; //z; return ppos; } GLSubpath.prototype.makeStrokeMaterial = function() { var strokeMaterial; if (this.getStrokeMaterial()) strokeMaterial = this.getStrokeMaterial().dup(); else strokeMaterial = new FlatMaterial(); if (strokeMaterial) { strokeMaterial.init(); //if(!this.getStrokeMaterial() && this._strokeColor) if(this._strokeColor) { strokeMaterial.setProperty("color", this._strokeColor); } } this._materialArray.push( strokeMaterial ); this._materialTypeArray.push( "stroke" ); return strokeMaterial; } GLSubpath.prototype.makeFillMaterial = function() { var fillMaterial; if (this.getFillMaterial()) fillMaterial = this.getFillMaterial().dup(); else fillMaterial = new FlatMaterial(); if (fillMaterial) { fillMaterial.init(); //if(!this.getFillMaterial() && this._fillColor) if (this._fillColor) { fillMaterial.setProperty("color", this._fillColor); } } this._materialArray.push( fillMaterial ); this._materialTypeArray.push( "fill" ); return fillMaterial; } GLSubpath.prototype.getNearVertex = function( eyePt, dir ){ //get the parameters used for computing perspective transformation var bboxDim = []; var bboxMid = []; bboxDim[0] = 0.5 * (this._BBoxMax[0] - this._BBoxMin[0]); bboxMid[0] = 0.5 * (this._BBoxMax[0] + this._BBoxMin[0]); bboxDim[1] = 0.5 * (this._BBoxMax[1] - this._BBoxMin[1]); bboxMid[1] = 0.5 * (this._BBoxMax[1] + this._BBoxMin[1]); bboxDim[2] = 0.5 * (this._BBoxMax[2] - this._BBoxMin[2]); bboxMid[3] = 0.5 * (this._BBoxMax[2] + this._BBoxMin[2]); // convert the stroke vertices into normalized device coordinates var world = this.getWorld(); if (!world) return null; var aspect = world.getAspect(); var zn = world.getZNear(), zf = world.getZFar(); var t = zn * Math.tan(world.getFOV() * Math.PI / 360.0), //top of the frustum b = -t, //bottom r = aspect * t, //right l = -r; //left // calculate the object coordinates from their NDC coordinates var z = -world.getViewDistance(); // the eyePt and dir are in WebGL space...we need to convert each anchor point into the WebGL space var numAnchors = this._Anchors.length; var selAnchorPpos = null; var minDistance = Infinity; for (var i = 0; i < numAnchors; i++) { var anchorPos = Vector.create([this._Anchors[i].getPosX(), this._Anchors[i].getPosY(), this._Anchors[i].getPosZ()]); var ppos = this.computeUnprojectedNDC(anchorPos, bboxMid, bboxDim, r, l, t, b, z, zn); var dist = MathUtils.distPointToRay(ppos, eyePt, dir); if (dist < minDistance) { selAnchorPpos = ppos; minDistance = dist; } } return selAnchorPpos; } 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.export = function() { var rtnStr = "type: " + this.geomType() + "\n"; rtnStr += "strokeWidth: " + this._strokeWidth + "\n"; rtnStr += "strokeStyle: " + this._strokeStyle + "\n"; rtnStr += "strokeMat: "; if (this._strokeMaterial) rtnStr += this._strokeMaterial.getName(); else rtnStr += "flatMaterial"; rtnStr += "\n"; rtnStr += "fillMat: "; if (this._fillMaterial) rtnStr += this._fillMaterial.getName(); else rtnStr += "flatMaterial"; rtnStr += "\n"; var isClosedStr = "false"; if (this._isClosed) isClosedStr = "true"; rtnStr += "isClosed: " + isClosedStr + "\n"; //add the anchor points var numAnchors = this._Anchors.length; rtnStr += "numAnchors: " + numAnchors + "\n"; for (var i=0;i<numAnchors;i++){ rtnStr += "anchor"+i+"x: " + this._Anchors[i].getPosX() + "\n"; rtnStr += "anchor"+i+"y: " + this._Anchors[i].getPosY() + "\n"; rtnStr += "anchor"+i+"z: " + this._Anchors[i].getPosZ() + "\n"; rtnStr += "anchor"+i+"prevx: " + this._Anchors[i].getPrevX() + "\n"; rtnStr += "anchor"+i+"prevy: " + this._Anchors[i].getPrevY() + "\n"; rtnStr += "anchor"+i+"prevz: " + this._Anchors[i].getPrevZ() + "\n"; rtnStr += "anchor"+i+"nextx: " + this._Anchors[i].getNextX() + "\n"; rtnStr += "anchor"+i+"nexty: " + this._Anchors[i].getNextY() + "\n"; rtnStr += "anchor"+i+"nextz: " + this._Anchors[i].getNextZ() + "\n"; } return rtnStr; } GLSubpath.prototype.import = function( importStr ) { this._strokeWidth = this.getPropertyFromString( "strokeWidth: ", importStr ); this._strokeStyle = this.getPropertyFromString( "strokeStyle: ", importStr ); var strokeMaterialName = this.getPropertyFromString( "strokeMat: ", importStr ); var fillMaterialName = this.getPropertyFromString( "fillMat: ", importStr ); var strokeMat = MaterialsLibrary.getMaterial( strokeMaterialName ); if (!strokeMat) { console.log( "object material not found in library: " + strokeMaterialName ); strokeMat = new FlatMaterial(); } this._strokeMaterial = strokeMat; var fillMat = MaterialsLibrary.getMaterial( fillMaterialName ); if (!fillMat) { console.log( "object material not found in library: " + fillMaterialName ); fillMat = new FlatMaterial(); } this._fillMaterial = fillMat; var isClosedStr = this.getPropertyFromString( "isClosed: ", importStr); this._isClosed = isClosedStr === "true"; var numAnchors = this.getPropertyFromString("numAnchors: ", importStr); for (var i=0;i<numAnchors;i++) { var posX = this.getPropertyFromString("anchor"+i+"x", importStr); var posY = this.getPropertyFromString("anchor"+i+"y", importStr); var posZ = this.getPropertyFromString("anchor"+i+"z", importStr); var prevX = this.getPropertyFromString("anchor"+i+"prevx", importStr); var prevY = this.getPropertyFromString("anchor"+i+"prevy", importStr); var prevZ = this.getPropertyFromString("anchor"+i+"prevz", importStr); var nextX = this.getPropertyFromString("anchor"+i+"nextx", importStr); var nextY = this.getPropertyFromString("anchor"+i+"nexty", importStr); var nextZ = this.getPropertyFromString("anchor"+i+"nextz", importStr); var newAnchor = new GLAnchorPoint(); newAnchor.setPos(posX, posY, posZ); newAnchor.setPrevPos(prevX, prevY, prevZ); newAnchor.setNextPos(nextX, nextY, nextZ); this._selectedSubpath.addAnchor(newAnchor); } } 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; }