From 74c7a5ba52fa1b6eb2a4aa389d32a280b7f28d19 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 16 Oct 2025 17:34:52 +0300 Subject: [PATCH 01/11] refactor(`findBezierIntersections`): extract `bezierBBoxIntersects` --- intersect.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/intersect.js b/intersect.js index 5925720..aff7a7b 100644 --- a/intersect.js +++ b/intersect.js @@ -299,13 +299,14 @@ function fixError(number) { return Math.round(number * 100000000000) / 100000000000; } -function findBezierIntersections(bez1, bez2, justCount) { +function bezierBBoxIntersects(bez1, bez2) { var bbox1 = bezierBBox(bez1), bbox2 = bezierBBox(bez2); - if (!isBBoxIntersect(bbox1, bbox2)) { - return justCount ? 0 : []; - } + return isBBoxIntersect(bbox1, bbox2); +} + +function findBezierIntersections(bez1, bez2, justCount) { // As an optimization, lines will have only 1 segment @@ -448,7 +449,9 @@ export default function findPathIntersections(path1, path2, justCount) { y2 = y2m; } - var intr = findBezierIntersections(bez1, bez2, justCount); + var intr = bezierBBoxIntersects(bez1, bez2) ? + findBezierIntersections(bez1, bez2, justCount) : + justCount ? 0 : []; if (justCount) { res += intr; From f5455f9c6a996b20370156c82a1e13430bff4454 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 16 Oct 2025 18:16:57 +0300 Subject: [PATCH 02/11] refactor(): `pathToCurve` `pathToAbsolute` `parsePathString` extract caching to `getPathCurve` --- intersect.js | 53 +++++++++++++++++++++++++++------------------------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/intersect.js b/intersect.js index aff7a7b..8c19a18 100644 --- a/intersect.js +++ b/intersect.js @@ -75,12 +75,6 @@ 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 = []; @@ -114,7 +108,6 @@ function parsePathString(pathString) { } data.toString = paths.toString; - pth.arr = clone(data); return data; } @@ -407,8 +400,8 @@ function findBezierIntersections(bez1, bez2, justCount) { * @return {Array|Number} */ export default function findPathIntersections(path1, path2, justCount) { - path1 = pathToCurve(path1); - path2 = pathToCurve(path2); + path1 = getPathCurve(path1); + path2 = getPathCurve(path2); var x1, y1, x2, y2, x1m, y1m, x2m, y2m, bez1, bez2, res = justCount ? 0 : []; @@ -476,15 +469,6 @@ export default function findPathIntersections(path1, path2, justCount) { 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 ] ]; @@ -567,7 +551,6 @@ function pathToAbsolute(pathArray) { } res.toString = pathToString; - pth.abs = pathClone(res); return res; } @@ -788,16 +771,39 @@ function curveBBox(x0, y0, x1, y1, x2, y2, x3, y3) { }; } -function pathToCurve(path) { +/** + * Handles caches + */ +function getPathCurve(path) { - var pth = paths(path); + const pth = paths(path); // return cached curve, if existing if (pth.curve) { return pathClone(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]))) ? + parsePathString(path) : + path) + )); + + // cache curve + return pathClone(pth.curve = pathToCurve(abs)); +} + +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; @@ -921,8 +927,5 @@ function pathToCurve(path) { attrs.by = toFloat(seg[seglen - 3]) || attrs.y; } - // cache curve - pth.curve = pathClone(curvedPath); - return curvedPath; } \ No newline at end of file From a773a2e155da5e7255d93f0797ff73322fc5fb9f Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 16 Oct 2025 19:57:25 +0300 Subject: [PATCH 03/11] refactor(): rm `clone` usages --- intersect.js | 32 ++++++++++---------------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/intersect.js b/intersect.js index 8c19a18..84b5cf6 100644 --- a/intersect.js +++ b/intersect.js @@ -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]); @@ -78,10 +61,6 @@ function parsePathString(pathString) { 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) { @@ -159,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; } From 86917e3d0b379550d8612eb73399a5976351afbf Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Sun, 19 Oct 2025 10:55:30 +0300 Subject: [PATCH 04/11] feat(): rm redundant `pathClone` call --- intersect.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/intersect.js b/intersect.js index 84b5cf6..8c1c37a 100644 --- a/intersect.js +++ b/intersect.js @@ -768,7 +768,7 @@ function getPathCurve(path) { // return cached curve, if existing if (pth.curve) { - return pathClone(pth.curve); + return pth.curve; } // retrieve abs path OR create and cache if non existing @@ -786,7 +786,7 @@ function getPathCurve(path) { )); // cache curve - return pathClone(pth.curve = pathToCurve(abs)); + return (pth.curve = pathToCurve(abs)); } function pathToCurve(absPath) { From 06bcefe9261b4bfb228c90a423078e60caf947db Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Sun, 19 Oct 2025 10:54:51 +0300 Subject: [PATCH 05/11] docs(): jsdoc fixes --- intersect.d.ts | 3 +-- intersect.js | 16 +++++++++++----- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/intersect.d.ts b/intersect.d.ts index 7b764db..567d517 100644 --- a/intersect.d.ts +++ b/intersect.d.ts @@ -1,4 +1,3 @@ - /** * Find or counts the intersections between two SVG paths. * @@ -48,7 +47,7 @@ export default findPathIntersections; * ] */ declare type Path = string | PathComponent[]; -declare type PathComponent = any[]; +declare type PathComponent = [cmd: string, ...number[]]; declare interface Intersection { /** diff --git a/intersect.js b/intersect.js index 8c1c37a..a8315d1 100644 --- a/intersect.js +++ b/intersect.js @@ -287,6 +287,13 @@ function bezierBBoxIntersects(bez1, bez2) { return isBBoxIntersect(bbox1, bbox2); } +/** + * + * @param {import("./intersect").PathComponent} bez1 + * @param {import("./intersect").PathComponent} bez2 + * @param {boolean} [justCount=false] + * @returns {import("./intersect").Intersection} + */ function findBezierIntersections(bez1, bez2, justCount) { // As an optimization, lines will have only 1 segment @@ -381,11 +388,11 @@ function findBezierIntersections(bez1, bez2, justCount) { * // { x: 50, y: 50, segment1: 1, segment2: 1, t1: 0.5, t2: 0.5 } * // ] * - * @param {String|Array} path1 - * @param {String|Array} path2 - * @param {Boolean} [justCount=false] + * @param {import("./intersect").Path} path1 + * @param {import("./intersect").Path} path2 + * @param {boolean} [justCount=false] * - * @return {Array|Number} + * @return {import("./intersect").Intersection[] | number} */ export default function findPathIntersections(path1, path2, justCount) { path1 = getPathCurve(path1); @@ -455,7 +462,6 @@ export default function findPathIntersections(path1, path2, justCount) { return res; } - function pathToAbsolute(pathArray) { if (!pathArray || !pathArray.length) { From 9cfa86347e3e507daf369c9f070da89ccd2b5c45 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Sun, 19 Oct 2025 11:22:18 +0300 Subject: [PATCH 06/11] feat(): `parsePathCurve` --- intersect.d.ts | 12 ++++++++++++ intersect.js | 36 +++++++++++++++++++++++++++++++++--- test/intersect.spec.js | 17 ++++++++++++++++- 3 files changed, 61 insertions(+), 4 deletions(-) diff --git a/intersect.d.ts b/intersect.d.ts index 567d517..2a2bf09 100644 --- a/intersect.d.ts +++ b/intersect.d.ts @@ -35,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: diff --git a/intersect.js b/intersect.js index a8315d1..25e5010 100644 --- a/intersect.js +++ b/intersect.js @@ -395,8 +395,8 @@ function findBezierIntersections(bez1, bez2, justCount) { * @return {import("./intersect").Intersection[] | number} */ export default function findPathIntersections(path1, path2, justCount) { - path1 = getPathCurve(path1); - path2 = getPathCurve(path2); + path1 = path1.parsed ? path1 : getPathCurve(path1); + path2 = path2.parsed ? path2 : getPathCurve(path2); var x1, y1, x2, y2, x1m, y1m, x2m, y2m, bez1, bez2, res = justCount ? 0 : []; @@ -766,7 +766,7 @@ function curveBBox(x0, y0, x1, y1, x2, y2, x3, y3) { } /** - * Handles caches + * An impure version of {@link parsePathCurve} handling caching */ function getPathCurve(path) { @@ -795,6 +795,36 @@ function getPathCurve(path) { 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), diff --git a/test/intersect.spec.js b/test/intersect.spec.js index a4bffbe..b1a3fcd 100644 --- a/test/intersect.spec.js +++ b/test/intersect.spec.js @@ -1,4 +1,4 @@ -import intersect from 'path-intersection'; +import intersect, { parsePathCurve } from 'path-intersection'; import { expect } from 'chai'; import domify from 'domify'; @@ -21,6 +21,21 @@ describe('path-intersection', function() { expect(intersections).to.have.length(1); }); + it('parsePathCurve', function() { + + // when + var parsed1 = parsePathCurve(p1); + var 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) + + }); + it('should expose intersection', function() { From fba13064a66542f80448171f6c5facd1903a6910 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 20 Oct 2025 15:45:08 +0300 Subject: [PATCH 07/11] fix(): failing ts test --- test/intersect.spec.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/test/intersect.spec.ts b/test/intersect.spec.ts index 79f827c..3b47ca7 100644 --- a/test/intersect.spec.ts +++ b/test/intersect.spec.ts @@ -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() { From a19e9a67aab56d83500c60ce546236e141630573 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 20 Oct 2025 15:45:26 +0300 Subject: [PATCH 08/11] test(): test no mutation --- test/intersect.spec.js | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/test/intersect.spec.js b/test/intersect.spec.js index b1a3fcd..f92ff8a 100644 --- a/test/intersect.spec.js +++ b/test/intersect.spec.js @@ -24,8 +24,8 @@ describe('path-intersection', function() { it('parsePathCurve', function() { // when - var parsed1 = parsePathCurve(p1); - var parsed2 = parsePathCurve(p2); + const parsed1 = parsePathCurve(p1); + const parsed2 = parsePathCurve(p2); // then expect(parsed1).to.deep.eq([['M', 0, 0], ['C', 0, 0, 100, 100, 100, 100]]) @@ -34,6 +34,22 @@ describe('path-intersection', function() { 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]]) + }); From ef16424947875bee46a614f3e951ebd4203af9a2 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Tue, 21 Oct 2025 08:30:32 +0300 Subject: [PATCH 09/11] test(): caching --- test/intersect.spec.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/test/intersect.spec.js b/test/intersect.spec.js index f92ff8a..ae7ebfb 100644 --- a/test/intersect.spec.js +++ b/test/intersect.spec.js @@ -21,6 +21,30 @@ 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 From c51c3f671035ff38c4d533464fa84e1b7cd6db16 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Tue, 21 Oct 2025 08:36:52 +0300 Subject: [PATCH 10/11] chore(): inline `bezierBBoxIntersects` --- intersect.js | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/intersect.js b/intersect.js index 25e5010..b3cc26d 100644 --- a/intersect.js +++ b/intersect.js @@ -280,13 +280,6 @@ function fixError(number) { return Math.round(number * 100000000000) / 100000000000; } -function bezierBBoxIntersects(bez1, bez2) { - var bbox1 = bezierBBox(bez1), - bbox2 = bezierBBox(bez2); - - return isBBoxIntersect(bbox1, bbox2); -} - /** * * @param {import("./intersect").PathComponent} bez1 @@ -398,7 +391,7 @@ export default function findPathIntersections(path1, path2, justCount) { 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++) { @@ -437,7 +430,10 @@ export default function findPathIntersections(path1, path2, justCount) { y2 = y2m; } - var intr = bezierBBoxIntersects(bez1, bez2) ? + bbox1 = bezierBBox(bez1); + bbox2 = bezierBBox(bez2); + + var intr = isBBoxIntersect(bbox1, bbox2) ? findBezierIntersections(bez1, bez2, justCount) : justCount ? 0 : []; From 6772ff58026f36582700f48021f829bd4aeae63f Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Tue, 21 Oct 2025 09:02:50 +0300 Subject: [PATCH 11/11] perf(): memory alloc --- intersect.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/intersect.js b/intersect.js index b3cc26d..e20dbf2 100644 --- a/intersect.js +++ b/intersect.js @@ -297,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++) {