A low-level library for performing boolean operations on SVG paths.
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.
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));
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.
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.