Skip to content

Instantly share code, notes, and snippets.

@nitaku
Last active September 17, 2023 11:05
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nitaku/8751669 to your computer and use it in GitHub Desktop.
Save nitaku/8751669 to your computer and use it in GitHub Desktop.
Boolean operations on 2D shapes

This example shows the results (orange) of performing four different boolean operations (union, difference, xor and intersection) on two 2D shapes (blue). Thanks to the powerful clipper.js library, the computation is performed in client-side Javascript.

The example is almost entirely taken from this clipper.js demo.

width = 960
height = 500
### create the SVG ###
svg = d3.select('body').append('svg')
.attr('width', width)
.attr('height', height)
### define subject and clip paths ###
subj_paths = [
[{X:10,Y:10},{X:110,Y:10},{X:110,Y:110},{X:10,Y:110}],
[{X:20,Y:20},{X:20,Y:100},{X:100,Y:100},{X:100,Y:20}]
]
clip_paths = [
[{X:50,Y:50},{X:150,Y:50},{X:150,Y:150},{X:50,Y:150}],
[{X:60,Y:60},{X:60,Y:140},{X:140,Y:140},{X:140,Y:60}]
]
### create and instruct Clipper to work with the provided paths ###
cpr = new ClipperLib.Clipper()
### true for closed paths ###
cpr.AddPaths(subj_paths, ClipperLib.PolyType.ptSubject, true)
cpr.AddPaths(clip_paths, ClipperLib.PolyType.ptClip, true)
### perform a UNION, a DIFFERENCE, a XOR and an INTERSECTION ###
solutions = []
for clip_type in [ClipperLib.ClipType.ctUnion, ClipperLib.ClipType.ctDifference, ClipperLib.ClipType.ctXor, ClipperLib.ClipType.ctIntersection]
solution_paths = new ClipperLib.Paths()
succeeded = cpr.Execute(clip_type, solution_paths, ClipperLib.PolyFillType.pftNonZero, ClipperLib.PolyFillType.pftNonZero)
if not succeeded
throw new Error('Clipper operation failed!')
solutions.push solution_paths
### Converts Paths to SVG path string ###
### and scales down the coordinates ###
### from http://jsclipper.sourceforge.net/6.1.3.1/index.html?p=starter_boolean.html ###
paths2string = (paths, scale) ->
svgpath = ''
if not scale?
scale = 1
for path in paths
for p, i in path
if i is 0
svgpath += 'M'
else
svgpath += 'L'
svgpath += p.X/scale + ", " + p.Y/scale
svgpath += 'Z'
if svgpath is ''
svgpath = 'M0,0'
return svgpath
### display all the solutions in SVG ###
svg.selectAll('path')
.data(solutions)
.enter().append('path')
.attr('d', (d) -> paths2string(d))
.attr('transform', (d,i) -> "translate(#{width/2+(i-2)*200+50},#{height/2+25})")
### display the original paths as reference ###
for path in [subj_paths, clip_paths]
svg.append('path')
.attr('class', 'original')
.attr('d', paths2string(path))
.attr('transform', "translate(#{width/2-75},50)")
path {
fill: orange;
stroke: black;
shape-rendering: crispEdges;
}
.original {
fill: teal;
fill-opacity: 0.5;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Boolean operations on 2D shapes</title>
<link type="text/css" href="index.css" rel="stylesheet"/>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://jsclipper.sourceforge.net/6.1.3.1/clipper.js"></script>
</head>
<body>
</body>
<script src="index.js"></script>
</html>
(function() {
var clip_paths, clip_type, cpr, height, path, paths2string, solution_paths, solutions, subj_paths, succeeded, svg, width, _i, _j, _len, _len2, _ref, _ref2;
width = 960;
height = 500;
/* create the SVG
*/
svg = d3.select('body').append('svg').attr('width', width).attr('height', height);
/* define subject and clip paths
*/
subj_paths = [
[
{
X: 10,
Y: 10
}, {
X: 110,
Y: 10
}, {
X: 110,
Y: 110
}, {
X: 10,
Y: 110
}
], [
{
X: 20,
Y: 20
}, {
X: 20,
Y: 100
}, {
X: 100,
Y: 100
}, {
X: 100,
Y: 20
}
]
];
clip_paths = [
[
{
X: 50,
Y: 50
}, {
X: 150,
Y: 50
}, {
X: 150,
Y: 150
}, {
X: 50,
Y: 150
}
], [
{
X: 60,
Y: 60
}, {
X: 60,
Y: 140
}, {
X: 140,
Y: 140
}, {
X: 140,
Y: 60
}
]
];
/* create and instruct Clipper to work with the provided paths
*/
cpr = new ClipperLib.Clipper();
/* true for closed paths
*/
cpr.AddPaths(subj_paths, ClipperLib.PolyType.ptSubject, true);
cpr.AddPaths(clip_paths, ClipperLib.PolyType.ptClip, true);
/* perform a UNION, a DIFFERENCE, a XOR and an INTERSECTION
*/
solutions = [];
_ref = [ClipperLib.ClipType.ctUnion, ClipperLib.ClipType.ctDifference, ClipperLib.ClipType.ctXor, ClipperLib.ClipType.ctIntersection];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
clip_type = _ref[_i];
solution_paths = new ClipperLib.Paths();
succeeded = cpr.Execute(clip_type, solution_paths, ClipperLib.PolyFillType.pftNonZero, ClipperLib.PolyFillType.pftNonZero);
if (!succeeded) throw new Error('Clipper operation failed!');
solutions.push(solution_paths);
}
/* Converts Paths to SVG path string
*/
/* and scales down the coordinates
*/
/* from http://jsclipper.sourceforge.net/6.1.3.1/index.html?p=starter_boolean.html
*/
paths2string = function(paths, scale) {
var i, p, path, svgpath, _j, _len2, _len3;
svgpath = '';
if (!(scale != null)) scale = 1;
for (_j = 0, _len2 = paths.length; _j < _len2; _j++) {
path = paths[_j];
for (i = 0, _len3 = path.length; i < _len3; i++) {
p = path[i];
if (i === 0) {
svgpath += 'M';
} else {
svgpath += 'L';
}
svgpath += p.X / scale + ", " + p.Y / scale;
}
svgpath += 'Z';
}
if (svgpath === '') svgpath = 'M0,0';
return svgpath;
};
/* display all the solutions in SVG
*/
svg.selectAll('path').data(solutions).enter().append('path').attr('d', function(d) {
return paths2string(d);
}).attr('transform', function(d, i) {
return "translate(" + (width / 2 + (i - 2) * 200 + 50) + "," + (height / 2 + 25) + ")";
});
/* display the original paths as reference
*/
_ref2 = [subj_paths, clip_paths];
for (_j = 0, _len2 = _ref2.length; _j < _len2; _j++) {
path = _ref2[_j];
svg.append('path').attr('class', 'original').attr('d', paths2string(path)).attr('transform', "translate(" + (width / 2 - 75) + ",50)");
}
}).call(this);
path
fill: orange
stroke: black
shape-rendering: crispEdges
.original
fill: teal
fill-opacity: 0.5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment