Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 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,9 @@ export default findPathIntersections;
* ]
*/
declare type Path = string | PathComponent[];
declare type PathComponent = any[];
declare type RelativePathCmd = 'a' | 'c' | 'h' | 'l' | 'm' | 'q' | 's' | 't' | 'v' | 'z';
declare type AbsolutePathCmd = Capitalize<RelativePathCmd>;
declare type PathComponent = [cmd: AbsolutePathCmd | RelativePathCmd, ...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;
}
25 changes: 22 additions & 3 deletions karma.conf.cjs
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
/** eslint-env node */

// configures browsers to run test against
// any of [ 'ChromeHeadless', 'Chrome', 'Firefox' ]
const browsers = (process.env.TEST_BROWSERS || 'ChromeHeadless').split(',');
// any of [ 'ChromeHeadless', 'ChromeHeadlessDev', 'Chrome', 'ChromeDev', 'Firefox' ]
const browsers = (process.env.TEST_BROWSERS || 'ChromeHeadlessDev').split(',');

// use puppeteer provided Chrome for testing
process.env.CHROME_BIN = require('puppeteer').executablePath();


module.exports = function(karma) {
karma.set({

Expand All @@ -32,6 +31,26 @@ module.exports = function(karma) {
webpack: {
mode: 'development',
devtool: 'eval-source-map'
},

customLaunchers: {
ChromeDev: {
base: 'Chrome',
displayName: 'ChromeDev',
flags: [
// disable chromium safe storage access request security prompt on macOS
'--use-mock-keychain',
]
},
ChromeHeadlessDev: {
base: 'ChromeHeadless',
displayName: 'ChromeHeadlessDev',
flags: [
// disable chromium safe storage access request security prompt on macOS
'--use-mock-keychain',
]
}
}

});
};
Loading