Commit 5596e4fb authored by Mike Bostock's avatar Mike Bostock
Browse files

Standardize on open polygons. Fixes #443.

If you try to create a d3.geom.polygon with a closed polygon, it is now
automatically converted to an open polygon by stripping the closing coordinate.
parent 673c630e
import "geom"; import "geom";
// Note: removes the closing coordinate if the polygon is not already open.
d3.geom.polygon = function(coordinates) { d3.geom.polygon = function(coordinates) {
d3_geom_polygonOpen(coordinates);
coordinates.area = function() { coordinates.area = function() {
var i = -1, var i = -1,
...@@ -36,7 +38,7 @@ d3.geom.polygon = function(coordinates) { ...@@ -36,7 +38,7 @@ d3.geom.polygon = function(coordinates) {
}; };
// The Sutherland-Hodgman clipping algorithm. // The Sutherland-Hodgman clipping algorithm.
// Note: requires the clip polygon to be counterclockwise and convex. // Note: requires the clip polygon to be open, counterclockwise and convex.
coordinates.clip = function(subject) { coordinates.clip = function(subject) {
var input, var input,
i = -1, i = -1,
...@@ -84,3 +86,10 @@ function d3_geom_polygonIntersect(c, d, a, b) { ...@@ -84,3 +86,10 @@ function d3_geom_polygonIntersect(c, d, a, b) {
ua = (x43 * (y1 - y3) - y43 * (x1 - x3)) / (y43 * x21 - x43 * y21); ua = (x43 * (y1 - y3) - y43 * (x1 - x3)) / (y43 * x21 - x43 * y21);
return [x1 + ua * x21, y1 + ua * y21]; return [x1 + ua * x21, y1 + ua * y21];
} }
// If coordinates is not open, removes the closing point.
function d3_geom_polygonOpen(coordinates) {
var a = coordinates[0],
b = coordinates[coordinates.length - 1];
if (!(a[0] - b[0] || a[1] - b[1])) coordinates.pop();
}
...@@ -12,70 +12,99 @@ suite.addBatch({ ...@@ -12,70 +12,99 @@ suite.addBatch({
topic: function(polygon) { topic: function(polygon) {
return polygon([[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]); return polygon([[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]);
}, },
"is converted to an open polygon": function(p) {
assertPolygonInDelta(p, [[0, 0], [0, 1], [1, 1], [1, 0]]);
},
"has area 1": function(p) { "has area 1": function(p) {
assert.equal(p.area(), 1); assert.equal(p.area(), 1);
}, },
"has centroid ⟨.5,.5⟩": function(p) { "has centroid ⟨.5,.5⟩": function(p) {
assert.deepEqual(p.centroid(), [.5, .5]); assertPointInDelta(p.centroid(), [.5, .5]);
},
"can clip an open counterclockwise triangle": function(p) {
assertPolygonInDelta(p.clip([[0.9, 0.5], [2, -1], [0.5, 0.1]]), [[0.9, 0.5], [1, 0.363636], [1, 0], [0.636363, 0], [0.5, 0.1]], 1e-4);
} }
}, },
"closed clockwise unit square": { "closed clockwise unit square": {
topic: function(polygon) { topic: function(polygon) {
return polygon([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]); return polygon([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]);
}, },
"is converted to an open polygon": function(p) {
assertPolygonInDelta(p, [[0, 0], [1, 0], [1, 1], [0, 1]]);
},
"has area 1": function(p) { "has area 1": function(p) {
assert.equal(p.area(), -1); assert.equal(p.area(), -1);
}, },
"has centroid ⟨.5,.5⟩": function(p) { "has centroid ⟨.5,.5⟩": function(p) {
assert.deepEqual(p.centroid(), [.5, .5]); assertPointInDelta(p.centroid(), [.5, .5]);
},
"is not currently supported for clipping": function(p) {
// because clipping requires a counterclockwise source polygon
} }
}, },
"closed clockwise triangle": { "closed clockwise triangle": {
topic: function(polygon) { topic: function(polygon) {
return polygon([[1, 1], [3, 2], [2, 3], [1, 1]]); return polygon([[1, 1], [3, 2], [2, 3], [1, 1]]);
}, },
"is converted to an open polygon": function(p) {
assertPolygonInDelta(p, [[1, 1], [3, 2], [2, 3]]);
},
"has area 1.5": function(p) { "has area 1.5": function(p) {
assert.equal(p.area(), -1.5); assert.equal(p.area(), -1.5);
}, },
"has centroid ⟨2,2⟩": function(p) { "has centroid ⟨2,2⟩": function(p) {
var centroid = p.centroid(); assertPointInDelta(p.centroid(), [2, 2]);
assert.inDelta(centroid[0], 2, 1e-6); },
assert.inDelta(centroid[1], 2, 1e-6); "is not currently supported for clipping": function(p) {
// because clipping requires a counterclockwise source polygon
} }
}, },
"open counterclockwise unit square": { "open counterclockwise unit square": {
topic: function(polygon) { topic: function(polygon) {
return polygon([[0, 0], [0, 1], [1, 1], [1, 0]]); return polygon([[0, 0], [0, 1], [1, 1], [1, 0]]);
}, },
"remains an open polygon": function(p) {
assertPolygonInDelta(p, [[0, 0], [0, 1], [1, 1], [1, 0]]);
},
"has area 1": function(p) { "has area 1": function(p) {
assert.equal(p.area(), 1); assert.equal(p.area(), 1);
}, },
"has centroid ⟨.5,.5⟩": function(p) { "has centroid ⟨.5,.5⟩": function(p) {
assert.deepEqual(p.centroid(), [.5, .5]); assertPointInDelta(p.centroid(), [.5, .5]);
},
"can clip an open counterclockwise triangle": function(p) {
assertPolygonInDelta(p.clip([[0.9, 0.5], [2, -1], [0.5, 0.1]]), [[0.9, 0.5], [1, 0.363636], [1, 0], [0.636363, 0], [0.5, 0.1]], 1e-4);
} }
}, },
"open clockwise unit square": { "open clockwise unit square": {
topic: function(polygon) { topic: function(polygon) {
return polygon([[0, 0], [1, 0], [1, 1], [0, 1]]); return polygon([[0, 0], [1, 0], [1, 1], [0, 1]]);
}, },
"remains an open polygon": function(p) {
assertPolygonInDelta(p, [[0, 0], [1, 0], [1, 1], [0, 1]]);
},
"has area 1": function(p) { "has area 1": function(p) {
assert.equal(p.area(), -1); assert.equal(p.area(), -1);
}, },
"has centroid ⟨.5,.5⟩": function(p) { "has centroid ⟨.5,.5⟩": function(p) {
assert.deepEqual(p.centroid(), [.5, .5]); assertPointInDelta(p.centroid(), [.5, .5]);
},
"is not currently supported for clipping": function(p) {
// because clipping requires a counterclockwise source polygon
} }
}, },
"open clockwise triangle": { "open clockwise triangle": {
topic: function(polygon) { topic: function(polygon) {
return polygon([[1, 1], [3, 2], [2, 3]]); return polygon([[1, 1], [3, 2], [2, 3]]);
}, },
"remains an open polygon": function(p) {
assertPolygonInDelta(p, [[1, 1], [3, 2], [2, 3]]);
},
"has area 1.5": function(p) { "has area 1.5": function(p) {
assert.equal(p.area(), -1.5); assert.equal(p.area(), -1.5);
}, },
"has centroid ⟨2,2⟩": function(p) { "has centroid ⟨2,2⟩": function(p) {
var centroid = p.centroid(); assertPointInDelta(p.centroid(), [2, 2]);
assert.inDelta(centroid[0], 2, 1e-6);
assert.inDelta(centroid[1], 2, 1e-6);
} }
}, },
"large square": { "large square": {
...@@ -95,4 +124,22 @@ suite.addBatch({ ...@@ -95,4 +124,22 @@ suite.addBatch({
} }
}); });
function assertPointInDelta(expected, actual, δ, message) {
if (!δ) δ = 0;
if (!pointInDelta(expected, actual, δ)) {
assert.fail(JSON.stringify(actual), JSON.stringify(expected), message || "expected {expected}, got {actual}", "===", assertPointInDelta);
}
}
function assertPolygonInDelta(expected, actual, δ, message) {
if (!δ) δ = 0;
if (expected.length !== actual.length || expected.some(function(e, i) { return !pointInDelta(e, actual[i], δ); })) {
assert.fail(JSON.stringify(actual), JSON.stringify(expected), message || "expected {expected}, got {actual}", "===", assertPolygonInDelta);
}
}
function pointInDelta(a, b, δ) {
return !(Math.abs(a[0] - b[0]) > δ || Math.abs(a[1] - b[1]) > δ);
}
suite.export(module); suite.export(module);
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment