Skip to content

Commit 4b9c5a7

Browse files
committed
feat(): spatial indexing
1 parent 8edb820 commit 4b9c5a7

File tree

4 files changed

+199
-5
lines changed

4 files changed

+199
-5
lines changed

indexing.d.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
export type Intersection = import('./intersect.d.ts').Intersection & {
2+
id1: number;
3+
id2: number;
4+
};
5+
type Path = import('./intersect.d.ts').Path;
6+
type PathComponent = import('./intersect.d.ts').PathComponent;
7+
8+
export type BBox = { x0: number; y0: number; x1: number; y1: number };
9+
10+
export type IndexEntry = {
11+
pathId: number;
12+
curveIndex: number;
13+
curve: PathComponent;
14+
};
15+
16+
export type IndexIntersection = [IndexEntry, IndexEntry];
17+
18+
export interface SpatialIndex {
19+
add(
20+
pathId: number,
21+
curveIndex: number,
22+
curve: PathComponent,
23+
bbox: BBox
24+
): void;
25+
26+
remove(pathId: number): void;
27+
28+
intersect(pathIds: number[]): IndexIntersection[];
29+
}
30+
31+
/**
32+
* Index {@link path} into {@link spatialIndex}
33+
* Must be called before {@link findPathIntersections}
34+
* @returns index key to pass to {@link findPathIntersections}
35+
*/
36+
export function indexPath(path: Path, spatialIndex: SpatialIndex): number;
37+
38+
/**
39+
* Find or counts the intersections between two SVG paths.
40+
*
41+
* Returns a number in counting mode and a list of intersections otherwise.
42+
*
43+
* A single intersection entry contains the intersection coordinates (x, y)
44+
* as well as additional information regarding the intersecting segments
45+
* on each path (segment1, segment2) and the relative location of the
46+
* intersection on these segments (t1, t2).
47+
*
48+
* The path may be an SVG path string or a list of path components
49+
* such as `[ [ 'M', 0, 10 ], [ 'L', 20, 0 ] ]`.
50+
*
51+
* Uses spatial indexing to boost performance.
52+
* If a path is not indexed the method will return no intersections.
53+
* @see {@link indexPath}
54+
*
55+
* @example
56+
*
57+
* const spatialIndex = new SpatialIndex();
58+
* const id1 = indexPath('M0,0L100,100', spatialIndex);
59+
* const id2 = indexPath([ [ 'M', 0, 100 ], [ 'L', 100, 0 ] ], spatialIndex);
60+
* const id3 = indexPath([ [ 'M', 0, 50 ], [ 'L', 100, 50 ] ], spatialIndex);
61+
*
62+
* const intersections = findPathIntersections(id1, id2, spatialIndex, false);
63+
* const intersections2 = findPathIntersections(id1, id3, spatialIndex, false);
64+
*
65+
* // intersections = [
66+
* // { x: 50, y: 50, segment1: 1, segment2: 1, t1: 0.5, t2: 0.5 }
67+
* // ];
68+
* // intersections2 = [
69+
* // { x: 50, y: 50, segment1: 1, segment2: 1, t1: 0.5, t2: 0.5 }
70+
* // ];
71+
*/
72+
declare function findPathIntersections(
73+
pathIds: number[],
74+
index: SpatialIndex,
75+
justCount: true
76+
): number;
77+
declare function findPathIntersections(
78+
pathIds: number[],
79+
index: SpatialIndex,
80+
justCount: false
81+
): Intersection[];
82+
declare function findPathIntersections(
83+
pathIds: number[],
84+
index: SpatialIndex
85+
): Intersection[];
86+
declare function findPathIntersections(
87+
pathIds: number[],
88+
index: SpatialIndex,
89+
justCount?: boolean
90+
): Intersection[] | number;
91+
92+
export default findPathIntersections;

