From e1060df7dc4b4afc63d6cc86fc41748a4940918d Mon Sep 17 00:00:00 2001 From: GreLI Date: Sat, 21 Mar 2015 23:52:53 +0300 Subject: [PATCH] Check path intersection while merging. Fixes #253, resolves #260, fixes #298. --- plugins/_path.js | 288 ++++++++++++++++++++++++++++++++- plugins/mergePaths.js | 5 +- test/plugins/mergePaths.01.svg | 4 +- test/plugins/mergePaths.02.svg | 6 + test/plugins/mergePaths.03.svg | 44 +++++ test/plugins/mergePaths.04.svg | 10 ++ 6 files changed, 351 insertions(+), 6 deletions(-) create mode 100644 test/plugins/mergePaths.03.svg create mode 100644 test/plugins/mergePaths.04.svg diff --git a/plugins/_path.js b/plugins/_path.js index 3913ba2c..3d53746f 100644 --- a/plugins/_path.js +++ b/plugins/_path.js @@ -9,7 +9,8 @@ var regPathInstructions = /([MmLlHhVvCcSsQqTtAaZz])\s*/, referencesProps = collections.referencesProps, defaultStrokeWidth = collections.attrsGroupsDefaults.presentation['stroke-width'], cleanupOutData = require('../lib/svgo/tools').cleanupOutData, - removeLeadingZero = require('../lib/svgo/tools').removeLeadingZero; + removeLeadingZero = require('../lib/svgo/tools').removeLeadingZero, + prevCtrlPoint; /** * Convert path string to JS representation. @@ -311,7 +312,6 @@ exports.applyTransforms = function(elem, path, applyTransformsStroked, floatPrec elem.removeAttr('transform'); return path; - }; /** @@ -590,6 +590,290 @@ function set(dest, source) { return dest; } +/** + * Checks if two paths have an intersection by checking convex hulls + * collision using Gilbert-Johnson-Keerthi distance algorithm + * http://entropyinteractive.com/2011/04/gjk-algorithm/ + * + * @param {Array} path1 JS path representation + * @param {Array} path2 JS path representation + * @return {Boolean} + */ +exports.interesects = function(path1, path2) { + if (path1.length < 3 || path2.length < 3) return false; // nothing to fill + + // Collect points of every subpath. + var points1 = relative2absolute(path1).reduce(gatherPoints, []), + points2 = relative2absolute(path2).reduce(gatherPoints, []); + + // Axis-aligned bounding box check. + if (points1.maxX <= points2.minX || points2.maxX <= points1.minX || + points1.maxY <= points2.minY || points2.maxY <= points1.minY || + points1.every(function (set1) { + return points2.every(function (set2) { + return set1[set1.maxX][0] <= set2[set2.minX][0] || + set2[set2.maxX][0] <= set1[set1.minX][0] || + set1[set1.maxY][1] <= set2[set2.minY][1] || + set2[set2.maxY][1] <= set1[set1.minY][1]; + }); + }) + ) return false; + + // Get a convex hull from points of each subpath. Has the most complexity O(n·log n). + var hullNest1 = points1.map(convexHull), + hullNest2 = points2.map(convexHull); + + // Check intersection of every subpath of the first path with every subpath of the second. + return hullNest1.some(function(hull1) { + if (hull1.length < 3) return false; + + return hullNest2.some(function(hull2) { + if (hull2.length < 3) return false; + + var simplex = [getSupport(hull1, hull2, [1, 0])], // create the initial simplex + direction = minus(simplex[0]); // set the direction to point towards the origin + + var iterations = 1e4; // infinite loop protection, 10 000 iterations is more than enough + while (true) { + if (iterations-- == 0) { + console.error('Error: infinite loop while processing mergePaths plugin.'); + return true; // true is the safe value that means “do nothing with paths” + } + // add a new point + simplex.push(getSupport(hull1, hull2, direction)); + // see if the new point was on the correct side of the origin + if (dot(direction, simplex[simplex.length - 1]) <= 0) return false; + // process the simplex + if (processSimplex(simplex, direction)) return true; + } + }); + }); + + function getSupport(a, b, direction) { + return sub(supportPoint(a, direction), supportPoint(b, minus(direction))); + } + + // Compute farthest polygon point in particular direction. + function supportPoint(polygon, direction) { + // Choose a quadrant to search in. In the worst case only one quadrant would be iterated. + var index = direction[1] >= 0 ? + direction[0] < 0 ? polygon.maxY : polygon.maxX : // [1, 0] lands right on maxX + direction[0] < 0 ? polygon.minX : polygon.minY, + max = dot(polygon[index], direction); + + for (var i = index; i < polygon.length; i++) { + var value = dot(polygon[i], direction); + if (value >= max) { + index = i; + max = value; + } else break; // surely we've found the maximum since we've choosen a quadrant + } + return polygon[index]; + } +}; + +function processSimplex(simplex, direction) { + // we only need to handle to 1-simplex and 2-simplex + if (simplex.length == 2) { // 1-simplex + var a = simplex[1], + b = simplex[0], + AO = minus(simplex[1]), + AB = sub(b, a); + // AO is in the same direction as AB + if (dot(AO, AB) > 0) { + // get the vector perpendicular to AB facing O + set(direction, orth(AB, a)); + } else { + set(direction, AO); + // only A remains in the simplex + simplex.shift(); + } + } else { // 2-simplex + var a = simplex[2], // [a, b, c] = simplex + b = simplex[1], + c = simplex[0], + AB = sub(b, a), + AC = sub(c, a), + AO = minus(a), + ACB = orth(AB, AC), // the vector perpendicular to AB facing away from C + ABC = orth(AC, AB); // the vector perpendicular to AC facing away from B + + if (dot(ACB, AO) > 0) { + if (dot(AB, AO) > 0) { // region 4 + set(direction, ACB); + simplex.shift(); // simplex = [b, a] + } else { // region 5 + set(direction, AO); + simplex.splice(0, 2); // simplex = [a] + } + } else if (dot(ABC, AO) > 0) { + if (dot(AC, AO) > 0) { // region 6 + set(direction, ABC); + simplex.splice(1, 1); // simplex = [c, a] + } else { // region 5 (again) + set(direction, AO); + simplex.splice(0, 2); // simplex = [a] + } + } else // region 7 + return true; + } + return false; +} + +function minus(v) { + return [-v[0], -v[1]]; +} + +function sub(v1, v2) { + return [v1[0] - v2[0], v1[1] - v2[1]]; +} + +function dot(v1, v2) { + return v1[0] * v2[0] + v1[1] * v2[1]; +} + +function orth(v, from) { + var o = [-v[1], v[0]]; + return dot(o, minus(from)) < 0 ? minus(o) : o; +} + +function gatherPoints(points, item, index, path) { + + var subPath = points.length && points[points.length - 1], + prev = index && path[index - 1], + basePoint = subPath.length && subPath[subPath.length - 1], + data = item.data, + ctrlPoint = basePoint; + + switch (item.instruction) { + case 'M': + points.push(subPath = []); + break; + case 'H': + addPoint(subPath, [data[0], basePoint[1]]); + break; + case 'V': + addPoint(subPath, [basePoint[0], data[0]]); + break; + case 'Q': + addPoint(subPath, data.slice(0, 2)); + prevCtrlPoint = [data[2] - data[0], data[3] - data[1]]; // Save control point for shorthand + break; + case 'T': + if (prev.instruction == 'Q' && prev.instruction == 'T') { + ctrlPoint = [basePoint[0] + prevCtrlPoint[0], basePoint[1] + prevCtrlPoint[1]]; + addPoint(subPath, ctrlPoint); + prevCtrlPoint = [data[0] - ctrlPoint[0], data[1] - ctrlPoint[1]]; + } + break; + case 'C': + // Approximate quibic Bezier curve with middle points between control points + addPoint(subPath, [.5 * (basePoint[0] + data[0]), .5 * (basePoint[1] + data[1])]); + addPoint(subPath, [.5 * (data[0] + data[2]), .5 * (data[1] + data[3])]); + addPoint(subPath, [.5 * (data[2] + data[4]), .5 * (data[3] + data[5])]); + prevCtrlPoint = [data[4] - data[2], data[5] - data[3]]; // Save control point for shorthand + break; + case 'S': + if (prev.instruction == 'C' && prev.instruction == 'S') { + addPoint(subPath, [basePoint[0] + .5 * prevCtrlPoint[0], basePoint[1] + .5 * prevCtrlPoint[1]]); + ctrlPoint = [basePoint[0] + prevCtrlPoint[0], basePoint[1] + prevCtrlPoint[1]]; + } + addPoint(subPath, [.5 * (ctrlPoint[0] + data[0]), .5 * (ctrlPoint[1]+ data[1])]); + addPoint(subPath, [.5 * (data[0] + data[2]), .5 * (data[1] + data[3])]); + prevCtrlPoint = [data[2] - data[0], data[3] - data[1]]; + break; + case 'A': + // Convert the arc to bezier curves and use the same approximation + var curves = a2c.apply(0, basePoint.concat(data)); + for (var cData; (cData = curves.splice(0,6).map(toAbsolute)).length;) { + addPoint(subPath, [.5 * (basePoint[0] + cData[0]), .5 * (basePoint[1] + cData[1])]); + addPoint(subPath, [.5 * (cData[0] + cData[2]), .5 * (cData[1] + cData[3])]); + addPoint(subPath, [.5 * (cData[2] + cData[4]), .5 * (cData[3] + cData[5])]); + if (curves.length) addPoint(subPath, basePoint = cData.slice(-2)); + } + break; + } + // Save final command coordinates + if (data && data.length >= 2) addPoint(subPath, data.slice(-2)); + return points; + + function toAbsolute(n, i) { return n + basePoint[i % 2] } + + // Writes data about the extreme points on each axle + function addPoint(path, point) { + if (!path.length || point[1] > path[path.maxY][1]) { + path.maxY = path.length; + points.maxY = points.length ? Math.max(point[1], points.maxY) : point[1]; + } + if (!path.length || point[0] > path[path.maxX][0]) { + path.maxX = path.length; + points.maxX = points.length ? Math.max(point[0], points.maxX) : point[0]; + } + if (!path.length || point[1] < path[path.minY][1]) { + path.minY = path.length; + points.minY = points.length ? Math.min(point[1], points.minY) : point[1]; + } + if (!path.length || point[0] < path[path.minX][0]) { + path.minX = path.length; + points.minX = points.length ? Math.min(point[0], points.minX) : point[0]; + } + path.push(point); + } +} + +/** + * Forms a convex hull from set of points of every subpath using monotone chain convex hull algorithm. + * http://en.wikibooks.org/wiki/Algorithm_Implementation/Geometry/Convex_hull/Monotone_chain + * + * @param points An array of [X, Y] coordinates + */ +function convexHull(points) { + points.sort(function(a, b) { + return a[0] == b[0] ? a[1] - b[1] : a[0] - b[0]; + }); + + var lower = [], + minY = 0; + for (var i = 0; i < points.length; i++) { + while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], points[i]) <= 0) { + lower.pop(); + } + if (points[i][1] < points[minY][1]) { + minY = lower.length; + } + lower.push(points[i]); + } + + var upper = [], + maxY = points.length - 1; + for (var i = points.length ; i--;) { + while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], points[i]) <= 0) { + upper.pop(); + } + if (points[i][1] > points[maxY][1]) { + maxY = upper.length; + } + upper.push(points[i]); + } + + // last points are equal to starting points of the other part + upper.pop(); + lower.pop(); + + var hull = lower.concat(upper); + + hull.minX = 0; // by sorting + hull.maxX = lower.length; + hull.minY = minY; + hull.maxY = (lower.length + maxY) % hull.length; + + return hull; +} + +function cross(o, a, b) { + return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]) +} + /* Based on code from Snap.svg (Apache 2 license). http://snapsvg.io/ * Thanks to Dmitry Baranovskiy for his great work! */ diff --git a/plugins/mergePaths.js b/plugins/mergePaths.js index 7612ad06..f1bebd8f 100644 --- a/plugins/mergePaths.js +++ b/plugins/mergePaths.js @@ -11,7 +11,8 @@ exports.params = { }; var path2js = require('./_path.js').path2js, - js2path = require('./_path.js').js2path; + js2path = require('./_path.js').js2path, + interesects = require('./_path.js').interesects; /** * Merge multiple Paths into one. @@ -51,7 +52,7 @@ exports.fn = function(item, params) { prevPathJS = path2js(prevContentItem), curPathJS = path2js(contentItem); - if (equalData) { + if (equalData && !interesects(prevPathJS, curPathJS)) { js2path(prevContentItem, prevPathJS.concat(curPathJS), params); return false; } diff --git a/test/plugins/mergePaths.01.svg b/test/plugins/mergePaths.01.svg index 64bf585e..ae2f2642 100644 --- a/test/plugins/mergePaths.01.svg +++ b/test/plugins/mergePaths.01.svg @@ -5,7 +5,7 @@ - + @@@ @@ -13,5 +13,5 @@ - + diff --git a/test/plugins/mergePaths.02.svg b/test/plugins/mergePaths.02.svg index 56039dde..28331927 100644 --- a/test/plugins/mergePaths.02.svg +++ b/test/plugins/mergePaths.02.svg @@ -13,6 +13,10 @@ + + + + @@@ @@ -22,4 +26,6 @@ + + diff --git a/test/plugins/mergePaths.03.svg b/test/plugins/mergePaths.03.svg new file mode 100644 index 00000000..ee0613a2 --- /dev/null +++ b/test/plugins/mergePaths.03.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + +@@@ + + + + + + + + + + + + + + + + + + + diff --git a/test/plugins/mergePaths.04.svg b/test/plugins/mergePaths.04.svg new file mode 100644 index 00000000..815b88ab --- /dev/null +++ b/test/plugins/mergePaths.04.svg @@ -0,0 +1,10 @@ + + + + + +@@@ + + + +