1
0
mirror of https://github.com/svg/svgo.git synced 2025-07-29 20:21:14 +03:00

Check path intersection while merging.

Fixes #253, resolves #260, fixes #298.
This commit is contained in:
GreLI
2015-03-21 23:52:53 +03:00
parent e220b6cad0
commit e1060df7dc
6 changed files with 351 additions and 6 deletions

View File

@ -9,7 +9,8 @@ var regPathInstructions = /([MmLlHhVvCcSsQqTtAaZz])\s*/,
referencesProps = collections.referencesProps, referencesProps = collections.referencesProps,
defaultStrokeWidth = collections.attrsGroupsDefaults.presentation['stroke-width'], defaultStrokeWidth = collections.attrsGroupsDefaults.presentation['stroke-width'],
cleanupOutData = require('../lib/svgo/tools').cleanupOutData, 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. * Convert path string to JS representation.
@ -311,7 +312,6 @@ exports.applyTransforms = function(elem, path, applyTransformsStroked, floatPrec
elem.removeAttr('transform'); elem.removeAttr('transform');
return path; return path;
}; };
/** /**
@ -590,6 +590,290 @@ function set(dest, source) {
return dest; 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/ /* Based on code from Snap.svg (Apache 2 license). http://snapsvg.io/
* Thanks to Dmitry Baranovskiy for his great work! * Thanks to Dmitry Baranovskiy for his great work!
*/ */

View File

