Skip to content

Instantly share code, notes, and snippets.

@nitaku
Last active August 29, 2015 14:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nitaku/8df0fb99b5e30a4785ea to your computer and use it in GitHub Desktop.
Save nitaku/8df0fb99b5e30a4785ea to your computer and use it in GitHub Desktop.
Merging a lot of squares

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

`
// 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
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;
}
<!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="http://d3js.org/d3.v3.min.js"></script>
<script src="http://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>
(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);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment