Giter Club home page Giter Club logo

isect's Introduction

isect - intersection detection library build status

This library allows you to find all intersections in a given set of segments.

demo

Try the demo online

Algorithms

The library implements three algorithms

Bentley-Ottmann sweep line algorithm

This algorithm has O(n*log(n) + k*log(n)) performance, where n is number of segments, and k is number of intersections.

This method is preferred when you have large number of lines, and not too many intersections (k = o(n^2/log(n)), to be more specific).

The algorithm follows "Computation Geometry, Algorithms and Applications" book by Mark de Berg, Otfried Cheong, Marc van Kreveld, and Mark Overmars. It does support degenerate cases, though read limitations to learn more.

demo

Brute force algorithm

This is "naive" implementation where each segment is compared with all other segments, and thus has O(n*n) performance.

Despite its naiveté, it works much faster than Bentley-Ottmann algorithm for the cases when there are a few thousand lines and millions of intersections. This scenario is common in force-based graph drawing, where "hairball" is formed by a few thousand lines.

demo

"Bush" algorithm

This algorithm was suggested by @mourner and @dy. It uses mourner/flatbush as a spatial index of segments, and then iterates over every segment, checking overlapping bounding boxes.

Intuitively, worst case performance of this algorithm is comparable with brute force. When every segment overlaps with every other segment we should expect O(n^2) operations. In practice, however, this algorithm beats both Bentley-Ottman and Brute force approaches.

Its beauty is in its simplicity. It adapts very well to both sparse and dense segments distribution.

You can also find performance test suite below, so you can decide for yourself. I would absolutely go with this algorithm as my default choice.

Performance

The benchmark code is available here. Higher ops per second is better!

K12 graph

K12 graph

  • Sweep: Circular lines 12x40 x 1,069 ops/sec ±1.98% (91 runs sampled)
  • Brute: Circular lines 12x40 x 7,463 ops/sec ±3.01% (76 runs sampled)
  • Bush: Circular lines 12x40 x 5,678 ops/sec ±2.20% (80 runs sampled)

The graph has only 66 unique segments, and 313 unique intersections. Brute force algorithm is 7x faster than Sweep Line, closely followed by

100 random lines

100 random lines

In this demo 100 lines are randomly sampled inside a box with a side of 42px.

  • Sweep: 100 Random lines lines in 42px box x 277 ops/sec ±1.20% (87 runs sampled)
  • Brute: 100 Random lines lines in 42px box x 3,606 ops/sec ±3.61% (74 runs sampled)
  • Bush: 100 Random lines in 42px box x 3,314 ops/sec ±2.66% (83 runs sampled)

Again, the brute force algorithm wins. The distance between brute force and Bush shortens. Sweep line comes last.

Sparse intersections

sparse

  • Sweep: 2,500 sparse lines x 156 ops/sec ±0.97% (80 runs sampled)
  • Brute: 2,500 sparse lines x 13.62 ops/sec ±0.91% (38 runs sampled)
  • Bush: 2,500 sparse lines x 592 ops/sec ±1.05% (93 runs sampled)

Now Bush algorithm wins by huge margin. Bentley-Ottman comes second, and brute force comes the last.

Parallel Slanted lines

slanted

  • Sweep: 1000 slanted, not intersect x 622 ops/sec ±1.23% (91 runs sampled)
  • Brute: 1000 slanted, not intersect x 230 ops/sec ±2.37% (87 runs sampled)
  • Bush: 1000 slanted, not intersect x 243 ops/sec ±1.07% (87 runs sampled)

In this example there too many lines, and none of them intersect. Furthermore, most of the rectangular bounding boxes do intersect, which gives more work for the bush algorithm

usage

Install the module from npm:

npm i isect

Or download from CDN:

<script src='https://cdn.rawgit.com/anvaka/isect/v2.0.0/build/isect.min.js'></script>

If you download from CDN the library will be available under isect global name.

Basic usage

The code below detects all intersections between segments in the array:

