PathBool.js

A low-level library for performing boolean operations on SVG paths.

View the Project on GitHub r-flash/PathBool.js

PathBool.js documentation

Demos

Installation

You can use npm or any compatible package manager:

npm install path-bool

Alternatively, you can just import from dist/path-bool.js, require() dist/path-bool.umd.js or load the UMD build using a <script src="...">, in which case the library will be exposed under the global variable PathBool.

There are also “core” builds available which don’t include the string path data parser.

A quick look at the API

import * as PathBool from "path-bool";

// initialize path from SVG path data...
const pathA = PathBool.pathFromPathData("M0,0 C...");
// ...or from an array of SVG path commands...
const pathA = PathBool.pathFromCommands([["M", [0, 0]], ["C", [/*...*/], /*...*/]/*...*/]);
// ...or directly from path segments (L, C, Q, or A with the start point prepended)
const pathA = [["C", [0, 0], [/*...*/], /*...*/], /*...*/];

const fillRuleA = PathBool.FillRule.EvenOdd;
const pathB = PathBool.pathFromPathData("M0,0 C...");
const fillRuleB = PathBool.FillRule.NonZero;

// Build the arrangement once from any number of { path, fillRule } inputs...
const pathBoolean = new PathBool.PathBoolean([
    { path: pathA, fillRule: fillRuleA },
    { path: pathB, fillRule: fillRuleB },
]);

// ...then select results for one or more operations (the heavy work is reused).
const result = pathBoolean.get(PathBool.PathBooleanOperation.Union);

console.log(result.map(PathBool.pathToPathData));
console.log(result.map(PathBool.pathToCommands));

Usage

The main entry point is the PathBoolean class:

type PathBooleanInput = {
    path: Path;
    fillRule: FillRule;
};

class PathBoolean {
    constructor(inputs: PathBooleanInput[]);
    get(op: PathBooleanOperation): Path[];
    getFaces(): Path[];
    buildShape(indices: Iterable<number>): Path;
}

The constructor takes any number of { path, fillRule } inputs and does the heavy geometric work (intersection, graph building, and face flagging) up front. You then call get(op) for each operation you want; this reuses everything computed in the constructor, so asking for several operations on the same inputs is cheap.

The output array is empty if the input paths are empty. It contains exactly one Path when the operation is Union, Difference, Intersection, or Exclusion. Potentially multiple Paths are output for operations Division and Fracture.

With more than two inputs, the order-dependent operations generalize by a left-fold (“the first path vs. the rest”): Difference is the first path minus the union of the others, Exclusion keeps the regions covered by an odd number of paths, and Division slices the first path.

Shape builder

getFaces and buildShape support interactive “shape builder” tools. The same PathBoolean object reuses the arrangement it already computed:

// One Path per atomic region (the same regions, in the same order, that
// `Fracture` produces). Use the array index as a stable handle.
const faces = pathBoolean.getFaces();

// Merge a chosen subset of those regions (by index) into a single shape,
// tracing the outline of their union, with holes where appropriate. Indices
// out of range are ignored.
const shape = pathBoolean.buildShape([0, 2, 5]);

Render getFaces() as the selectable pieces, track which indices the user picks, and call buildShape to get the combined outline. Unlike get, which always wraps its result in an array, buildShape returns a single Path (which may contain several subpaths and holes). See the shape builder demo.

Here, FillRule is an enum (see fill-rule documentation on MDN for details):

enum FillRule {
    NonZero,
    EvenOdd,
}

PathBooleanOperation is an enum:

enum PathBooleanOperation {
    Union,        // logical OR
    Difference,   // A and not B
    Intersection, // logical AND
    Exclusion,    // logical XOR
    Division,     // use B to slice A into partitions
    Fracture,     // output all partitions
}

Path is an array of PathSegments, which in turn are defined as:

type Vector = [number, number];

type PathLineSegment = ["L", Vector, Vector];

type PathCubicSegment = ["C", Vector, Vector, Vector, Vector];

type PathQuadraticSegment = ["Q", Vector, Vector, Vector];

type PathArcSegment = [
    "A",
    Vector,
    number, // rx
    number, // ry
    number, // rotation
    boolean, // large-arc-flag
    boolean, // sweep-flag
    Vector,
];

type PathSegment =
    | PathLineSegment
    | PathCubicSegment
    | PathQuadraticSegment
    | PathArcSegment;

They correspond to the L, C, Q, and A SVG path commands with the start point (the previous point, if you will) as the second element after the letter. This is the minimal representation of SVG path data, but it’s not very useful if your inputs and outputs are SVG paths.

Therefore, there are two other representations that you can convert to and from Paths.

The middle ground are arrays of PathCommands. This is a representation that you should be able to obtain from any other representation without substantial pain. PathCommands are defined as:

type PathCommand =
    | ["M", Vector]
    | ["L", Vector]
    | ["C", Vector, Vector, Vector]
    | ["S", Vector, Vector]
    | ["Q", Vector, Vector]
    | ["T", Vector]
    | ["A", number, number, number, boolean, boolean, Vector]
    | ["Z"]
    | ["z"]
    | ["H", number]
    | ["V", number]
    | ["m", number, number]
    | ["l", number, number]
    | ["h", number]
    | ["v", number]
    | ["c", number, number, number, number, number, number]
    | ["s", number, number, number, number]
    | ["q", number, number, number, number]
    | ["t", number, number]
    | ["a", number, number, number, boolean, boolean, number, number];

The command letters and order of parameters match with what you would write in the d attribute of a <path>. Absolute coordinates are encoded as Vectors, i.e., [number, number] arrays.

There is the following pair of functions to convert Paths to and from this representation:

const path = PathBool.pathFromCommands(commands);
const commands = PathBool.pathToCommands(path);

Another option is working with d attributes directly:

const path = PathBool.pathFromPathData("M 0,0 h 10 v 10 z");
const pathData = PathBool.pathToPathData(path);

Please bear in mind that the parser behind pathFromPathData is not fully standards-compliant. For one, it throws when encountering bad or weird data, rather than outputting the good part of the parse.