Skip to content
15 changes: 13 additions & 2 deletions intersect.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

/**
* Find or counts the intersections between two SVG paths.
*
Expand Down Expand Up @@ -36,6 +35,18 @@ declare function findPathIntersections(path1: Path, path2: Path, justCount?: boo

export default findPathIntersections;

/**
* Parse a path so it is suitable to pass to {@link findPathIntersections}
* Used in order to opt out of internal path caching.
*
* @example
* const p1 = parsePathCurve('M0,0L100,100');
* const p2 = parsePathCurve([ [ 'M', 0, 100 ], [ 'L', 100, 0 ] ]);
* const intersections = findPathIntersections(p1, p2);
*
*/
export declare function parsePathCurve(path: Path): PathComponent[]

/**
* A string in the form of 'M150,150m0,-18a18,18,0,1,1,0,36a18,18,0,1,1,0,-36z'
* or something like:
Expand All @@ -48,7 +59,7 @@ export default findPathIntersections;
* ]
*/
declare type Path = string | PathComponent[];
declare type PathComponent = any[];
declare type PathComponent = [cmd: string, ...number[]];

declare interface Intersection {
/**
Expand Down
156 changes: 91 additions & 65 deletions intersect.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,23 +23,6 @@ function hasProperty(obj, property) {
return Object.prototype.hasOwnProperty.call(obj, property);
}

function clone(obj) {

if (typeof obj == 'function' || Object(obj) !== obj) {
return obj;
}

var res = new obj.constructor;

for (var key in obj) {
if (hasProperty(obj, key)) {
res[key] = clone(obj[key]);
}
}

return res;
}

function repush(array, item) {
for (var i = 0, ii = array.length; i < ii; i++) if (array[i] === item) {
return array.push(array.splice(i, 1)[0]);
Expand Down Expand Up @@ -75,19 +58,9 @@ function parsePathString(pathString) {
return null;
}

var pth = paths(pathString);

if (pth.arr) {
return clone(pth.arr);
}

var paramCounts = { a: 7, c: 6, h: 1, l: 2, m: 2, q: 4, s: 4, t: 2, v: 1, z: 0 },
data = [];

if (isArray(pathString) && isArray(pathString[0])) { // rough assumption
data = clone(pathString);
}

if (!data.length) {

String(pathString).replace(pathCommand, function(a, b, c) {
Expand All @@ -114,7 +87,6 @@ function parsePathString(pathString) {
}

data.toString = paths.toString;
pth.arr = clone(data);

return data;
}
Expand Down Expand Up @@ -166,7 +138,16 @@ function pathToString() {
}

function pathClone(pathArray) {
var res = clone(pathArray);
const res = new Array(pathArray.length);
for (let i = 0; i < pathArray.length; i++) {
const sourceRow = pathArray[i];
const destinationRow = new Array(sourceRow.length);
res[i] = destinationRow;
for (let j = 0; j < sourceRow.length; j++) {
destinationRow[j] = sourceRow[j];
}
}

res.toString = pathToString;
return res;
}
Expand Down Expand Up @@ -299,13 +280,14 @@ function fixError(number) {
return Math.round(number * 100000000000) / 100000000000;
}

/**
*
* @param {import("./intersect").PathComponent} bez1
* @param {import("./intersect").PathComponent} bez2
* @param {boolean} [justCount=false]
* @returns {import("./intersect").Intersection}
*/
function findBezierIntersections(bez1, bez2, justCount) {
var bbox1 = bezierBBox(bez1),
bbox2 = bezierBBox(bez2);

if (!isBBoxIntersect(bbox1, bbox2)) {
return justCount ? 0 : [];
}

// As an optimization, lines will have only 1 segment

Expand All @@ -315,19 +297,19 @@ function findBezierIntersections(bez1, bez2, justCount) {
n1 = isLine(bez1) ? 1 : ~~(l1 / 5) || 1,
// eslint-disable-next-line no-bitwise
n2 = isLine(bez2) ? 1 : ~~(l2 / 5) || 1,
dots1 = [],
dots2 = [],
dots1 = new Array(n1 + 1),
dots2 = new Array(n2 + 1),
xy = {},
res = justCount ? 0 : [];

for (var i = 0; i < n1 + 1; i++) {
var p = findDotsAtSegment(...bez1, i / n1);
dots1.push({ x: p.x, y: p.y, t: i / n1 });
dots1[i] = { x: p.x, y: p.y, t: i / n1 };
}

for (i = 0; i < n2 + 1; i++) {
p = findDotsAtSegment(...bez2, i / n2);
dots2.push({ x: p.x, y: p.y, t: i / n2 });
dots2[i] = { x: p.x, y: p.y, t: i / n2 };
}

for (i = 0; i < n1; i++) {
Expand Down Expand Up @@ -399,17 +381,17 @@ function findBezierIntersections(bez1, bez2, justCount) {
* // { x: 50, y: 50, segment1: 1, segment2: 1, t1: 0.5, t2: 0.5 }
* // ]
*
* @param {String|Array<PathDef>} path1
* @param {String|Array<PathDef>} path2
* @param {Boolean} [justCount=false]
* @param {import("./intersect").Path} path1
* @param {import("./intersect").Path} path2
* @param {boolean} [justCount=false]
*
* @return {Array<Intersection>|Number}
* @return {import("./intersect").Intersection[] | number}
*/
export default function findPathIntersections(path1, path2, justCount) {
path1 = pathToCurve(path1);
path2 = pathToCurve(path2);
path1 = path1.parsed ? path1 : getPathCurve(path1);
path2 = path2.parsed ? path2 : getPathCurve(path2);

var x1, y1, x2, y2, x1m, y1m, x2m, y2m, bez1, bez2,
var x1, y1, x2, y2, x1m, y1m, x2m, y2m, bez1, bez2, bbox1, bbox2,
res = justCount ? 0 : [];

for (var i = 0, ii = path1.length; i < ii; i++) {
Expand Down Expand Up @@ -448,7 +430,12 @@ export default function findPathIntersections(path1, path2, justCount) {
y2 = y2m;
}

var intr = findBezierIntersections(bez1, bez2, justCount);
bbox1 = bezierBBox(bez1);
bbox2 = bezierBBox(bez2);

var intr = isBBoxIntersect(bbox1, bbox2) ?
findBezierIntersections(bez1, bez2, justCount) :
justCount ? 0 : [];

if (justCount) {
res += intr;
Expand All @@ -471,17 +458,7 @@ export default function findPathIntersections(path1, path2, justCount) {
return res;
}


function pathToAbsolute(pathArray) {
var pth = paths(pathArray);

if (pth.abs) {
return pathClone(pth.abs);
}

if (!isArray(pathArray) || !isArray(pathArray && pathArray[0])) { // rough assumption
pathArray = parsePathString(pathArray);
}

if (!pathArray || !pathArray.length) {
return [ [ 'M', 0, 0 ] ];
Expand Down Expand Up @@ -564,7 +541,6 @@ function pathToAbsolute(pathArray) {
}

res.toString = pathToString;
pth.abs = pathClone(res);

return res;
}
Expand Down Expand Up @@ -785,16 +761,69 @@ function curveBBox(x0, y0, x1, y1, x2, y2, x3, y3) {
};
}

function pathToCurve(path) {
/**
* An impure version of {@link parsePathCurve} handling caching
*/
function getPathCurve(path) {

var pth = paths(path);
const pth = paths(path);

// return cached curve, if existing
if (pth.curve) {
return pathClone(pth.curve);
return pth.curve;
}

var curvedPath = pathToAbsolute(path),
// retrieve abs path OR create and cache if non existing
const abs = pth.abs ||
(pth.abs = pathToAbsolute(

// retrieve parsed path OR create and cache if non existing
pth.arr ||
(pth.arr = (

// rough assumption
(!isArray(path) || !isArray(path && path[0]))) ?
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this...

Suggested change
(!isArray(path) || !isArray(path && path[0]))) ?
typeof path === 'string' ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nikku @barmac can I commit this?

parsePathString(path) :
path)
));

// cache curve
return (pth.curve = pathToCurve(abs));
}

/**
* A pure version of {@link getPathCurve}
* @param {import("./intersect").Path} path
* @returns {import("./intersect").PathComponent[]}
*/
export function parsePathCurve(path) {

const abs = (pathToAbsolute(
!Array.isArray(path) ?
parsePathString(path) :
path)
);

const curve = pathToCurve(abs);

/**
* Flag to skip {@link getPathCurve}
*/
return Object.defineProperty(
curve,
'parsed',
{
value: true,
configurable: false,
enumerable: false,
writable: false
}
);
}

function pathToCurve(absPath) {

var curvedPath = pathClone(absPath),
attrs = { x: 0, y: 0, bx: 0, by: 0, X: 0, Y: 0, qx: null, qy: null },
processPath = function(path, d, pathCommand) {
var nx, ny;
Expand Down Expand Up @@ -918,8 +947,5 @@ function pathToCurve(path) {
attrs.by = toFloat(seg[seglen - 3]) || attrs.y;
}

// cache curve
pth.curve = pathClone(curvedPath);

return curvedPath;
}
57 changes: 56 additions & 1 deletion test/intersect.spec.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import intersect from 'path-intersection';
import intersect, { parsePathCurve } from 'path-intersection';
import { expect } from 'chai';

import domify from 'domify';
Expand All @@ -21,6 +21,61 @@ describe('path-intersection', function() {
expect(intersections).to.have.length(1);
});

it('should cache paths to boost performance', function() {

const max = 1000;
const p1 = [
[ 'M', 0, 0 ],
...new Array(max).fill(0).map((_, i) => [ 'L', max * (i + 1), max * (i + 1) ])
];
const p2 = [
[ 'M', 0, max * max ],
...new Array(max).fill(0).map((_, i) => [ 'L', max * (i + 1), max * (max - i + 1) ])
].flat().join(',');

// when
performance.mark('a')
const ra = intersect(p1, p2);
const { duration: a } = performance.measure('not cached', 'a');
performance.mark('b')
const rb = intersect(p1, p2);
const { duration: b } = performance.measure('cached', 'b');
// then
expect(b).to.lessThanOrEqual(a);
expect(rb).to.deep.eq(ra);
});

it('parsePathCurve', function() {

// when
const parsed1 = parsePathCurve(p1);
const parsed2 = parsePathCurve(p2);

// then
expect(parsed1).to.deep.eq([['M', 0, 0], ['C', 0, 0, 100, 100, 100, 100]])
expect(parsed1.parsed).to.eq(true)

expect(parsed2).to.deep.eq([['M', 0, 100], ['C', 0, 100, 100, 0, 100, 0]])
expect(parsed2.parsed).to.eq(true)

expect(intersect(parsed1, parsed2)).to.deep.eq([
{
x: 50,
y: 50,
segment1: 1,
segment2: 1,
t1: 0.5,
t2: 0.5,
bez1: [0, 0, 0, 0, 100, 100, 100, 100],
bez2: [0, 100, 0, 100, 100, 0, 100, 0]
}
])

expect(parsed1, 'intersect should not mutate paths').to.deep.eq([['M', 0, 0], ['C', 0, 0, 100, 100, 100, 100]])
expect(parsed2, 'intersect should not mutate paths').to.deep.eq([['M', 0, 100], ['C', 0, 100, 100, 0, 100, 0]])

});


it('should expose intersection', function() {

Expand Down
8 changes: 3 additions & 5 deletions test/intersect.spec.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import intersect from 'path-intersection';

import domify from 'domify';
import intersect, { Path } from 'path-intersection';


describe('path-intersection', function() {

describe('api', function() {

var p1 = [ [ 'M', 0, 0 ], [ 'L', 100, 100 ] ];
var p2 = 'M0,100L100,0';
const p1: Path = [ [ 'M', 0, 0 ], [ 'L', 100, 100 ] ];
const p2: Path = 'M0,100L100,0';


it('should support SVG path and component args', function() {
Expand Down