Skip to content

Commit

Permalink
Check path intersection while merging.
Browse files Browse the repository at this point in the history
Fixes #253, resolves #260, fixes #298.
  • Loading branch information
GreLI committed Mar 22, 2015
1 parent e220b6c commit e1060df
Show file tree
Hide file tree
Showing 6 changed files with 351 additions and 6 deletions.
288 changes: 286 additions & 2 deletions plugins/_path.js
Expand Up @@ -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.
Expand Down Expand Up @@ -311,7 +312,6 @@ exports.applyTransforms = function(elem, path, applyTransformsStroked, floatPrec
elem.removeAttr('transform');

return path;

};

/**
Expand Down Expand Up @@ -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!
*/
Expand Down
5 changes: 3 additions & 2 deletions plugins/mergePaths.js
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
}
Expand Down
4 changes: 2 additions & 2 deletions test/plugins/mergePaths.01.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions test/plugins/mergePaths.02.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit e1060df

Please sign in to comment.