block by nitaku 8df0fb99b5e30a4785ea

Merging a lot of squares

Full Screen

A crash test for the versatile ClipperJS geometric library. A grid made by 128x128 square cells is populated, creating a square with a probability of 90% or a hole in the remaining 10%. The squares are then merged to create a single region, that is finally displayed in a zoomable SVG.

The heaviest operation turns out to be the merging (surprise!), taking the vast majority of the execution time (you can open a JavaScript console to see when each operation begins).

index.js

(function() {
  
// noprotect
;
  var SCALE, SIDE, cpr, height, i, j, map, merge, squares, succeeded, svg, width, zoom, zoomable_layer, _i, _j;

  console.debug('Generating squares...');

  SIDE = 128;

  cpr = new ClipperLib.Clipper();

  squares = new ClipperLib.Paths();

  merge = new ClipperLib.Paths();

  for (i = _i = 0; 0 <= SIDE ? _i < SIDE : _i > SIDE; i = 0 <= SIDE ? ++_i : --_i) {
    for (j = _j = 0; 0 <= SIDE ? _j < SIDE : _j > SIDE; j = 0 <= SIDE ? ++_j : --_j) {
      if (Math.random() < 0.1) {
        continue;
      }
      squares.push([
        {
          X: j,
          Y: i
        }, {
          X: j + 1,
          Y: i
        }, {
          X: j + 1,
          Y: i + 1
        }, {
          X: j,
          Y: i + 1
        }
      ]);
    }
  }

  console.debug('Merging...');

  cpr.AddPaths(squares, ClipperLib.PolyType.ptSubject, true);

  succeeded = cpr.Execute(ClipperLib.ClipType.ctUnion, merge);

  console.debug('Drawing...');

  SCALE = 400 / SIDE;

  svg = d3.select('svg');

  width = svg.node().getBoundingClientRect().width;

  height = svg.node().getBoundingClientRect().height;

  svg.attr({
    viewBox: "" + (-width / 2) + " " + (-height / 2) + " " + width + " " + height
  });

  zoomable_layer = svg.append('g');

  zoom = d3.behavior.zoom().scaleExtent([1, 16]).on('zoom', function() {
    return zoomable_layer.attr({
      transform: "translate(" + (zoom.translate()) + ")scale(" + (zoom.scale()) + ")"
    });
  });

  svg.call(zoom);

  map = zoomable_layer.append('g').attr({
    transform: "translate(" + (-SCALE * SIDE / 2) + ", " + (-SCALE * SIDE / 2) + ")"
  });

  map.append('path').datum(merge).attr({
    "class": 'land',
    d: function(merge) {
      var d_str, linestring, _k, _len;

      d_str = '';
      for (_k = 0, _len = merge.length; _k < _len; _k++) {
        linestring = merge[_k];
        d_str += 'M' + linestring.map(function(p) {
          return "" + (SCALE * p.X) + " " + (SCALE * p.Y);
        }).join('L') + 'z';
      }
      return d_str;
    }
  });

  map.append('rect').attr({
    "class": 'boundary',
    width: SCALE * SIDE,
    height: SCALE * SIDE
  });

}).call(this);

index.html

<!DOCTYPE html>
<html>
	<head>
        <meta charset="utf-8">
        <meta name="description" content="Merging a lot of squares" />
        <title>Merging a lot of squares</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>
        <svg height="500" width="960"></svg>
        <script src="index.js"></script>
	</body>
</html>

index.coffee

`
// noprotect
`

# generate a lot of random squares and merge them
console.debug 'Generating squares...'
SIDE = 128

cpr = new ClipperLib.Clipper()
squares = new ClipperLib.Paths()
merge = new ClipperLib.Paths()

for i in [0...SIDE]
  for j in [0...SIDE]
    continue if Math.random() < 0.1
    
    squares.push [{X:j,Y:i},{X:j+1,Y:i},{X:j+1,Y:i+1},{X:j,Y:i+1}]
    
console.debug 'Merging...'
#ClipperLib.JS.ScaleUpPaths(squares, 100)
cpr.AddPaths(squares, ClipperLib.PolyType.ptSubject, true)  
succeeded = cpr.Execute(ClipperLib.ClipType.ctUnion, merge)
#ClipperLib.JS.ScaleDownPaths(merge, 100)

# drawing
console.debug 'Drawing...'
SCALE = 400/SIDE

svg = d3.select('svg')
width = svg.node().getBoundingClientRect().width
height = svg.node().getBoundingClientRect().height

# translate the viewBox to have (0,0) at the center of the vis
svg
  .attr
    viewBox: "#{-width/2} #{-height/2} #{width} #{height}"
    
# append a group for zoomable content
zoomable_layer = svg.append('g')
 
# define a zoom behavior
zoom = d3.behavior.zoom()
  .scaleExtent([1,16]) # min-max zoom -  a value of 1 represent the initial zoom
  .on 'zoom', () ->
    zoomable_layer
      .attr
        transform: "translate(#{zoom.translate()})scale(#{zoom.scale()})"
    

# bind the zoom behavior to the main SVG (this is needed to have pan work on empty space - a group would pan only when dragging child elements)
svg.call(zoom)

map = zoomable_layer.append('g')
  .attr
    transform: "translate(#{-SCALE*SIDE/2}, #{-SCALE*SIDE/2})"
    
map.append('path')
  .datum(merge)
  .attr
    class: 'land'
    d: (merge) ->
      d_str = ''
      for linestring in merge
        d_str += 'M' + linestring.map((p) -> "#{SCALE*p.X} #{SCALE*p.Y}").join('L') + 'z'
      return d_str
    
map.append('rect')
  .attr
    class: 'boundary'
    width: SCALE*SIDE
    height: SCALE*SIDE
    

index.css

svg {
  background: white;
}
.boundary {
  fill: none;
  shape-rendering: crispEdges;
  stroke: black;
  stroke-width: 2px;
  vector-effect: non-scaling-stroke;
}
.land {
  fill: teal;
  fill-opacity: 0.3;
  shape-rendering: crispEdges;
  stroke: teal;
  stroke-width: 1px;
  vector-effect: non-scaling-stroke;
}