Skip to content

Instantly share code, notes, and snippets.

@bmershon
Last active February 3, 2017 02:30
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 bmershon/fee0228bca8280f2e5cebdd9b11f2398 to your computer and use it in GitHub Desktop.
Save bmershon/fee0228bca8280f2e5cebdd9b11f2398 to your computer and use it in GitHub Desktop.
Mesh clipping
border: no
license: MIT
height: 960
// Version 0.0.0. Copyright 2017 Mike Bostock.
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(factory((global.d3 = global.d3 || {})));
}(this, function (exports) { 'use strict';
// Clips the specified subject polygon to the specified clip polygon;
// requires the clip polygon to be counterclockwise and convex.
// https://en.wikipedia.org/wiki/Sutherland–Hodgman_algorithm
exports.polygonClip = function(clip, subject) {
var input,
closed = polygonClosed(subject),
i = -1,
n = clip.length - polygonClosed(clip),
j,
m,
a = clip[n - 1],
b,
c,
d;
while (++i < n) {
input = subject.slice();
subject.length = 0;
b = clip[i];
c = input[(m = input.length - closed) - 1];
j = -1;
while (++j < m) {
d = input[j];
if (polygonInside(d, a, b)) {
if (!polygonInside(c, a, b)) {
subject.push(polygonIntersect(c, d, a, b));
}
subject.push(d);
} else if (polygonInside(c, a, b)) {
subject.push(polygonIntersect(c, d, a, b));
}
c = d;
}
if (closed) subject.push(subject[0]);
a = b;
}
return subject;
};
function polygonInside(p, a, b) {
return (b[0] - a[0]) * (p[1] - a[1]) < (b[1] - a[1]) * (p[0] - a[0]);
}
// Intersect two infinite lines cd and ab.
function polygonIntersect(c, d, a, b) {
var x1 = c[0], x3 = a[0], x21 = d[0] - x1, x43 = b[0] - x3,
y1 = c[1], y3 = a[1], y21 = d[1] - y1, y43 = b[1] - y3,
ua = (x43 * (y1 - y3) - y43 * (x1 - x3)) / (y43 * x21 - x43 * y21);
return [x1 + ua * x21, y1 + ua * y21];
}
// Returns true if the polygon is closed.
function polygonClosed(coordinates) {
var a = coordinates[0],
b = coordinates[coordinates.length - 1];
return !(a[0] - b[0] || a[1] - b[1]);
}
Object.defineProperty(exports, '__esModule', {value: true});
}));
<!DOCTYPE html>
<style>
circle {
cursor: move;
fill: #fff;
stroke: #000;
stroke-width: 1px;
}
circle.active {
stroke: #000;
stroke-width: 2px;
}
path.clip {
fill: rgba(0, 0, 0, 0.3);
stroke: #000;
stroke-width: 1px;
}
path.subject {
fill: rgba(0, 0, 0, 0);
stroke: #000;
stroke-width: 1px;
}
path.clipped {
fill: rgba(240, 0, 0, 1);
stroke: #000;
stroke-width: 3px;
}
</style>
<svg width="960" height="960"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="d3-polygon-clip.js"></script>
<script>
var svg = d3.select("svg"),
width = svg.attr("width"),
height = svg.attr("height");
var randomX = d3.randomNormal(width / 2, width * 0.1),
randomY = d3.randomNormal(height / 2, height * 0.1);
var sites = d3.range(20).map(function(d) {
return [randomX(), randomY()];
});
// Counter-clockwise orientation.
var clip = [[width / 2, height / 5],
[width / 5, 3 * height / 5],
[3 * width / 5, 3 * height / 5]];
var voronoi = d3.voronoi()
.extent([[width / 4, height / 4], [3 * width / 4, 3* height / 4]]);
var subject = svg.append("g")
.selectAll("path")
.data(voronoi.polygons(sites))
.enter().append("path")
.attr("class", "subject")
.attr("d", function(d) { return "M" + d.join("L") + "Z"; });
var clipPolygon = svg.append("path")
.datum(clip)
.attr("class", "clip");
// Control points for the clip polygon.
var circle = svg.selectAll("circle")
.data(clip);
var origin = [0, 0];
circle.enter()
.append("circle")
.attr("r", 9)
.attr("cx", function(d) { return d[0]; })
.attr("cy", function(d) { return d[1]; })
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
update();
// Redraw polygons in response to control point movement.
function update() {
// Ensure clip polygon has counter-clockwise orientation.
if (!ccw(clip)) clip.reverse();
clipPolygon.attr("d", function(d) { return "M" + d.join("L") + "Z"; });
clipped = svg.selectAll(".clipped")
.data(
voronoi.polygons(sites)
.map(function(d) {
d3.polygonClip(clip, d);
return d;
})
.filter(function(d) {
return d.length > 2;
})
, function(d) { return d; }); // Key on Object reference.
clipped.enter().insert("path")
.attr("class", "clipped")
.attr("d", function(d) { return "M" + d.join("L") + "Z"; });
clipped.exit().remove();
// Circles drawn on top of all other SVG elements.
d3.selectAll("circle").raise();
}
function dragstarted(d) {
d3.select(this).classed("active", true);
}
function dragged(d) {
d3.select(this)
.attr("cx", d[0] = d3.event.x)
.attr("cy", d[1] = d3.event.y)
update();
}
function dragended(d, i) {
d3.select(this).classed("active", false);
}
function polygonClone(polygon) {
var cloned = [],
i,
n;
for (i = 0, n = polygon.length; i < n; i++) {
cloned.push([polygon[i][0], polygon[i][1]]);
}
return cloned;
}
// Returns true of the given polygon has counter-clockwise orientation.
function ccw(polygon) {
function polygonInside(p, a, b) {
return (b[0] - a[0]) * (p[1] - a[1]) < (b[1] - a[1]) * (p[0] - a[0]);
}
return polygonInside(d3.polygonCentroid(polygon), polygon[0], polygon[1]);
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment