block by nitaku 8751669

Boolean operations on 2D shapes

Full Screen

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.

index.js

(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);

index.html

<!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="//d3js.org/d3.v3.min.js"></script>
        <script src="//jsclipper.sourceforge.net/6.1.3.1/clipper.js"></script>
    </head>
    <body>
    </body>
    <script src="index.js"></script>
</html>

index.coffee

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)")
        

index.css

path {
  fill: orange;
  stroke: black;
  shape-rendering: crispEdges;
}

.original {
  fill: teal;
  fill-opacity: 0.5;
}

index.sass

path
    fill: orange
    stroke: black
    shape-rendering: crispEdges
    
.original
    fill: teal
    fill-opacity: 0.5