var isect = require('isect');

// Prepare the library to detect all intersection
var detectIntersections = isect.bush([{
  from: {x:  0, y:  0},
  to:   {x: 10, y: 10}
}, {
  from: {x:  0, y: 10},
  to:   {x: 10, y:  0}
}]);

// Detect them all, operation is synchronous. 
var intersections = detectIntersections.run();
console.log(intersections);
// Prints:
// 
//    [ { point: { x: 5, y: 5 }, segments: [ [Object], [Object] ] } ]
// 
// array of segments contain both segments.

Brute force and Sweep Line

You can also run the above example with a different algorithm. Simply change .bush() to .sweep() (to run Bentley-Ottman) or to .brute() (to try brute force):

var isect = require('isect');

// Prepare the library to detect all intersection
var bruteForce = isect.brute([{
  from: {x:  0, y:  0},
  to:   {x: 10, y: 10}
}, {
  from: {x:  0, y: 10},
  to:   {x: 10, y:  0}
}]);

var intersections = bruteForce.run();
console.log(intersections);

// do the same with sweep line:
var sweepLine = isect.sweep([{
  from: {x:  0, y:  0},
  to:   {x: 10, y: 10}
}, {
  from: {x:  0, y: 10},
  to:   {x: 10, y:  0}
}]);

var intersections = sweepLine.run();
console.log(intersections);

All algorithms have identical API. In every example below you can replace .bush() with .sweeep() or .brute() - just pay attention to notes that calls out a discrepancies in the API.

Early stopping

If you don't care about all intersections, but want to know if there is at least one intersection, you can pass a onFound() callback and request the library to stop as soon as it finds an intersection:

var intersections = isect.bush([/* array of segments */], {
  onFound(point, segments) {
    // `point` is {x, y} of the intersection,
    // `segments` are intersecting segments.

    // If you return true from this method, no further processing will be done:

    return true; // yes, stop right now!
  }
});

Asynchronous workflow

If you want to give browser time to catch up with user input, it may be desirable to break the algorithm into chunks (so that UI thread is not swamped). You can do this by calling .step() method of the algorithm's instance:

var detector = isect.bush([/* array of segments */]);
// instead of detector.run(), we do:
var isDone = detector.step()
// isDone will be set to true, once the algorithm is completed.

This is precisely how I do step-by-step animation of the algorithm:

demo

Click here to see it in action.

Limitations

The sweep line algorithm is susceptible to floating point rounding errors. It is possible to construct an example, with nearly horizontal lines, that would cause it to fail.

While sweep line implementation detects point-segment overlap, I didn't implement point-point overlap. I.e. identical points in the input array that do not overlap any segment are not reported.

Miscellaneous

  • The source code for the demo is available here.
  • The sweep line algorithm requires a binary search tree. I'm using w8r/splay-tree for this purpose. Love the library a lot! I have also tried AVL tree, but found their performance worse than splay tree.
  • If you need a sweep line with higher precision, consider porting this library to use decimal.js.
  • I would absolutely love to have faster intersection algorithms implemented in JavaScript. If you know any - please share. In particular this paper An optimal algorithm for finding segments intersections looks very promising! Their runtime is O(n * log(n) + k) which should be faster than Bentley-Ottmann.

License

MIT

Thanks!