indexing.js

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { curveBBox, findBezierIntersections, parseCurve } from './intersect.js';
2+
3+
let indexKey = 0;
4+
5+
/**
6+
*
7+
* @param {string | import('./intersect').Path[]} path
8+
* @param {import('./indexing').SpatialIndex} index
9+
* @returns {number} index key
10+
*/
11+
export function indexPath(path, index) {
12+
const curve = parseCurve(path);
13+
14+
const pathId = indexKey++;
15+
16+
for (
17+
let curveIndex = 0, x1, y1, x1m, y1m, bez, pi;
18+
curveIndex < curve.length;
19+
curveIndex++
20+
) {
21+
pi = curve[curveIndex];
22+
23+
if (pi[0] == 'M') {
24+
x1 = x1m = pi[1];
25+
y1 = y1m = pi[2];
26+
} else {
27+
if (pi[0] == 'C') {
28+
bez = [ x1, y1, ...pi.slice(1) ];
29+
x1 = bez[6];
30+
y1 = bez[7];
31+
} else {
32+
bez = [ x1, y1, x1, y1, x1m, y1m, x1m, y1m ];
33+
x1 = x1m;
34+
y1 = y1m;
35+
}
36+
37+
index.add(pathId, curveIndex, bez, curveBBox(...bez));
38+
}
39+
}
40+
41+
return pathId;
42+
}
43+
44+
/**
45+
*
46+
* @param {number[]} pathIds
47+
* @param {import('./indexing').SpatialIndex} index
48+
* @param {boolean} [justCount]
49+
*/
50+
export default function findIndexedPathIntersections(
51+
pathIds,
52+
index,
53+
justCount
54+
) {
55+
let res = justCount ? 0 : [];
56+
57+
index.intersect(pathIds).forEach(([ a, b ]) => {
58+
59+
/**
60+
* @type {import('./indexing.js').Intersection[]}
61+
*/
62+
const intr = findBezierIntersections(a.curve, b.curve, justCount);
63+
64+
if (justCount) {
65+
res += intr;
66+
} else {
67+
for (var k = 0, kk = intr.length; k < kk; k++) {
68+
intr[k].id1 = a.pathId;
69+
intr[k].id2 = b.pathId;
70+
intr[k].segment1 = a.curveIndex;
71+
intr[k].segment2 = b.curveIndex;
72+
intr[k].bez1 = a.curve;
73+
intr[k].bez2 = b.curve;
74+
}
75+
76+
res = res.concat(intr);
77+
}
78+
});
79+
80+
return res;
81+
}

intersect.d.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
/**
32
* Find or counts the intersections between two SVG paths.
43
*
@@ -48,7 +47,7 @@ export default findPathIntersections;
4847
* ]
4948
*/
5049
declare type Path = string | PathComponent[];
51-
declare type PathComponent = any[];
50+
declare type PathComponent = [cmd: string, ...number[]];
5251

5352
declare interface Intersection {
5453
/**

intersect.js

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,14 @@ function bezierBBoxIntersects(bez1, bez2) {
287287
return isBBoxIntersect(bbox1, bbox2);
288288
}
289289

290-
function findBezierIntersections(bez1, bez2, justCount) {
290+
/**
291+
*
292+
* @param {import("./intersect").PathComponent} bez1
293+
* @param {import("./intersect").PathComponent} bez2
294+
* @param {boolean} justCount
295+
* @returns {import("./intersect").Intersection}
296+
*/
297+
export function findBezierIntersections(bez1, bez2, justCount) {
291298

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

@@ -455,7 +462,6 @@ export default function findPathIntersections(path1, path2, justCount) {
455462
return res;
456463
}
457464

458-
459465
function pathToAbsolute(pathArray) {
460466

461467
if (!pathArray || !pathArray.length) {
@@ -682,7 +688,7 @@ function arcToCurve(x1, y1, rx, ry, angle, large_arc_flag, sweep_flag, x2, y2, r
682688
// Source: http://blog.hackers-cafe.net/2009/06/how-to-calculate-bezier-curves-bounding.html
683689
// Original version: NISHIO Hirokazu
684690
// Modifications: https://github.com/timo22345
685-
function curveBBox(x0, y0, x1, y1, x2, y2, x3, y3) {
691+
export function curveBBox(x0, y0, x1, y1, x2, y2, x3, y3) {
686692
var tvalues = [],
687693
bounds = [ [], [] ],
688694
a, b, c, t, t1, t2, b2ac, sqrtb2ac;
@@ -789,6 +795,22 @@ function getPathCurve(path) {
789795
return pathClone(pth.curve = pathToCurve(abs));
790796
}
791797

798+
/**
799+
*
800+
* @param {import("./intersect").Path} path
801+
* @returns {import("./intersect").PathComponent[]}
802+
*/
803+
export function parseCurve(path) {
804+
805+
const abs = (pathToAbsolute(
806+
!Array.isArray(path) ?
807+
parsePathString(path) :
808+
path)
809+
);
810+
811+
return pathToCurve(abs);
812+
}
813+
792814
function pathToCurve(absPath) {
793815

794816
var curvedPath = pathClone(absPath),

0 commit comments

Comments
 (0)