From b89a7ee8b956c96a1dcee995ea840feddc5d4b27 Mon Sep 17 00:00:00 2001 From: Pierre Frisch Date: Thu, 22 Dec 2011 07:25:50 -0800 Subject: First commit of Ninja to ninja-internal Signed-off-by: Valerio Virgillito --- js/helper-classes/RDGE/GLSubpath.js | 1671 +++++++++++++++++++++++++++++++++++ 1 file changed, 1671 insertions(+) create mode 100644 js/helper-classes/RDGE/GLSubpath.js (limited to 'js/helper-classes/RDGE/GLSubpath.js') diff --git a/js/helper-classes/RDGE/GLSubpath.js b/js/helper-classes/RDGE/GLSubpath.js new file mode 100644 index 00000000..25b12093 --- /dev/null +++ b/js/helper-classes/RDGE/GLSubpath.js @@ -0,0 +1,1671 @@ +/* +This file contains proprietary software owned by Motorola Mobility, Inc.
+No rights, expressed or implied, whatsoever to this software are provided by Motorola Mobility, Inc. hereunder.
+(c) Copyright 2011 Motorola Mobility, Inc. All Rights Reserved. +
*/ + +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 = []; + + //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; + + //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; + + // initialize the inherited members + 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 + + ///////////////////////////////////////////////////////// + // 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; + } + + this.insertAnchor = function(anchorPt, index){ + this._Anchors.splice(index, 0, anchorPt); + } + + //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; + } + + //remove all the anchor points + this.clearAllAnchors = function () { + this._Anchors = []; + this._isClosed = false; + this._dirty = true; + } + + 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; + + //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; + } + + 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 + + for (var i=startIndex; i 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 + 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]; + } + } + } + //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; + } + + //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 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; + } + + // _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); + } + } + + // _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 + } + + //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); + } + retOffsetPoints.push(offsetPoints[i]); + } //for (var i = 1; i < offsetPoints.length; i++) { + + return retOffsetPoints; + } + + + 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 0) { + P = Vector.create([this._samples[3 * (i - 1)], this._samples[3 * (i - 1) + 1], this._samples[3 * (i - 1) + 2]]); + } + + //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); + } + + 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; + } + + 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++) { + + + //add offset samples near cusps or corners + this._offsetPointsLeft = this._addOffsetSamples(this._offsetPointsLeft); + this._offsetPointsRight = this._addOffsetSamples(this._offsetPointsRight); + + //break up the offset samples at intersections + //this._offsetPointsLeft = this._addOffsetIntersectionPoints(this._offsetPointsLeft, this._isClosed); + //this._offsetPointsRight = this._addOffsetIntersectionPoints(this._offsetPointsRight , this._isClosed); + + //cleanup of the offset path + //this._offsetPointsLeft = this._cleanupOffsetSamples(this._offsetPointsLeft, width); + //this._offsetPointsRight = this._cleanupOffsetSamples(this._offsetPointsRight, width); + + //triangulate the offset path + //this._triangulateOffset(this._offsetPointsLeft, this._offsetTrianglesLeft); + //this._triangulateOffset(this._offsetPointsRight, this._offsetTrianglesRight); + } + + this._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]); + } + + this.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); + } + + this._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 + this._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 + this.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) { + + + //also create the offset path samples + if (this._strokeWidth === 0) { + this._strokeWidth = this._DEFAULT_STROKE_WIDTH; + } + //generate offset stroke if we're not using canvas drawing + if (!this._useCanvasDrawing) { + this._offsetFromSamples(this._strokeWidth/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; + } + + this.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 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{ + 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] + this._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' + this.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; + }, + + this.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; + } + + + this.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; + } + + this.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; + } + + //buildBuffers + // Build the stroke vertices, normals, textures and colors + // Add that array data to the GPU using OpenGL data binding + this.buildBuffers = function () { + if (this._useCanvasDrawing) + return; + // get the world + var world = this.getWorld(); + if (!world) throw ("null world in GLSubpath buildBuffers"); + + // create the gl buffer + var gl = world.getGLContext(); + if (!gl) throw ("null GL context in GLSubpath buildBuffers"); + + var scaleMat = Matrix.I(3); + + this._primArray = []; + this._materialNodeArray = []; + + //unproject the anchor points + // COMMENTED THIS OUT FOR NOW...CURRENTLY UNPROJECTING ALL POINTS OF THE TRIANGLES BELOW + //for (var a = 0;a=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) + this.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; + } + + //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 = "blue"; + 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[