I hope you enjoy the library. Feel free to ping me ([email protected] or https://twitter.com/anvaka) if you have any feedback.

isect's People

Contributors

anvaka avatar georeith avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

isect's Issues

Update of Typescript Types

Hey and thank you for this library, i was also reading "Computation Geometry, Algorithms and Applications" and i was very confused by the type of tree they used there (with information only on the leaf nodes) and how to implement these algorithms with the normal tree (which is from what i can tell has been done here in the case of bentley-ottmann, but please correct me)

so this implementation has helped me alot !

Through usage i have noticed that the TS types could be improved;

I have posted below the declarations i am using locally, i have

  1. added types for options
  2. added types for individual functions in the format that they are exported
  3. i have added a generic param as i have noticed you can add your own metadata to the objects and get this served back to you in the results which i found useful. However i may be levaraging something i am not meant to here.

If its agreeable i will open a pr, just thought i would get some initial thoughts before i did this

Thanks again !

export type Position = {
        x: number;
        y: number;
    };

export type OutputSegment<SEGMENT_META extends MetaBase> = {
    dy: number;
    dx: number;
    angle: number;
} & InputSegment<SEGMENT_META>;

export type OutputIntersection<SEGMENT_META extends MetaBase> = {
    point: Position;
    segments: Array<OutputSegment<SEGMENT_META>>;
};

export type MetaBase = Record<string, any>;

export type InputSegment<SEGMENT_META extends MetaBase> = {
    from: Position;
    to: Position;
} & SEGMENT_META;

export type FunctionResult<SEGMENT_META extends MetaBase> = {
    run: () => Array<OutputIntersection<SEGMENT_META>>;
};

export type Options<SEGMENT_META extends MetaBase> = {
    onError?: () => void;
    onFound?: (intersection: OutputIntersection<SEGMENT_META>) => void;
};

export function sweep<SEGMENT_META extends MetaBase = MetaBase>(
    segments: Array<InputSegment<SEGMENT_META>>,
    opt?: Options<SEGMENT_META>,
): FunctionResult<SEGMENT_META>;

export function brute<SEGMENT_META extends MetaBase = MetaBase>(
    segments: Array<InputSegment<SEGMENT_META>>,
    opt?: Options<SEGMENT_META>,
): FunctionResult<SEGMENT_META>;

export function bush<SEGMENT_META extends MetaBase = MetaBase>(
    segments: Array<InputSegment<SEGMENT_META>>,
    opt?: Options<SEGMENT_META>,
): FunctionResult<SEGMENT_META>;

graphing intersections

Hi first of all thank you for this library, its extremely useful and just what I was looking for.

I'm attempting to use isect to combine a series of lines into a graph that I can then perform A* routing on, however I'm finding that some adjacent nodes are not linked to each other:

Frame 2 (3)

I'm using isect and ngraph.graph and ngraph.path to do so, building my graph like so:

const intersections = detectIntersections.run();
  const graph = createGraph<{ point: { x: number; y: number } }>();
  intersections.forEach((intersection) => {
    const intersectionId = isectPointToId(intersection.point);
    graph.addNode(intersectionId, { point: intersection.point });
    intersection.segments.forEach((isectSegment) => {
      const fromId = isectPointToId(isectSegment.from);
      const toId = isectPointToId(isectSegment.to);
      graph.addNode(fromId, { point: isectSegment.from });
      graph.addNode(toId, { point: isectSegment.to });
      graph.addLink(fromId, intersectionId);
      graph.addLink(toId, intersectionId);
    });
  });

Is my expectation that all adjacent intersections should be linked by linking the intersections as I do wrong?

Here's the line data used to generate those intersections:

https://gist.github.com/georeith/d83510b39718ac4980f6179cbe2c2728

Ivan Balaban's Optimal Algorithm in O(n log n + k)

It would be worth adding Ivan Balaban's "Optimal Algorithm" which hits the theoretical lower bound for segment intersection time and space complexity of O(n log n + k) and O(n), respectively.

Here is a link to his paper: https://www2.cs.sfu.ca/~binay/813.2011/Balaban.pdf

Also, he seems to have created a C++ implementation here: https://github.com/ivvaan/balaban-segments-intersections

Some additional work would need to be done to handle degenerate cases.

isect/brute, isect/sweep, isect/bush entries

Thanks for the incredible work @anvaka!

That would be nice to have separate file entries for each algorithm, to reduce amount of bundled code and simplify API:

import isect from "isect/brute"
isect(options)

//or
require("isect/bush")(data)

Just suggestion.

Another intersection library

Hey @anvaka

Just letting you know that I've tackled a similar problem over in this repo using the shamos-hoey algorithm.

In my case however I'm dealing with connected segments (like polygons or linestrings) and I'm looking for genuine crossing segments, not just touching endpoints.

Anyway just dropping the note here, nothing to action though :)