@ -11,7 +11,8 @@ exports.params = {
}; };
var path2js = require('./_path.js').path2js, 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. * Merge multiple Paths into one.
@ -51,7 +52,7 @@ exports.fn = function(item, params) {
prevPathJS = path2js(prevContentItem), prevPathJS = path2js(prevContentItem),
curPathJS = path2js(contentItem); curPathJS = path2js(contentItem);
if (equalData) { if (equalData && !interesects(prevPathJS, curPathJS)) {
js2path(prevContentItem, prevPathJS.concat(curPathJS), params); js2path(prevContentItem, prevPathJS.concat(curPathJS), params);
return false; return false;
} }

View File

@ -5,7 +5,7 @@
<path d="M 30,30 z"/> <path d="M 30,30 z"/>
<path d="M 30,30 z" fill="#f00"/> <path d="M 30,30 z" fill="#f00"/>
<path d="M 40,40 z"/> <path d="M 40,40 z"/>
<path d="m 50,50 z"/> <path d="m 50,50 0,10 20,30 40,0"/>
</svg> </svg>
@@@ @@@
@ -13,5 +13,5 @@
<svg xmlns="http://www.w3.org/2000/svg"> <svg xmlns="http://www.w3.org/2000/svg">
<path d="M0 0zM10 10zM20 20l10 10M30 0c10 0 20 10 20 20M30 30z"/> <path d="M0 0zM10 10zM20 20l10 10M30 0c10 0 20 10 20 20M30 30z"/>
<path d="M 30,30 z" fill="#f00"/> <path d="M 30,30 z" fill="#f00"/>
<path d="M40 40zM50 50z"/> <path d="M40 40zM50 50l0 10 20 30 40 0"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 465 B

After

Width:  |  Height:  |  Size: 494 B

View File

@ -13,6 +13,10 @@
<path d="M 40,40" fill="#fff" stroke="#333"/> <path d="M 40,40" fill="#fff" stroke="#333"/>
<path d="m 50,50" fill="#fff" stroke="#333"/> <path d="m 50,50" fill="#fff" stroke="#333"/>
<path d="m 50,50 z" fill="#fff" stroke="#333"/> <path d="m 50,50 z" fill="#fff" stroke="#333"/>
<path d="M0 0v100h100V0z" fill="red"/>
<path d="M200 0v100h100V0z" fill="red"/>
<path d="M0 0v100h100V0z" fill="blue"/>
<path d="M200 0v100h100V0zM0 200h100v100H0z" fill="blue"/>
</svg> </svg>
@@@ @@@
@ -22,4 +26,6 @@
<path d="M 30,30 z" fill="#f00"/> <path d="M 30,30 z" fill="#f00"/>
<path d="M40 40zM50 50zM40 40 50 50"/> <path d="M40 40zM50 50zM40 40 50 50"/>
<path d="M40 40zM50 50zM40 40 50 50 50 50z" fill="#fff" stroke="#333"/> <path d="M40 40zM50 50zM40 40 50 50 50 50z" fill="#fff" stroke="#333"/>
<path d="M0 0v100h100V0zM200 0v100h100V0z" fill="red"/>
<path d="M0 0v100h100V0zM200 0v100h100V0zM0 200h100v100H0z" fill="blue"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 925 B

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,44 @@
<svg xmlns="http://www.w3.org/2000/svg">
<path d="M30 0L0 40H60z"/>
<path d="M0 10H60L30 50z"/>
<path d="M0 0V50L50 0"/>
<path d="M0 60L50 10V60"/>
<g>
<path d="M100 0a50 50 0 0 1 0 100"/>
<path d="M25 25H75V75H25z"/>
<path d="M135 85H185V135H135z"/>
</g>
<g>
<path d="M10 14H7v1h3v-1z"/>
<path d="M9 21H8v1h1v-1z"/>
</g>
<g>
<path d="M30 32.705V40h10.42L30 32.705z"/>
<path d="M46.25 34.928V30h-7.04l7.04 4.928z"/>
</g>
<g>
<path d="M20 20H60L100 30"/>
<path d="M20 20L50 30H100"/>
</g>
</svg>
@@@
<svg xmlns="http://www.w3.org/2000/svg">
<path d="M30 0L0 40H60z"/>
<path d="M0 10H60L30 50z"/>
<path d="M0 0V50L50 0M0 60L50 10V60"/>
<g>
<path d="M100 0a50 50 0 0 1 0 100M25 25H75V75H25z"/>
<path d="M135 85H185V135H135z"/>
</g>
<g>
<path d="M10 14H7v1h3v-1zM9 21H8v1h1v-1z"/>
</g>
<g>
<path d="M30 32.705V40h10.42L30 32.705zM46.25 34.928V30h-7.04l7.04 4.928z"/>
</g>
<g>
<path d="M20 20H60L100 30M20 20L50 30H100"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg">
<path d="M320 60c17.466-8.733 33.76-12.78 46.593-12.484 12.856.297 22.254 4.936 26.612 12.484 4.358 7.548 3.676 18.007-2.494 29.29-6.16 11.26-17.812 23.348-34.107 34.107-16.26 10.735-37.164 20.14-60.72 26.613C272.356 156.473 246.178 160 220 160c-26.18 0-52.357-3.527-75.882-9.99-23.557-6.472-44.462-15.878-60.72-26.613-16.296-10.76-27.95-22.846-34.11-34.108-6.17-11.283-6.85-21.742-2.493-29.29 4.358-7.548 13.756-12.187 26.612-12.484C86.24 47.22 102.535 51.266 120 60c17.426 8.713 36.024 22.114 53.407 39.28C190.767 116.42 206.91 137.33 220 160c13.09 22.67 23.124 47.106 29.29 70.71 6.173 23.638 8.48 46.445 7.313 65.893-1.17 19.49-5.812 35.627-12.485 46.592C237.432 354.18 228.716 360 220 360s-17.432-5.82-24.118-16.805c-6.673-10.965-11.315-27.1-12.485-46.592-1.167-19.448 1.14-42.255 7.314-65.892 6.166-23.604 16.2-48.04 29.29-70.71 13.09-22.67 29.233-43.58 46.593-60.72C283.976 82.113 302.573 68.712 320 60z"/>
<path d="M280 320l100-173.2h200l100 173.2-100 173.2h-200"/>
</svg>
@@@
<svg xmlns="http://www.w3.org/2000/svg">
<path d="M320 60c17.466-8.733 33.76-12.78 46.593-12.484 12.856.297 22.254 4.936 26.612 12.484 4.358 7.548 3.676 18.007-2.494 29.29-6.16 11.26-17.812 23.348-34.107 34.107-16.26 10.735-37.164 20.14-60.72 26.613C272.356 156.473 246.178 160 220 160c-26.18 0-52.357-3.527-75.882-9.99-23.557-6.472-44.462-15.878-60.72-26.613-16.296-10.76-27.95-22.846-34.11-34.108-6.17-11.283-6.85-21.742-2.493-29.29 4.358-7.548 13.756-12.187 26.612-12.484C86.24 47.22 102.535 51.266 120 60c17.426 8.713 36.024 22.114 53.407 39.28C190.767 116.42 206.91 137.33 220 160c13.09 22.67 23.124 47.106 29.29 70.71 6.173 23.638 8.48 46.445 7.313 65.893-1.17 19.49-5.812 35.627-12.485 46.592C237.432 354.18 228.716 360 220 360s-17.432-5.82-24.118-16.805c-6.673-10.965-11.315-27.1-12.485-46.592-1.167-19.448 1.14-42.255 7.314-65.892 6.166-23.604 16.2-48.04 29.29-70.71 13.09-22.67 29.233-43.58 46.593-60.72C283.976 82.113 302.573 68.712 320 60zM280 320l100-173.2h200l100 173.2-100 173.2h-200"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB