From 7686fbad557dc2a8248d1013d5d3adb526701fc4 Mon Sep 17 00:00:00 2001 From: Dan Bagnell Date: Mon, 24 Nov 2014 16:38:55 -0500 Subject: [PATCH 01/10] Start cleaning up triangulate code by removing infinite loop (could only exit from return statements). --- Source/Core/PolygonPipeline.js | 56 ++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/Source/Core/PolygonPipeline.js b/Source/Core/PolygonPipeline.js index b7ce1d9a9fe0..1d24a13f9a6b 100644 --- a/Source/Core/PolygonPipeline.js +++ b/Source/Core/PolygonPipeline.js @@ -731,20 +731,20 @@ define([ } // Search for clean cut - var cutFound = false; var tries = 0; - while (!cutFound) { - // Make sure we don't go into an endless loop - var maxTries = nodeArray.length * 10; - if (tries > maxTries) { - // Hopefully that part of the polygon isn't important - return []; - } - tries++; + var maxTries = nodeArray.length * 10; + + var cutFound = false; + var exceptionOccurred = false; + var exceptionVertexIndex; + var index1; + var index2; + + while (!cutFound && tries++ < maxTries) { // Generate random indices - var index1 = getRandomIndex(nodeArray.length); - var index2 = index1 + 1; + index1 = getRandomIndex(nodeArray.length); + index2 = index1 + 1; while (Math.abs(index1 - index2) < 2 || Math.abs(index1 - index2) > nodeArray.length - 2) { index2 = getRandomIndex(nodeArray.length); } @@ -755,24 +755,34 @@ define([ index1 = index2; index2 = index; } + try { // Check for a clean cut - if (cleanCut(index1, index2, nodeArray)) { - // Divide polygon - var nodeArray2 = nodeArray.splice(index1, (index2 - index1 + 1), nodeArray[index1], nodeArray[index2]); - - // Chop up resulting polygons - return randomChop(nodeArray).concat(randomChop(nodeArray2)); - } + cutFound = cleanCut(index1, index2, nodeArray); } catch (exception) { - // Eliminate superfluous vertex and start over - if (exception.hasOwnProperty("vertexIndex")) { - nodeArray.splice(exception.vertexIndex, 1); - return randomChop(nodeArray); + if (!defined(exception.vertexIndex)) { + throw exception; } - throw exception; + + cutFound = true; + exceptionOccurred = true; + exceptionVertexIndex = exception.vertexIndex; } } + + if (!cutFound) { + return []; + } else if (exceptionOccurred) { + // Eliminate superfluous vertex and start over + nodeArray.splice(exceptionVertexIndex, 1); + return randomChop(nodeArray); + } + + // Divide polygon + var nodeArray2 = nodeArray.splice(index1, (index2 - index1 + 1), nodeArray[index1], nodeArray[index2]); + + // Chop up resulting polygons + return randomChop(nodeArray).concat(randomChop(nodeArray2)); } var scaleToGeodeticHeightN = new Cartesian3(); From 5f27cef4dd4d727c90427e37a16aa8f1a2ca0cc2 Mon Sep 17 00:00:00 2001 From: Dan Bagnell Date: Mon, 24 Nov 2014 17:52:05 -0500 Subject: [PATCH 02/10] Remove exception catching/throwing from triangulation algorithm. --- Source/Core/Math.js | 19 +++++ Source/Core/PolygonPipeline.js | 130 ++++++++++++++++----------------- 2 files changed, 81 insertions(+), 68 deletions(-) diff --git a/Source/Core/Math.js b/Source/Core/Math.js index d9f1db3e63f5..9e851b0d6efc 100644 --- a/Source/Core/Math.js +++ b/Source/Core/Math.js @@ -511,6 +511,25 @@ define([ return (value < 0.0) ? (value + CesiumMath.TWO_PI) % CesiumMath.TWO_PI : value; }; + /** + * The modulo operation that also works for negative dividends. + * + * @param {Number} m The dividend. + * @param {Number} n The divisor. + * @returns {Number} The remainder. + */ + CesiumMath.mod = function(m, n) { + //>>includeStart('debug', pragmas.debug); + if (!defined(m)) { + throw new DeveloperError('m is required.'); + } + if (!defined(n)) { + throw new DeveloperError('n is required.'); + } + //>>includeEnd('debug'); + return ((m % n) + n) % n; + }; + /** * Determines if two values are equal within the provided epsilon. This is useful * to avoid problems due to roundoff error when comparing floating-point values directly. diff --git a/Source/Core/PolygonPipeline.js b/Source/Core/PolygonPipeline.js index 1d24a13f9a6b..99198216173a 100644 --- a/Source/Core/PolygonPipeline.js +++ b/Source/Core/PolygonPipeline.js @@ -348,20 +348,42 @@ define([ } var rseed = 0; + var INTERNAL = -1; + var EXTERNAL = -2; + + var CLEAN_CUT = -1; + var INVALID_CUT = -2; + /** * Determine whether a cut between two polygon vertices is clean. * * @param {Number} a1i Index of first vertex. * @param {Number} a2i Index of second vertex. * @param {Array} pArray Array of { position, index } objects representing the polygon. - * @returns {Boolean} If true, a cut from the first vertex to the second is internal and does not cross any other sides. + * @returns {Number} If CLEAN_CUT, a cut from the first vertex to the second is internal and does not cross any other sides. + * If INVALID_CUT, then the vertices were valid but a cut could not be made. If the value is greater than or equal to zero, + * then the value is the index of an invalid vertex. * * @private */ function cleanCut(a1i, a2i, pArray) { - return (internalCut(a1i, a2i, pArray) && internalCut(a2i, a1i, pArray)) && + var internalCut12 = internalCut(a1i, a2i, pArray); + if (internalCut12 >= 0) { + return internalCut12; + } + + var internalCut21 = internalCut(a2i, a1i, pArray); + if (internalCut21 >= 0) { + return internalCut21; + } + + if (internalCut12 === INTERNAL && internalCut21 === INTERNAL && !intersectsSide(pArray[a1i].position, pArray[a2i].position, pArray) && - !Cartesian2.equals(pArray[a1i].position, pArray[a2i].position); + !Cartesian2.equals(pArray[a1i].position, pArray[a2i].position)) { + return CLEAN_CUT; + } + + return INVALID_CUT; } /** @@ -371,47 +393,56 @@ define([ * @param {Number} a1i Index of first vertex. * @param {Number} a2i Index of second vertex. * @param {Array} pArray Array of { position, index } objects representing the polygon. - * @returns {Boolean} If true, the cut formed between the two vertices is internal to the angle at vertex 1 + * @returns {Number} If INTERNAL, the cut formed between the two vertices is internal to the angle at vertex 1. + * If EXTERNAL, then the cut formed between the two vertices is external to the angle at vertex 1. If the value + * is greater than or equal to zero, then the value is the index of an invalid vertex. * * @private */ - var BEFORE = -1; - var AFTER = 1; var s1Scratch = new Cartesian3(); var s2Scratch = new Cartesian3(); var cutScratch = new Cartesian3(); function internalCut(a1i, a2i, pArray) { // Make sure vertex is valid - validateVertex(a1i, pArray); + if (!validateVertex(a1i, pArray)) { + return a1i; + } // Get the nodes from the array var a1 = pArray[a1i]; var a2 = pArray[a2i]; // Define side and cut vectors - var before = getNextVertex(a1i, pArray, BEFORE); - var after = getNextVertex(a1i, pArray, AFTER); + var before = CesiumMath.mod(a1i - 1, pArray.length); + if (!validateVertex(before, pArray)) { + return before; + } + + var after = CesiumMath.mod(a1i + 1, pArray.length); + if (!validateVertex(after, pArray)) { + return after; + } var s1 = Cartesian2.subtract(pArray[before].position, a1.position, s1Scratch); var s2 = Cartesian2.subtract(pArray[after].position, a1.position, s2Scratch); var cut = Cartesian2.subtract(a2.position, a1.position, cutScratch); if (isParallel(s1, cut)) { // Cut is parallel to s1 - return isInternalToParallelSide(s1, cut); + return isInternalToParallelSide(s1, cut) ? INTERNAL : EXTERNAL; } else if (isParallel(s2, cut)) { // Cut is parallel to s2 - return isInternalToParallelSide(s2, cut); + return isInternalToParallelSide(s2, cut) ? INTERNAL : EXTERNAL; } else if (angleLessThan180(s1, s2)) { // Angle at point is less than 180 if (isInsideSmallAngle(s1, s2, cut)) { // Cut is in-between sides - return true; + return INTERNAL; } - return false; + return EXTERNAL; } else if (angleGreaterThan180(s1, s2)) { // Angle at point is greater than 180 if (isInsideBigAngle(s1, s2, cut)) { // Cut is in-between sides - return false; + return EXTERNAL; } - return true; + return INTERNAL; } } @@ -470,30 +501,6 @@ define([ return Cartesian2.magnitude(cut) < Cartesian2.magnitude(side); } - /** - * Provides next vertex in some direction and also validates that vertex. - * - * @param {Number} index Index of original vertex. - * @param {Number} pArray Array of vertices. - * @param {Number} direction Direction of traversal. - * @returns {Number} Index of vertex. - * - * @private - */ - function getNextVertex(index, pArray, direction) { - var next = index + direction; - if (next < 0) { - next = pArray.length - 1; - } - if (next === pArray.length) { - next = 0; - } - - validateVertex(next, pArray); - - return next; - } - /** * Checks to make sure vertex is not superfluous. * @@ -520,10 +527,10 @@ define([ var s2 = Cartesian2.subtract(pArray[after].position, pArray[index].position, vvScratch2); if (isParallel(s1, s2)) { - var e = new DeveloperError("Superfluous vertex found."); - e.vertexIndex = index; - throw e; + return false; } + + return true; } /** @@ -734,14 +741,11 @@ define([ var tries = 0; var maxTries = nodeArray.length * 10; - var cutFound = false; - var exceptionOccurred = false; - var exceptionVertexIndex; - + var cutResult = INVALID_CUT; var index1; var index2; - while (!cutFound && tries++ < maxTries) { + while (cutResult < CLEAN_CUT && tries++ < maxTries) { // Generate random indices index1 = getRandomIndex(nodeArray.length); index2 = index1 + 1; @@ -756,33 +760,23 @@ define([ index2 = index; } - try { - // Check for a clean cut - cutFound = cleanCut(index1, index2, nodeArray); - } catch (exception) { - if (!defined(exception.vertexIndex)) { - throw exception; - } - - cutFound = true; - exceptionOccurred = true; - exceptionVertexIndex = exception.vertexIndex; - } + cutResult = cleanCut(index1, index2, nodeArray); } - if (!cutFound) { - return []; - } else if (exceptionOccurred) { + if (cutResult === CLEAN_CUT) { + // Divide polygon + var nodeArray2 = nodeArray.splice(index1, (index2 - index1 + 1), nodeArray[index1], nodeArray[index2]); + + // Chop up resulting polygons + return randomChop(nodeArray).concat(randomChop(nodeArray2)); + } else if (cutResult >= 0) { // Eliminate superfluous vertex and start over - nodeArray.splice(exceptionVertexIndex, 1); + nodeArray.splice(cutResult, 1); return randomChop(nodeArray); } - // Divide polygon - var nodeArray2 = nodeArray.splice(index1, (index2 - index1 + 1), nodeArray[index1], nodeArray[index2]); - - // Chop up resulting polygons - return randomChop(nodeArray).concat(randomChop(nodeArray2)); + // No clean cut could be found + return []; } var scaleToGeodeticHeightN = new Cartesian3(); From efd5b7e1fe890215e4314d593a5079bed2895b4b Mon Sep 17 00:00:00 2001 From: Dan Bagnell Date: Tue, 25 Nov 2014 13:55:37 -0500 Subject: [PATCH 03/10] Clean up cross products that were being recomputed and only compute the component needed. --- Source/Core/PolygonPipeline.js | 161 +++++++++------------------------ 1 file changed, 42 insertions(+), 119 deletions(-) diff --git a/Source/Core/PolygonPipeline.js b/Source/Core/PolygonPipeline.js index 99198216173a..9372e2a2c11d 100644 --- a/Source/Core/PolygonPipeline.js +++ b/Source/Core/PolygonPipeline.js @@ -409,40 +409,40 @@ define([ } // Get the nodes from the array - var a1 = pArray[a1i]; - var a2 = pArray[a2i]; + var a1Position = pArray[a1i].position; + var a2Position = pArray[a2i].position; + var length = pArray.length; // Define side and cut vectors - var before = CesiumMath.mod(a1i - 1, pArray.length); + var before = CesiumMath.mod(a1i - 1, length); if (!validateVertex(before, pArray)) { return before; } - var after = CesiumMath.mod(a1i + 1, pArray.length); + var after = CesiumMath.mod(a1i + 1, length); if (!validateVertex(after, pArray)) { return after; } - var s1 = Cartesian2.subtract(pArray[before].position, a1.position, s1Scratch); - var s2 = Cartesian2.subtract(pArray[after].position, a1.position, s2Scratch); - var cut = Cartesian2.subtract(a2.position, a1.position, cutScratch); - if (isParallel(s1, cut)) { // Cut is parallel to s1 + var s1 = Cartesian2.subtract(pArray[before].position, a1Position, s1Scratch); + var s2 = Cartesian2.subtract(pArray[after].position, a1Position, s2Scratch); + var cut = Cartesian2.subtract(a2Position, a1Position, cutScratch); + + var leftEdgeCutZ = crossZ(s1, cut); + var rightEdgeCutZ = crossZ(s2, cut); + + if (leftEdgeCutZ === 0.0) { // cut is parallel to (a1i - 1, a1i) edge return isInternalToParallelSide(s1, cut) ? INTERNAL : EXTERNAL; - } else if (isParallel(s2, cut)) { // Cut is parallel to s2 + } else if (rightEdgeCutZ === 0.0) { // cut is parallel to (a1i + 1, a1i) edge return isInternalToParallelSide(s2, cut) ? INTERNAL : EXTERNAL; - } else if (angleLessThan180(s1, s2)) { // Angle at point is less than 180 - if (isInsideSmallAngle(s1, s2, cut)) { // Cut is in-between sides - return INTERNAL; - } - - return EXTERNAL; - } else if (angleGreaterThan180(s1, s2)) { // Angle at point is greater than 180 - if (isInsideBigAngle(s1, s2, cut)) { // Cut is in-between sides - return EXTERNAL; + } else { + var z = crossZ(s1, s2); + if (z < 0.0) { // angle at a1i is less than 180 degrees + return leftEdgeCutZ < 0.0 && rightEdgeCutZ > 0.0 ? INTERNAL : EXTERNAL; // Cut is in-between sides + } else if (z > 0.0) { // angle at a1i is greater than 180 degrees + return leftEdgeCutZ > 0.0 && rightEdgeCutZ < 0.0 ? EXTERNAL : INTERNAL; // Cut is in-between sides } - - return INTERNAL; } } @@ -511,106 +511,39 @@ define([ * * @private */ - var vvScratch1 = new Cartesian3(); - var vvScratch2 = new Cartesian3(); function validateVertex(index, pArray) { - var before = index - 1; - var after = index + 1; - if (before < 0) { - before = pArray.length - 1; - } - if (after === pArray.length) { - after = 0; - } - - var s1 = Cartesian2.subtract(pArray[before].position, pArray[index].position, vvScratch1); - var s2 = Cartesian2.subtract(pArray[after].position, pArray[index].position, vvScratch2); + var length = pArray.length; + var before = CesiumMath.mod(index - 1, length); + var after = CesiumMath.mod(index + 1, length); - if (isParallel(s1, s2)) { + // check if adjacent edges are parallel + if (indexedEdgeCrossZ(before, after, index, pArray) === 0.0) { return false; } return true; } - /** - * Determine whether s1 and s2 are parallel. - * - * @param {Cartesian3} s1 - * @param {Cartesian3} s2 - * @returns {Boolean} - * - * @private - */ - var parallelScratch = new Cartesian3(); - function isParallel(s1, s2) { - return Cartesian3.cross(s1, s2, parallelScratch).z === 0.0; - } + function indexedEdgeCrossZ(p0Index, p1Index, vertexIndex, array) { + var p0 = array[p0Index].position; + var p1 = array[p1Index].position; + var v = array[vertexIndex].position; - /** - * Assuming s1 is to the left of s2, determine whether - * the angle between them is less than 180 degrees. - * - * @param {Cartesian3} s1 - * @param {Cartesian3} s2 - * @returns {Boolean} - * - * @private - */ - var lessThanScratch = new Cartesian3(); - function angleLessThan180(s1, s2) { - return Cartesian3.cross(s1, s2, lessThanScratch).z < 0.0; - } + var vx = v.x; + var vy = v.y; - /** - * Assuming s1 is to the left of s2, determine whether - * the angle between them is greater than 180 degrees. - * - * @param {Cartesian3} s1 - * @param {Cartesian3} s2 - * @returns {Boolean} - * - * @private - */ - var greaterThanScratch = new Cartesian3(); - function angleGreaterThan180(s1, s2) { - return Cartesian3.cross(s1, s2, greaterThanScratch).z > 0.0; - } + // (p0 - v).cross(p1 - v).z + var leftX = p0.x - vx; + var leftY = p0.y - vy; + var rightX = p1.x - vx; + var rightY = p1.y - vy; - /** - * Determines whether s3 is inside the greater-than-180-degree angle - * between s1 and s2. - * - * Important: s1 must be to the left of s2. - * - * @param {Cartesian3} s1 - * @param {Cartesian3} s2 - * @param {Cartesian3} s3 - * @returns {Boolean} - * - * @private - */ - var insideBigAngleScratch = new Cartesian3(); - function isInsideBigAngle(s1, s2, s3) { - return (Cartesian3.cross(s1, s3, insideBigAngleScratch).z > 0.0) && (Cartesian3.cross(s3, s2, insideBigAngleScratch).z > 0.0); + return leftX * rightY - leftY * rightX; } - /** - * Determines whether s3 is inside the less-than-180-degree angle - * between s1 and s2. - * - * Important: s1 must be to the left of s2. - * - * @param {Cartesian3} s1 - * @param {Cartesian3} s2 - * @param {Cartesian3} s3 - * @returns {Boolean} - * - * @private - */ - var insideSmallAngleScratch = new Cartesian3(); - function isInsideSmallAngle(s1, s2, s3) { - return (Cartesian3.cross(s1, s3, insideSmallAngleScratch).z < 0.0) && (Cartesian3.cross(s3, s2, insideSmallAngleScratch).z < 0.0); + function crossZ(p0, p1) { + // p0.cross(p1).z + return p0.x * p1.y - p0.y * p1.x; } /** @@ -677,19 +610,9 @@ define([ return false; } - var side1Scratch = new Cartesian3(); - var side2Scratch = new Cartesian3(); function triangleInLine(pArray) { - // Get two sides - var v1 = pArray[0].position; - var v2 = pArray[1].position; - var v3 = pArray[2].position; - - var side1 = Cartesian2.subtract(v2, v1, side1Scratch); - var side2 = Cartesian2.subtract(v3, v1, side2Scratch); - - // If they're parallel, so is the last - return isParallel(side1, side2); + // Get two sides. If they're parallel, so is the last. + return indexedEdgeCrossZ(1, 2, 0, pArray) === 0.0; } /** From 041e491afc595f897c0a18e0d36b6a5e383ee7de Mon Sep 17 00:00:00 2001 From: Dan Bagnell Date: Tue, 25 Nov 2014 14:05:26 -0500 Subject: [PATCH 04/10] Move functions so they are defined before they are used. --- Source/Core/PolygonPipeline.js | 245 ++++++++++++++++----------------- 1 file changed, 122 insertions(+), 123 deletions(-) diff --git a/Source/Core/PolygonPipeline.js b/Source/Core/PolygonPipeline.js index 9372e2a2c11d..0f80621bad5a 100644 --- a/Source/Core/PolygonPipeline.js +++ b/Source/Core/PolygonPipeline.js @@ -348,102 +348,49 @@ define([ } var rseed = 0; - var INTERNAL = -1; - var EXTERNAL = -2; - - var CLEAN_CUT = -1; - var INVALID_CUT = -2; + function indexedEdgeCrossZ(p0Index, p1Index, vertexIndex, array) { + var p0 = array[p0Index].position; + var p1 = array[p1Index].position; + var v = array[vertexIndex].position; - /** - * Determine whether a cut between two polygon vertices is clean. - * - * @param {Number} a1i Index of first vertex. - * @param {Number} a2i Index of second vertex. - * @param {Array} pArray Array of { position, index } objects representing the polygon. - * @returns {Number} If CLEAN_CUT, a cut from the first vertex to the second is internal and does not cross any other sides. - * If INVALID_CUT, then the vertices were valid but a cut could not be made. If the value is greater than or equal to zero, - * then the value is the index of an invalid vertex. - * - * @private - */ - function cleanCut(a1i, a2i, pArray) { - var internalCut12 = internalCut(a1i, a2i, pArray); - if (internalCut12 >= 0) { - return internalCut12; - } + var vx = v.x; + var vy = v.y; - var internalCut21 = internalCut(a2i, a1i, pArray); - if (internalCut21 >= 0) { - return internalCut21; - } + // (p0 - v).cross(p1 - v).z + var leftX = p0.x - vx; + var leftY = p0.y - vy; + var rightX = p1.x - vx; + var rightY = p1.y - vy; - if (internalCut12 === INTERNAL && internalCut21 === INTERNAL && - !intersectsSide(pArray[a1i].position, pArray[a2i].position, pArray) && - !Cartesian2.equals(pArray[a1i].position, pArray[a2i].position)) { - return CLEAN_CUT; - } + return leftX * rightY - leftY * rightX; + } - return INVALID_CUT; + function crossZ(p0, p1) { + // p0.cross(p1).z + return p0.x * p1.y - p0.y * p1.x; } /** - * Determine whether the cut formed between the two vertices is internal - * to the angle formed by the sides connecting at the first vertex. + * Checks to make sure vertex is not superfluous. * - * @param {Number} a1i Index of first vertex. - * @param {Number} a2i Index of second vertex. - * @param {Array} pArray Array of { position, index } objects representing the polygon. - * @returns {Number} If INTERNAL, the cut formed between the two vertices is internal to the angle at vertex 1. - * If EXTERNAL, then the cut formed between the two vertices is external to the angle at vertex 1. If the value - * is greater than or equal to zero, then the value is the index of an invalid vertex. + * @param {Number} index Index of vertex. + * @param {Number} pArray Array of vertices. + * + * @exception {DeveloperError} Superfluous vertex found. * * @private */ - var s1Scratch = new Cartesian3(); - var s2Scratch = new Cartesian3(); - var cutScratch = new Cartesian3(); - function internalCut(a1i, a2i, pArray) { - // Make sure vertex is valid - if (!validateVertex(a1i, pArray)) { - return a1i; - } - - // Get the nodes from the array - var a1Position = pArray[a1i].position; - var a2Position = pArray[a2i].position; + function validateVertex(index, pArray) { var length = pArray.length; + var before = CesiumMath.mod(index - 1, length); + var after = CesiumMath.mod(index + 1, length); - // Define side and cut vectors - var before = CesiumMath.mod(a1i - 1, length); - if (!validateVertex(before, pArray)) { - return before; - } - - var after = CesiumMath.mod(a1i + 1, length); - if (!validateVertex(after, pArray)) { - return after; + // check if adjacent edges are parallel + if (indexedEdgeCrossZ(before, after, index, pArray) === 0.0) { + return false; } - - var s1 = Cartesian2.subtract(pArray[before].position, a1Position, s1Scratch); - var s2 = Cartesian2.subtract(pArray[after].position, a1Position, s2Scratch); - var cut = Cartesian2.subtract(a2Position, a1Position, cutScratch); - - var leftEdgeCutZ = crossZ(s1, cut); - var rightEdgeCutZ = crossZ(s2, cut); - - if (leftEdgeCutZ === 0.0) { // cut is parallel to (a1i - 1, a1i) edge - return isInternalToParallelSide(s1, cut) ? INTERNAL : EXTERNAL; - } else if (rightEdgeCutZ === 0.0) { // cut is parallel to (a1i + 1, a1i) edge - return isInternalToParallelSide(s2, cut) ? INTERNAL : EXTERNAL; - } else { - var z = crossZ(s1, s2); - if (z < 0.0) { // angle at a1i is less than 180 degrees - return leftEdgeCutZ < 0.0 && rightEdgeCutZ > 0.0 ? INTERNAL : EXTERNAL; // Cut is in-between sides - } else if (z > 0.0) { // angle at a1i is greater than 180 degrees - return leftEdgeCutZ > 0.0 && rightEdgeCutZ < 0.0 ? EXTERNAL : INTERNAL; // Cut is in-between sides - } - } + return true; } /** @@ -501,49 +448,82 @@ define([ return Cartesian2.magnitude(cut) < Cartesian2.magnitude(side); } + var INTERNAL = -1; + var EXTERNAL = -2; + /** - * Checks to make sure vertex is not superfluous. - * - * @param {Number} index Index of vertex. - * @param {Number} pArray Array of vertices. + * Determine whether the cut formed between the two vertices is internal + * to the angle formed by the sides connecting at the first vertex. * - * @exception {DeveloperError} Superfluous vertex found. + * @param {Number} a1i Index of first vertex. + * @param {Number} a2i Index of second vertex. + * @param {Array} pArray Array of { position, index } objects representing the polygon. + * @returns {Number} If INTERNAL, the cut formed between the two vertices is internal to the angle at vertex 1. + * If EXTERNAL, then the cut formed between the two vertices is external to the angle at vertex 1. If the value + * is greater than or equal to zero, then the value is the index of an invalid vertex. * * @private */ - function validateVertex(index, pArray) { + var s1Scratch = new Cartesian3(); + var s2Scratch = new Cartesian3(); + var cutScratch = new Cartesian3(); + function internalCut(a1i, a2i, pArray) { + // Make sure vertex is valid + if (!validateVertex(a1i, pArray)) { + return a1i; + } + + // Get the nodes from the array + var a1Position = pArray[a1i].position; + var a2Position = pArray[a2i].position; var length = pArray.length; - var before = CesiumMath.mod(index - 1, length); - var after = CesiumMath.mod(index + 1, length); - // check if adjacent edges are parallel - if (indexedEdgeCrossZ(before, after, index, pArray) === 0.0) { - return false; + // Define side and cut vectors + var before = CesiumMath.mod(a1i - 1, length); + if (!validateVertex(before, pArray)) { + return before; } - return true; - } - - function indexedEdgeCrossZ(p0Index, p1Index, vertexIndex, array) { - var p0 = array[p0Index].position; - var p1 = array[p1Index].position; - var v = array[vertexIndex].position; + var after = CesiumMath.mod(a1i + 1, length); + if (!validateVertex(after, pArray)) { + return after; + } - var vx = v.x; - var vy = v.y; + var s1 = Cartesian2.subtract(pArray[before].position, a1Position, s1Scratch); + var s2 = Cartesian2.subtract(pArray[after].position, a1Position, s2Scratch); + var cut = Cartesian2.subtract(a2Position, a1Position, cutScratch); - // (p0 - v).cross(p1 - v).z - var leftX = p0.x - vx; - var leftY = p0.y - vy; - var rightX = p1.x - vx; - var rightY = p1.y - vy; + var leftEdgeCutZ = crossZ(s1, cut); + var rightEdgeCutZ = crossZ(s2, cut); - return leftX * rightY - leftY * rightX; + if (leftEdgeCutZ === 0.0) { // cut is parallel to (a1i - 1, a1i) edge + return isInternalToParallelSide(s1, cut) ? INTERNAL : EXTERNAL; + } else if (rightEdgeCutZ === 0.0) { // cut is parallel to (a1i + 1, a1i) edge + return isInternalToParallelSide(s2, cut) ? INTERNAL : EXTERNAL; + } else { + var z = crossZ(s1, s2); + if (z < 0.0) { // angle at a1i is less than 180 degrees + return leftEdgeCutZ < 0.0 && rightEdgeCutZ > 0.0 ? INTERNAL : EXTERNAL; // Cut is in-between sides + } else if (z > 0.0) { // angle at a1i is greater than 180 degrees + return leftEdgeCutZ > 0.0 && rightEdgeCutZ < 0.0 ? EXTERNAL : INTERNAL; // Cut is in-between sides + } + } } - function crossZ(p0, p1) { - // p0.cross(p1).z - return p0.x * p1.y - p0.y * p1.x; + /** + * Determine whether number is between n1 and n2. + * Do not include number === n1 or number === n2. + * Do include n1 === n2 === number. + * + * @param {Number} number The number tested. + * @param {Number} n1 First bound. + * @param {Number} n2 Secound bound. + * @returns {Boolean} number is between n1 and n2. + * + * @private + */ + function isBetween(number, n1, n2) { + return ((number > n1 || number > n2) && (number < n1 || number < n2)) || (n1 === n2 && n1 === number); } /** @@ -610,25 +590,44 @@ define([ return false; } - function triangleInLine(pArray) { - // Get two sides. If they're parallel, so is the last. - return indexedEdgeCrossZ(1, 2, 0, pArray) === 0.0; - } + var CLEAN_CUT = -1; + var INVALID_CUT = -2; /** - * Determine whether number is between n1 and n2. - * Do not include number === n1 or number === n2. - * Do include n1 === n2 === number. + * Determine whether a cut between two polygon vertices is clean. * - * @param {Number} number The number tested. - * @param {Number} n1 First bound. - * @param {Number} n2 Secound bound. - * @returns {Boolean} number is between n1 and n2. + * @param {Number} a1i Index of first vertex. + * @param {Number} a2i Index of second vertex. + * @param {Array} pArray Array of { position, index } objects representing the polygon. + * @returns {Number} If CLEAN_CUT, a cut from the first vertex to the second is internal and does not cross any other sides. + * If INVALID_CUT, then the vertices were valid but a cut could not be made. If the value is greater than or equal to zero, + * then the value is the index of an invalid vertex. * * @private */ - function isBetween(number, n1, n2) { - return ((number > n1 || number > n2) && (number < n1 || number < n2)) || (n1 === n2 && n1 === number); + function cleanCut(a1i, a2i, pArray) { + var internalCut12 = internalCut(a1i, a2i, pArray); + if (internalCut12 >= 0) { + return internalCut12; + } + + var internalCut21 = internalCut(a2i, a1i, pArray); + if (internalCut21 >= 0) { + return internalCut21; + } + + if (internalCut12 === INTERNAL && internalCut21 === INTERNAL && + !intersectsSide(pArray[a1i].position, pArray[a2i].position, pArray) && + !Cartesian2.equals(pArray[a1i].position, pArray[a2i].position)) { + return CLEAN_CUT; + } + + return INVALID_CUT; + } + + function triangleInLine(pArray) { + // Get two sides. If they're parallel, so is the last. + return indexedEdgeCrossZ(1, 2, 0, pArray) === 0.0; } /** From fbf6028b79da2373adebf6841efe1fa153eb7677 Mon Sep 17 00:00:00 2001 From: Dan Bagnell Date: Tue, 25 Nov 2014 16:19:48 -0500 Subject: [PATCH 05/10] Remove isNaN checks. Instead, prevent divide by zero. --- Source/Core/PolygonPipeline.js | 38 +++++++++++++++++++------------ Specs/Core/PolygonPipelineSpec.js | 1 + 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/Source/Core/PolygonPipeline.js b/Source/Core/PolygonPipeline.js index 0f80621bad5a..50d627051114 100644 --- a/Source/Core/PolygonPipeline.js +++ b/Source/Core/PolygonPipeline.js @@ -445,7 +445,7 @@ define([ * @private */ function isInternalToParallelSide(side, cut) { - return Cartesian2.magnitude(cut) < Cartesian2.magnitude(side); + return Cartesian2.magnitudeSquared(cut) < Cartesian2.magnitudeSquared(side); } var INTERNAL = -1; @@ -537,35 +537,45 @@ define([ * @private */ var intersectionScratch = new Cartesian2(); + function intersectsSide(a1, a2, pArray) { - for ( var i = 0; i < pArray.length; i++) { + var axDiff = a2.x - a1.x; + var aVertical = Math.abs(axDiff) < CesiumMath.EPSILON15; + + var slopeA; + if (!aVertical){ + slopeA = (a2.y - a1.y) / axDiff; + } + + var length = pArray.length; + for (var i = 0; i < length; i++) { var b1 = pArray[i].position; - var b2; - if (i < pArray.length - 1) { - b2 = pArray[i + 1].position; - } else { - b2 = pArray[0].position; - } + var b2 = pArray[CesiumMath.mod(i + 1, length)].position; // If there's a duplicate point, there's no intersection here. if (Cartesian2.equals(a1, b1) || Cartesian2.equals(a2, b2) || Cartesian2.equals(a1, b2) || Cartesian2.equals(a2, b1)) { continue; } - // Slopes (NaN means vertical) - var slopeA = (a2.y - a1.y) / (a2.x - a1.x); - var slopeB = (b2.y - b1.y) / (b2.x - b1.x); + // Slopes + var bxDiff = b2.x - b1.x; + var bVertical = Math.abs(bxDiff) < CesiumMath.EPSILON15; + + var slopeB; + if (!bVertical){ + slopeB = (b2.y - b1.y) / bxDiff; + } // If parallel, no intersection - if (slopeA === slopeB || (isNaN(slopeA) && isNaN(slopeB))) { + if (slopeA === slopeB || (aVertical && bVertical)) { continue; } // Calculate intersection point var intX; - if (isNaN(slopeA)) { + if (aVertical) { intX = a1.x; - } else if (isNaN(slopeB)) { + } else if (bVertical) { intX = b1.x; } else { intX = (a1.y - b1.y - slopeA * a1.x + slopeB * b1.x) / (slopeB - slopeA); diff --git a/Specs/Core/PolygonPipelineSpec.js b/Specs/Core/PolygonPipelineSpec.js index b7b1023613ec..0e8505e837ac 100644 --- a/Specs/Core/PolygonPipelineSpec.js +++ b/Specs/Core/PolygonPipelineSpec.js @@ -219,6 +219,7 @@ defineSuite([ expect(indices).toEqual([ 0, 1, 7, 1, 2, 7, 2, 3, 7, 3, 6, 7, 3, 5, 6, 3, 4, 5 ]); }); + it('triangulates an even more complicated concave', function() { var positions = [new Cartesian2(0,0), new Cartesian2(1,0), new Cartesian2(1, -1), new Cartesian2(2, -1.4), new Cartesian2(40, 2), new Cartesian2(10, 5), new Cartesian2(30, 10), new Cartesian2(25, 20), From 29c212f1b00b80ad66d8a849af3e781713140fe0 Mon Sep 17 00:00:00 2001 From: Dan Bagnell Date: Tue, 25 Nov 2014 18:17:28 -0500 Subject: [PATCH 06/10] Solve edge intersections using parametric line equations instead of a form requiring the slope which breaks down for vertical lines. --- Source/Core/PolygonPipeline.js | 58 ++++++++++++++++------------------ 1 file changed, 27 insertions(+), 31 deletions(-) diff --git a/Source/Core/PolygonPipeline.js b/Source/Core/PolygonPipeline.js index 50d627051114..7bcc40e59662 100644 --- a/Source/Core/PolygonPipeline.js +++ b/Source/Core/PolygonPipeline.js @@ -526,6 +526,25 @@ define([ return ((number > n1 || number > n2) && (number < n1 || number < n2)) || (n1 === n2 && n1 === number); } + var sqrEpsilon = CesiumMath.EPSILON14; + var eScratch = new Cartesian2(); + + function linesIntersection(p0, d0, p1, d1) { + var e = Cartesian2.subtract(p1, p0, eScratch); + var cross = d0.x * d1.y - d0.y * d1.x; + var sqrCross = cross * cross; + var sqrLen0 = Cartesian2.magnitudeSquared(d0); + var sqrLen1 = Cartesian2.magnitudeSquared(d1); + if (sqrCross > sqrEpsilon * sqrLen0 * sqrLen1) { + // lines of the segments are not parallel + var s = (e.x * d1.y - e.y * d1.x) / cross; + return Cartesian2.add(p0, Cartesian2.multiplyByScalar(d0, s, eScratch), eScratch); + } + + // lines of the segments are parallel (they cannot be the same line) + return undefined; + } + /** * Determine whether this segment intersects any other polygon sides. * @@ -537,15 +556,11 @@ define([ * @private */ var intersectionScratch = new Cartesian2(); + var aDirectionScratch = new Cartesian2(); + var bDirectionScratch = new Cartesian2(); function intersectsSide(a1, a2, pArray) { - var axDiff = a2.x - a1.x; - var aVertical = Math.abs(axDiff) < CesiumMath.EPSILON15; - - var slopeA; - if (!aVertical){ - slopeA = (a2.y - a1.y) / axDiff; - } + var aDirection = Cartesian2.subtract(a2, a1, aDirectionScratch); var length = pArray.length; for (var i = 0; i < length; i++) { @@ -557,39 +572,20 @@ define([ continue; } - // Slopes - var bxDiff = b2.x - b1.x; - var bVertical = Math.abs(bxDiff) < CesiumMath.EPSILON15; - - var slopeB; - if (!bVertical){ - slopeB = (b2.y - b1.y) / bxDiff; - } - - // If parallel, no intersection - if (slopeA === slopeB || (aVertical && bVertical)) { + var bDirection = Cartesian2.subtract(b2, b1, bDirectionScratch); + var intersection = linesIntersection(a1, aDirection, b1, bDirection); + if (!defined(intersection)) { continue; } - // Calculate intersection point - var intX; - if (aVertical) { - intX = a1.x; - } else if (bVertical) { - intX = b1.x; - } else { - intX = (a1.y - b1.y - slopeA * a1.x + slopeB * b1.x) / (slopeB - slopeA); - } - var intY = slopeA * intX + a1.y - slopeA * a1.x; - - var intersection = Cartesian2.fromElements(intX, intY, intersectionScratch); - // If intersection is on an endpoint, count no intersection if (Cartesian2.equals(intersection, a1) || Cartesian2.equals(intersection, a2) || Cartesian2.equals(intersection, b1) || Cartesian2.equals(intersection, b2)) { continue; } // Is intersection point between segments? + var intX = intersection.x; + var intY = intersection.y; var intersects = isBetween(intX, a1.x, a2.x) && isBetween(intY, a1.y, a2.y) && isBetween(intX, b1.x, b2.x) && isBetween(intY, b1.y, b2.y); // If intersecting, the cut is not clean From 2d286af9b50da7e722459651290e61e33e448d62 Mon Sep 17 00:00:00 2001 From: Dan Bagnell Date: Wed, 3 Dec 2014 13:56:07 -0500 Subject: [PATCH 07/10] Change the triangulation RNG to the Mersenne Twister. --- Source/Core/PolygonPipeline.js | 21 +------------------- Specs/Core/PolygonPipelineSpec.js | 33 +++++++++++++------------------ 2 files changed, 15 insertions(+), 39 deletions(-) diff --git a/Source/Core/PolygonPipeline.js b/Source/Core/PolygonPipeline.js index 9dc0cc7a22f3..4ed905156902 100644 --- a/Source/Core/PolygonPipeline.js +++ b/Source/Core/PolygonPipeline.js @@ -329,24 +329,14 @@ define([ return newPolygonVertices; } - /** - * Use seeded pseudo-random number to be testable. - * - * @param {Number} length - * @returns {Number} Random integer from 0 to length - 1 - * - * @private - */ function getRandomIndex(length) { - var random = '0.' + Math.sin(rseed).toString().substr(5); - rseed += 0.2; + var random = CesiumMath.nextRandomNumber(); var i = Math.floor(random * length); if (i === length) { i--; } return i; } - var rseed = 0; function indexedEdgeCrossZ(p0Index, p1Index, vertexIndex, array) { var p0 = array[p0Index].position; @@ -814,15 +804,6 @@ define([ return randomChop(nodeArray); }; - /** - * This function is used for predictable testing. - * - * @private - */ - PolygonPipeline.resetSeed = function(seed) { - rseed = defaultValue(seed, 0); - }; - /** * Subdivides positions and raises points to the surface of the ellipsoid. * diff --git a/Specs/Core/PolygonPipelineSpec.js b/Specs/Core/PolygonPipelineSpec.js index 0e8505e837ac..c8ae5b9ad2b4 100644 --- a/Specs/Core/PolygonPipelineSpec.js +++ b/Specs/Core/PolygonPipelineSpec.js @@ -4,18 +4,20 @@ defineSuite([ 'Core/Cartesian2', 'Core/Cartesian3', 'Core/Ellipsoid', + 'Core/Math', 'Core/WindingOrder' ], function( PolygonPipeline, Cartesian2, Cartesian3, Ellipsoid, + CesiumMath, WindingOrder) { "use strict"; /*global jasmine,describe,xdescribe,it,xit,expect,beforeEach,afterEach,beforeAll,afterAll,spyOn,runs,waits,waitsFor*/ beforeEach(function() { - PolygonPipeline.resetSeed(); + CesiumMath.setRandomNumberSeed(0.0); }); it('removeDuplicates removes duplicate points', function() { @@ -170,10 +172,7 @@ defineSuite([ new Cartesian2(2,6), new Cartesian2(6,6), new Cartesian2(6,9), new Cartesian2(0,9)]; var indices = PolygonPipeline.triangulate(positions); - expect(indices).toEqual([ 0, 4, 7, 0, 3, 4, 0, 2, 3, 4, 6, 7, 4, 5, 6, 0, 1, 2 ]); - - indices = PolygonPipeline.triangulate(positions); - expect(indices).toEqual([ 0, 3, 7, 0, 1, 3, 1, 2, 3, 3, 4, 7, 4, 5, 7, 5, 6, 7 ]); + expect(indices).toEqual([ 0, 3, 7, 0, 2, 3, 0, 1, 2, 3, 4, 7, 4, 6, 7, 4, 5, 6 ]); /* Do it a few times to make sure we never get stuck on it */ for (var i = 0; i < 30; i++) { @@ -195,14 +194,11 @@ defineSuite([ * 0 1 */ it('triangulates a convex polygon with vertical and horizontal sides', function() { - var positions = [new Cartesian2(0,0), new Cartesian2(6,0), new Cartesian2(6,3), new Cartesian2(2,3), - new Cartesian2(2,6), new Cartesian2(6,6), new Cartesian2(6,9), new Cartesian2(0,9)]; + var positions = [new Cartesian2(0,0), new Cartesian2(6,0), new Cartesian2(6,3), new Cartesian2(8,3), + new Cartesian2(8,6), new Cartesian2(6,6), new Cartesian2(6,9), new Cartesian2(0,9)]; var indices = PolygonPipeline.triangulate(positions); - expect(indices).toEqual([ 0, 4, 7, 0, 3, 4, 0, 2, 3, 4, 6, 7, 4, 5, 6, 0, 1, 2 ]); - - indices = PolygonPipeline.triangulate(positions); - expect(indices).toEqual([ 0, 3, 7, 0, 1, 3, 1, 2, 3, 3, 4, 7, 4, 5, 7, 5, 6, 7 ]); + expect(indices).toEqual([ 0, 2, 7, 0, 1, 2, 2, 3, 7, 3, 5, 7, 5, 6, 7, 3, 4, 5 ]); /* Do it a few times to make sure we never get stuck on it */ for (var i = 0; i < 30; i++) { @@ -216,7 +212,6 @@ defineSuite([ new Cartesian2(0.0, 1.0), new Cartesian2(1.9, 0.5)]; var indices = PolygonPipeline.triangulate(positions); - expect(indices).toEqual([ 0, 1, 7, 1, 2, 7, 2, 3, 7, 3, 6, 7, 3, 5, 6, 3, 4, 5 ]); }); @@ -225,9 +220,9 @@ defineSuite([ new Cartesian2(40, 2), new Cartesian2(10, 5), new Cartesian2(30, 10), new Cartesian2(25, 20), new Cartesian2(20,20), new Cartesian2(10,15), new Cartesian2(15, 10), new Cartesian2(8, 10), new Cartesian2(-1, 3)]; - var indices = PolygonPipeline.triangulate(positions); - expect(indices).toEqual([ 0, 11, 12, 0, 10, 11, 0, 5, 10, 5, 6, 10, 6, 8, 10, 8, 9, 10, 6, 7, 8, 0, 4, 5, 0, 1, 4, 1, 3, 4, 1, 2, 3 ]); + var indices = PolygonPipeline.triangulate(positions); + expect(indices).toEqual([ 0, 1, 12, 1, 5, 12, 1, 4, 5, 1, 2, 4, 5, 11, 12, 2, 3, 4, 5, 10, 11, 5, 6, 10, 6, 9, 10, 6, 7, 9, 7, 8, 9 ]); /* Try it a bunch of times to make sure we can never get stuck on it */ for (var i = 0; i < 50; i++) { @@ -246,9 +241,9 @@ defineSuite([ */ it('triangulates a polygon with a side that intersects on of its other vertices', function() { var positions = [new Cartesian2(0,0), new Cartesian2(0, -5), new Cartesian2(2,0), new Cartesian2(4, -5), new Cartesian2(4, 0)]; - var indices = PolygonPipeline.triangulate(positions); - expect(indices).toEqual([ 2, 3, 4, 0, 1, 2 ]); + var indices = PolygonPipeline.triangulate(positions); + expect(indices).toEqual([ 0, 1, 2, 2, 3, 4 ]); }); /* @@ -263,8 +258,8 @@ defineSuite([ it('triangulates a polygon with a side that intersects on of its other vertices and superfluous vertices', function() { var positions = [ new Cartesian2(0,0), new Cartesian2(2, -5), new Cartesian2(4,0), new Cartesian2(6, -5), new Cartesian2(8, 0), new Cartesian2(6,0), new Cartesian2(2,0) ]; - var indices = PolygonPipeline.triangulate(positions); + var indices = PolygonPipeline.triangulate(positions); expect(indices).toEqual([ 0, 1, 2, 2, 3, 4 ]); }); @@ -281,9 +276,9 @@ defineSuite([ it('triangulations a polygon with a "tucked" vertex', function() { var positions = [new Cartesian2(0,0), new Cartesian2(5,0), new Cartesian2(5,2), new Cartesian2(1, 2), new Cartesian2(3, 2), new Cartesian2(3,4), new Cartesian2(0,4)]; - var indices = PolygonPipeline.triangulate(positions); - expect(indices).toEqual([ 0, 4, 6, 0, 2, 4, 4, 5, 6, 0, 1, 2 ]); + var indices = PolygonPipeline.triangulate(positions); + expect(indices).toEqual([ 0, 1, 6, 1, 4, 6, 1, 2, 4, 4, 5, 6 ]); }); it('throws without positions', function() { From 76279d54b93bedc1369910da35aad4d033f5076d Mon Sep 17 00:00:00 2001 From: Dan Bagnell Date: Wed, 3 Dec 2014 15:33:31 -0500 Subject: [PATCH 08/10] Update CHANGES.md. --- CHANGES.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index b71d53971cbb..f96ed2de988c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,10 @@ Change Log ========== +### 1.5 - 2015-01-05 + +* Improved polygon triangulation performance. + ### 1.4 - 2014-12-01 * Breaking changes From 1a8cd757c68917dee797b89613279f799dcb1883 Mon Sep 17 00:00:00 2001 From: Dan Bagnell Date: Wed, 3 Dec 2014 15:36:48 -0500 Subject: [PATCH 09/10] Tweak CHANGES.md. --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index f96ed2de988c..d9b45b386393 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,7 @@ Change Log ### 1.5 - 2015-01-05 * Improved polygon triangulation performance. +* Added `Math.mod` which computes `m % n` but also works when `m` is negative. ### 1.4 - 2014-12-01 From 6720def7d9c7b832a01a905631c5e272ae1693fa Mon Sep 17 00:00:00 2001 From: Patrick Cozzi Date: Wed, 3 Dec 2014 16:32:23 -0500 Subject: [PATCH 10/10] Tweak wording. --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index d9b45b386393..ffd8e9fd0d9b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,7 +3,7 @@ Change Log ### 1.5 - 2015-01-05 -* Improved polygon triangulation performance. +* Improved polygon loading performance. * Added `Math.mod` which computes `m % n` but also works when `m` is negative. ### 1.4 - 2014-12-01