index.js has wrong path in package.json, breaks Vite/Esbuild

Because the index file has the wrong path, I get the following error when building a project that depends on this library:

✘ [ERROR] [plugin vite:dep-pre-bundle] Failed to resolve entry for package "isect". The package may have incorrect main/module/exports specified in its package.json.

    node_modules/.pnpm/[email protected]/node_modules/esbuild/lib/main.js:1365:21:
      1365 │         let result = await callback({
           ╵                      ^

    at packageEntryFailure (/node_modules/.pnpm/[email protected]/node_modules/vite/dist/node/chunks/dep-3007b26d.js:22004:11)
    at resolvePackageEntry (/node_modules/.pnpm/[email protected]/node_modules/vite/dist/node/chunks/dep-3007b26d.js:22001:5)
    at tryNodeResolve (/node_modules/.pnpm/[email protected]/node_modules/vite/dist/node/chunks/dep-3007b26d.js:21736:20)
    at Context.resolveId (/node_modules/.pnpm/[email protected]/node_modules/vite/dist/node/chunks/dep-3007b26d.js:21487:28)
    at Object.resolveId (/node_modules/.pnpm/[email protected]/node_modules/vite/dist/node/chunks/dep-3007b26d.js:41587:46)
    at processTicksAndRejections (internal/process/task_queues.js:95:5)
    at async /node_modules/.pnpm/[email protected]/node_modules/vite/dist/node/chunks/dep-3007b26d.js:61953:21
    at async /node_modules/.pnpm/[email protected]/node_modules/vite/dist/node/chunks/dep-3007b26d.js:22322:34
    at async requestCallbacks.on-resolve (/node_modules/.pnpm/[email protected]/node_modules/esbuild/lib/main.js:1365:22)
    at async handleRequest (/node_modules/.pnpm/[email protected]/node_modules/esbuild/lib/main.js:727:13)

  This error came from the "onResolve" callback registered here:

    node_modules/.pnpm/[email protected]/node_modules/esbuild/lib/main.js:1287:20:
      1287 │       let promise = setup({
           ╵                     ^

    at setup (/node_modules/.pnpm/[email protected]/node_modules/vite/dist/node/chunks/dep-3007b26d.js:22302:19)
    at handlePlugins (/node_modules/.pnpm/[email protected]/node_modules/esbuild/lib/main.js:1287:21)
    at buildOrServeImpl (/node_modules/.pnpm/[email protected]/node_modules/esbuild/lib/main.js:974:5)
    at Object.buildOrServe (/node_modules/.pnpm/[email protected]/node_modules/esbuild/lib/main.js:780:5)
    at /node_modules/.pnpm/[email protected]/node_modules/esbuild/lib/main.js:2132:17
    at new Promise (<anonymous>)
    at Object.build (/node_modules/.pnpm/[email protected]/node_modules/esbuild/lib/main.js:2131:14)
    at build (/node_modules/.pnpm/[email protected]/node_modules/esbuild/lib/main.js:1978:51)
    at runOptimizeDeps (/node_modules/.pnpm/[email protected]/node_modules/vite/dist/node/chunks/dep-3007b26d.js:42941:26)

  The plugin "vite:dep-pre-bundle" was triggered by this import

    node_modules/.pnpm/[email protected][email protected]/node_modules/three-svg-renderer/build/index.esm.js:3:21:
      3 │ import { bush } from 'isect';

My setup

  • Basically unmodified Vite + React + TS setup
  • Node 19
  • uses react-three-fiber which depends on isect

suggested fix

I've tested locally and verified that changing the package.json content fixes the issue.

Current package.json content

"module": "./src/index.js",

Requested package.json content

"module": "./index.js",

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.