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 @@
+
+
+@@@
+
+