Skip to content

Commit 15c15ae

Browse files
committed
feat: rework path parsing + allow external caching
Closes #25
1 parent 96840db commit 15c15ae

5 files changed

Lines changed: 277 additions & 76 deletions

File tree

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,25 @@ const intersection = intersect(path0, path1);
2727
Results are approximate, as we use [bezier clipping](https://math.stackexchange.com/questions/118937) to find intersections.
2828

2929

30+
## Path Caching
31+
32+
Where performance matters, you can pre-parse paths and cache them:
33+
34+
```javascript
35+
import intersect, { parsePath } from 'path-intersection';
36+
37+
// parse paths once
38+
const path1 = parsePath('M0,0L100,100');
39+
const path2 = parsePath('M0,100L100,0');
40+
41+
// they won't be re-parsed during intersection checking
42+
const result1 = intersect(path1, path2);
43+
const result2 = intersect(path2, path2);
44+
```
45+
46+
For repeated calculations, this optimization can result in substantial performance improvements.
47+
48+
3049
## Building the Project
3150

3251
```

intersect.d.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,13 @@
1212
* The path may be an SVG path string or a list of path components
1313
* such as `[ [ 'M', 0, 10 ], [ 'L', 20, 0 ] ]`.
1414
*
15+
* For performance optimization, pre-parsed paths can be passed directly,
16+
* the utility `parsePath` can be used to pre-parse any path.
17+
*
1518
* @example
1619
*
20+
* import findPathIntersections from 'path-intersection';
21+
*
1722
* var intersections = findPathIntersections(
1823
* 'M0,0L100,100',
1924
* [ [ 'M', 0, 100 ], [ 'L', 100, 0 ] ]
@@ -36,6 +41,34 @@ declare function findPathIntersections(path1: Path, path2: Path, justCount?: boo
3641

3742
export default findPathIntersections;
3843

44+
/**
45+
* Parses and optimizes a path for use with intersection calculations.
46+
*
47+
* Takes an SVG path string or component array and converts it to an optimized
48+
* internal format. The result can be cached and reused for maximum
49+
* performance in intersection calculations.
50+
*
51+
* This is the recommended way to pre-parse paths for repeated use.
52+
* Paths parsed this way will not be re-parsed when passed to `intersect()`.
53+
*
54+
* @example
55+
*
56+
* import intersect, { parsePath } from 'path-intersection';
57+
*
58+
* // parse once
59+
* const path1 = parsePath('M0,0L100,100');
60+
* const path2 = parsePath('M0,100L100,0');
61+
*
62+
* // cache and reuse
63+
* const result1 = intersect(path1, parsedPath2);
64+
* const result2 = intersect(path2, parsedPath2);
65+
*
66+
* @param {Path} path - the path to parse
67+
*
68+
* @return {PathComponent[]} pre-parsed and optimized path
69+
*/
70+
export function parsePath(path: Path): PathComponent[];
71+
3972
/**
4073
* A string in the form of 'M150,150m0,-18a18,18,0,1,1,0,36a18,18,0,1,1,0,-36z'
4174
* or something like:

intersect.js

Lines changed: 80 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -75,71 +75,61 @@ function parsePathString(pathString) {
7575
return null;
7676
}
7777

78-
var pth = paths(pathString);
78+
var paramCounts = { a: 7, c: 6, h: 1, l: 2, m: 2, q: 4, s: 4, t: 2, v: 1, z: 0 },
79+
pathComponents = [];
7980

80-
if (pth.arr) {
81-
return clone(pth.arr);
82-
}
81+
String(pathString).replace(pathCommand, function(a, b, c) {
82+
var params = [],
83+
name = b.toLowerCase();
8384

84-
var paramCounts = { a: 7, c: 6, h: 1, l: 2, m: 2, q: 4, s: 4, t: 2, v: 1, z: 0 },
85-
data = [];
85+
c.replace(pathValues, function(a, b) {
86+
b && params.push(+b);
87+
});
8688

87-
if (isArray(pathString) && isArray(pathString[0])) { // rough assumption
88-
data = clone(pathString);
89-
}
89+
if (name == 'm' && params.length > 2) {
90+
pathComponents.push([ b, ...params.splice(0, 2) ]);
91+
name = 'l';
92+
b = b == 'm' ? 'l' : 'L';
93+
}
9094

91-
if (!data.length) {
95+
while (params.length >= paramCounts[name]) {
96+
pathComponents.push([ b, ...params.splice(0, paramCounts[name]) ]);
97+
if (!paramCounts[name]) {
98+
break;
99+
}
100+
}
101+
});
92102

93-
String(pathString).replace(pathCommand, function(a, b, c) {
94-
var params = [],
95-
name = b.toLowerCase();
103+
pathComponents.toString = pathToString;
96104

97-
c.replace(pathValues, function(a, b) {
98-
b && params.push(+b);
99-
});
105+
return pathComponents;
106+
}
100107

101-
if (name == 'm' && params.length > 2) {
102-
data.push([ b, ...params.splice(0, 2) ]);
103-
name = 'l';
104-
b = b == 'm' ? 'l' : 'L';
105-
}
108+
function isPathAbsolute(pathComponents) {
106109

107-
while (params.length >= paramCounts[name]) {
108-
data.push([ b, ...params.splice(0, paramCounts[name]) ]);
109-
if (!paramCounts[name]) {
110-
break;
111-
}
112-
}
113-
});
110+
for (var i = 0; i < pathComponents.length; i++) {
111+
var command = pathComponents[i][0];
112+
if (typeof command === 'string' && command !== command.toUpperCase()) {
113+
return false;
114+
}
114115
}
115116

116-
data.toString = paths.toString;
117-
pth.arr = clone(data);
118-
119-
return data;
117+
return true;
120118
}
121119

122-
function paths(ps) {
123-
var p = paths.ps = paths.ps || {};
124-
125-
if (p[ps]) {
126-
p[ps].sleep = 100;
127-
} else {
128-
p[ps] = {
129-
sleep: 100
130-
};
120+
function isPathCurve(pathArray) {
121+
if (!isArray(pathArray) || !pathArray.length) {
122+
return false;
131123
}
132124

133-
setTimeout(function() {
134-
for (var key in p) {
135-
if (hasProperty(p, key) && key != ps) {
136-
p[key].sleep--;
137-
!p[key].sleep && delete p[key];
138-
}
125+
for (var i = 0; i < pathArray.length; i++) {
126+
var command = pathArray[i][0];
127+
if (command !== 'M' && command !== 'C') {
128+
return false;
139129
}
140-
});
130+
}
141131

142-
return p[ps];
132+
return true;
143133
}
144134

145135
function rectBBox(x, y, width, height) {
@@ -471,20 +461,14 @@ export default function findPathIntersections(path1, path2, justCount) {
471461
return res;
472462
}
473463

464+
function isPathComponents(path) {
465+
return isArray(path) && isArray(path[0]);
466+
}
474467

475468
function pathToAbsolute(pathArray) {
476-
var pth = paths(pathArray);
477469

478-
if (pth.abs) {
479-
return pathClone(pth.abs);
480-
}
481-
482-
if (!isArray(pathArray) || !isArray(pathArray && pathArray[0])) { // rough assumption
483-
pathArray = parsePathString(pathArray);
484-
}
485-
486-
if (!pathArray || !pathArray.length) {
487-
return [ [ 'M', 0, 0 ] ];
470+
if (isPathAbsolute(pathArray)) {
471+
return pathArray;
488472
}
489473

490474
var res = [],
@@ -564,7 +548,6 @@ function pathToAbsolute(pathArray) {
564548
}
565549

566550
res.toString = pathToString;
567-
pth.abs = pathClone(res);
568551

569552
return res;
570553
}
@@ -787,14 +770,15 @@ function curveBBox(x0, y0, x1, y1, x2, y2, x3, y3) {
787770

788771
function pathToCurve(path) {
789772

790-
var pth = paths(path);
773+
if (!isPathComponents(path)) {
774+
path = parsePathString(path);
775+
}
791776

792-
// return cached curve, if existing
793-
if (pth.curve) {
794-
return pathClone(pth.curve);
777+
if (isPathCurve(path)) {
778+
return path;
795779
}
796780

797-
var curvedPath = pathToAbsolute(path),
781+
var curvedPath = pathClone(pathToAbsolute(path)),
798782
attrs = { x: 0, y: 0, bx: 0, by: 0, X: 0, Y: 0, qx: null, qy: null },
799783
processPath = function(path, d, pathCommand) {
800784
var nx, ny;
@@ -918,8 +902,35 @@ function pathToCurve(path) {
918902
attrs.by = toFloat(seg[seglen - 3]) || attrs.y;
919903
}
920904

921-
// cache curve
922-
pth.curve = pathClone(curvedPath);
923-
924905
return curvedPath;
906+
}
907+
908+
/**
909+
* Parses and optimizes a path for use with intersection calculations.
910+
*
911+
* Takes an SVG path string or component array and converts it to an optimized
912+
* internal format. The result can be cached and reused for maximum
913+
* performance in intersection calculations.
914+
*
915+
* This is the recommended way to pre-parse paths for repeated use.
916+
* Paths parsed this way will not be re-parsed when passed to `intersect()`.
917+
*
918+
* @example
919+
*
920+
* import intersect, { parsePath } from 'path-intersection';
921+
*
922+
* // parse once
923+
* const path1 = parsePath('M0,0L100,100');
924+
* const path2 = parsePath('M0,100L100,0');
925+
*
926+
* // cache and reuse
927+
* const result1 = intersect(path1, parsedPath2);
928+
* const result2 = intersect(path2, parsedPath2);
929+
*
930+
* @param {Path} path - the path to parse
931+
*
932+
* @return {PathComponent[]} pre-parsed and optimized path
933+
*/
934+
export function parsePath(path) {
935+
return pathToCurve(path);
925936
}

0 commit comments

Comments
 (0)