Skip to content

Commit

Permalink
Compute the exact enclosing circle.
Browse files Browse the repository at this point in the history
  • Loading branch information
mbostock committed Sep 15, 2015
1 parent 269ac8c commit b429060
Show file tree
Hide file tree
Showing 2 changed files with 119 additions and 24 deletions.
112 changes: 112 additions & 0 deletions src/enclosingCircle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// Returns the smallest circle that contains the specified circles.
export default function(circles) {
return enclosingCircleIntersectingCircles(randomizedList(circles), []);
};

// Returns a linked list from the specified array in random order.
function randomizedList(array) {
var i,
n = (array = array.slice()).length,
head = null,
node = head;

while (n) {
var next = {id: array.length - n, value: array[n - 1], next: null};
if (node) node = node.next = next;
else node = head = next;
array[i] = array[--n];
}

return {head: head, tail: node};
}

// Returns the smallest circle that contains the circles L
// and intersects the circles B.
function enclosingCircleIntersectingCircles(L, B) {
var circle,
l0 = null,
l1 = L.head,
l2,
p1;

switch (B.length) {
case 1: circle = B[0]; break;
case 2: circle = circleIntersectingTwoCircles(B[0], B[1]); break;
case 3: circle = circleIntersectingThreeCircles(B[0], B[1], B[2]); break;
}

while (l1) {
p1 = l1.value, l2 = l1.next;
if (!circle || !circleContainsCircle(circle, p1)) {

// Temporarily truncate L before l1.
if (l0) L.tail = l0, l0.next = null;
else L.head = L.tail = null;

B.push(p1);
circle = enclosingCircleIntersectingCircles(L, B); // Note: reorders L!
B.pop();

// Move l1 to the front of L and reconnect the truncated list L.
if (L.head) l1.next = L.head, L.head = l1;
else l1.next = null, L.head = L.tail = l1;
l0 = L.tail, l0.next = l2;

} else {
l0 = l1;
}
l1 = l2;
}

L.tail = l0;
return circle;
}

// Returns true if the specified circle1 contains the specified circle2.
function circleContainsCircle(circle1, circle2) {
var xc0 = circle1.x - circle2.x,
yc0 = circle1.y - circle2.y;
return Math.sqrt(xc0 * xc0 + yc0 * yc0) < circle1.r - circle2.r + 1e-6;
}

// Returns the smallest circle that intersects the two specified circles.
function circleIntersectingTwoCircles(circle1, circle2) {
var x1 = circle1.x, y1 = circle1.y, r1 = circle1.r,
x2 = circle2.x, y2 = circle2.y, r2 = circle2.r,
x12 = x2 - x1, y12 = y2 - y1, r12 = r2 - r1,
l = Math.sqrt(x12 * x12 + y12 * y12);
return {
x: (x1 + x2 + x12 / l * r12) / 2,
y: (y1 + y2 + y12 / l * r12) / 2,
r: (l + r1 + r2) / 2
};
}

// Returns the smallest circle that intersects the three specified circles.
function circleIntersectingThreeCircles(circle1, circle2, circle3) {
var x1 = circle1.x, y1 = circle1.y, r1 = circle1.r,
x2 = circle2.x, y2 = circle2.y, r2 = circle2.r,
x3 = circle3.x, y3 = circle3.y, r3 = circle3.r,
a2 = 2 * (x1 - x2),
b2 = 2 * (y1 - y2),
c2 = 2 * (r2 - r1),
d2 = x1 * x1 + y1 * y1 - r1 * r1 - x2 * x2 - y2 * y2 + r2 * r2,
a3 = 2 * (x1 - x3),
b3 = 2 * (y1 - y3),
c3 = 2 * (r3 - r1),
d3 = x1 * x1 + y1 * y1 - r1 * r1 - x3 * x3 - y3 * y3 + r3 * r3,
ab = a3 * b2 - a2 * b3,
xa = (b2 * d3 - b3 * d2) / ab - x1,
xb = (b3 * c2 - b2 * c3) / ab,
ya = (a3 * d2 - a2 * d3) / ab - y1,
yb = (a2 * c3 - a3 * c2) / ab,
A = xb * xb + yb * yb - 1,
B = 2 * (xa * xb + ya * yb + r1),
C = xa * xa + ya * ya - r1 * r1,
r = (-B - Math.sqrt(B * B - 4 * A * C)) / (2 * A);
return {
x: xa + xb * r + x1,
y: ya + yb * r + y1,
r: r
};
}
31 changes: 7 additions & 24 deletions src/pack.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {default as enclosingCircle} from "./enclosingCircle";
import {default as hierarchy, rebind} from "./hierarchy";
import {visitAfter} from "./visit";

Expand Down Expand Up @@ -29,40 +30,26 @@ function packChildren(node) {
if (!(nodes = node.children) || !(n = nodes.length)) return;

var nodes,
xMin = Infinity,
xMax = -Infinity,
yMin = Infinity,
yMax = -Infinity,
a, b, c, i, j, k, n;

function bound(node) {
xMin = Math.min(node.x - node.r, xMin);
xMax = Math.max(node.x + node.r, xMax);
yMin = Math.min(node.y - node.r, yMin);
yMax = Math.max(node.y + node.r, yMax);
}

// Create node links.
nodes.forEach(link);

// Create first node.
a = nodes[0];
a.x = -a.r;
a.y = 0;
bound(a);

// Create second node.
if (n > 1) {
b = nodes[1];
b.x = b.r;
b.y = 0;
bound(b);

// Create third node and build chain.
if (n > 2) {
c = nodes[2];
place(a, b, c);
bound(c);
insert(a, c);
a._pack_prev = c;
insert(c, b);
Expand Down Expand Up @@ -96,23 +83,19 @@ function packChildren(node) {
} else {
insert(a, c);
b = c;
bound(c);
}
}
}
}

// Re-center the circles and compute the encompassing radius.
var cx = (xMin + xMax) / 2,
cy = (yMin + yMax) / 2,
cr = 0;
for (i = 0; i < n; i++) {
c = nodes[i];
c.x -= cx;
c.y -= cy;
cr = Math.max(cr, c.r + Math.sqrt(c.x * c.x + c.y * c.y));
var c = enclosingCircle(nodes);
for (i = 0; i < n; ++i) {
a = nodes[i];
a.x -= c.x;
a.y -= c.y;
}
node.r = cr;
node.r = c.r;

// Remove node links.
nodes.forEach(unlink);
Expand Down

0 comments on commit b429060

Please sign in to comment.