将不同大小的圆形装入矩形 - d3.js

33

我想尝试将不同大小的圆形装进一个矩形容器中,而不是在d3.js捆绑的圆形容器中进行打包,使用d3.layout.pack。

这是我想要实现的布局:

我找到了这篇论文,但我不是数学专家,无法彻底理解文章,并将其转化为代码...

有人可以建议我从哪里开始将其转换为d3.js布局插件,或者如果您有类似于此布局的气泡视觉效果,请建议您采取的任何方向来解决它。

谢谢。


1
你不是在追求最优解,对吧?该网站建议,即使限制在正方形的情况下,找到最小化矩形大小的最优解也很棘手。 - MvG
你是否找到了解决方案(我看到你在oDesk的职位发布)?我想使用相同的布局。 - cerberos
1
@cerberos 正如MvG去年指出的那样,获得一个最优解(将圆形装入尽可能小的矩形中)是棘手的;即使是原帖中链接的数学密集型论文也使用了“贪婪”(即不完美)算法。 然而,获得一个可以接受的包装应该是可行的。 此程序类似,但限制了圆的水平位置。 如果我有时间,我会在本周的某个时候试一试,但与此同时,其他任何人都可以将其用作起点! - AmeliaBR
你有许多已知大小的圆形,想要将它们放入一个最小面积的矩形中?还是最小化max(宽度,高度)?或者你有一个固定大小的已知矩形,想要用尽可能多的已知大小的圆来填满它?还是其他情况? - j_random_hacker
我一直在假设矩形是固定的,圆的相对大小也是固定的,但用于调整圆与矩形相对大小的比例是可调节的,并且您希望尽可能地将圆装入矩形中。 - AmeliaBR
显示剩余7条评论
5个回答

32

这里是对您算法实现的一次尝试。

我对其进行了一些修改,但我认为它基本上做了同样的事情。

包围圆

我使用了一个技巧使计算更加规则。

我使用了半径“无限”的圆来定义边界框,可以被视为直线的好近似:

bounding circles

当半径减小时,图片显示出4个包围圆的外观。它们被计算为穿过边界框的角落,并在半径增长时向实际边缘收敛。

“角”圆(算法称之为角)全部计算为两个圆的切线,从而消除了特殊的圆+线段或线段+线段的情况。

这也大大简化了起始条件。算法只需从四个边框圆开始,每次添加一个圆,使用贪心启发式λ参数选择“最佳”位置。

最佳匹配策略

原始算法不能生成能容纳所有圆的最小矩形(它只是试图将一堆圆放入给定的矩形中)。

我在其上添加了一个简单的二分搜索,以猜测最小表面(为给定长宽比提供最小边框)。

代码

这里是一个演示

var Packer = function (circles, ratio)
{
    this.circles = circles;
    this.ratio   = ratio || 1;
    this.list = this.solve();
}

Packer.prototype = {
    // try to fit all circles into a rectangle of a given surface
    compute: function (surface)
    {
        // check if a circle is inside our rectangle
        function in_rect (radius, center)
        {
            if (center.x - radius < - w/2) return false;
            if (center.x + radius >   w/2) return false;
            if (center.y - radius < - h/2) return false;
            if (center.y + radius >   h/2) return false;
            return true;
        }

        // approximate a segment with an "infinite" radius circle
        function bounding_circle (x0, y0, x1, y1)
        {
            var xm = Math.abs ((x1-x0)*w);
            var ym = Math.abs ((y1-y0)*h);
            var m = xm > ym ? xm : ym;
            var theta = Math.asin(m/4/bounding_r);
            var r = bounding_r * Math.cos (theta);
            return new Circle (bounding_r, 
                new Point (r*(y0-y1)/2+(x0+x1)*w/4, 
                           r*(x1-x0)/2+(y0+y1)*h/4));
        }

        // return the corner placements for two circles
        function corner (radius, c1, c2)
        {
            var u = c1.c.vect(c2.c); // c1 to c2 vector
            var A = u.norm();
            if (A == 0) return [] // same centers
            u = u.mult(1/A); // c1 to c2 unary vector
            // compute c1 and c2 intersection coordinates in (u,v) base
            var B = c1.r+radius;
            var C = c2.r+radius;
            if (A > (B + C)) return []; // too far apart
            var x = (A + (B*B-C*C)/A)/2;
            var y = Math.sqrt (B*B - x*x);
            var base = c1.c.add (u.mult(x));

            var res = [];
            var p1 = new Point (base.x -u.y * y, base.y + u.x * y);
            var p2 = new Point (base.x +u.y * y, base.y - u.x * y);
            if (in_rect(radius, p1)) res.push(new Circle (radius, p1));
            if (in_rect(radius, p2)) res.push(new Circle (radius, p2));
            return res;
        }

        /////////////////////////////////////////////////////////////////

        // deduce starting dimensions from surface
        var bounding_r = Math.sqrt(surface) * 100; // "infinite" radius
        var w = this.w = Math.sqrt (surface * this.ratio);
        var h = this.h = this.w/this.ratio;

        // place our bounding circles
        var placed=[
            bounding_circle ( 1,  1,  1, -1),
            bounding_circle ( 1, -1, -1, -1),
            bounding_circle (-1, -1, -1,  1),
            bounding_circle (-1,  1,  1,  1)];

        // Initialize our rectangles list
        var unplaced = this.circles.slice(0); // clones the array
        while (unplaced.length > 0)
        {
            // compute all possible placements of the unplaced circles
            var lambda = {};
            var circle = {};
            for (var i = 0 ; i != unplaced.length ; i++)
            {
                var lambda_min = 1e10;
                lambda[i] = -1e10;
                // match current circle against all possible pairs of placed circles
                for (var j = 0   ; j < placed.length ; j++)
                for (var k = j+1 ; k < placed.length ; k++)
                {
                    // find corner placement
                    var corners = corner (unplaced[i], placed[j], placed[k]);

                    // check each placement
                    for (var c = 0 ; c != corners.length ; c++)
                    {
                        // check for overlap and compute min distance
                        var d_min = 1e10;
                        for (var l = 0 ; l != placed.length ; l++)
                        {
                            // skip the two circles used for the placement
                            if (l==j || l==k) continue;

                            // compute distance from current circle
                            var d = placed[l].distance (corners[c]);
                            if (d < 0) break; // circles overlap

                            if (d < d_min) d_min = d;
                        }
                        if (l == placed.length) // no overlap
                        {
                            if (d_min < lambda_min)
                            {
                                lambda_min = d_min;
                                lambda[i] = 1- d_min/unplaced[i];
                                circle[i] = corners[c];
                            }
                        }
                    }
                }
            }

            // select the circle with maximal gain
            var lambda_max = -1e10;
            var i_max = -1;
            for (var i = 0 ; i != unplaced.length ; i++)
            {
                if (lambda[i] > lambda_max)
                {
                    lambda_max = lambda[i];
                    i_max = i;
                }
            }

            // failure if no circle fits
            if (i_max == -1) break;

            // place the selected circle
            unplaced.splice(i_max,1);
            placed.push (circle[i_max]);
        }

        // return all placed circles except the four bounding circles
        this.tmp_bounds = placed.splice (0, 4);
        return placed;
    },

    // find the smallest rectangle to fit all circles
    solve: function ()
    {
        // compute total surface of the circles
        var surface = 0;
        for (var i = 0 ; i != this.circles.length ; i++)
        {
            surface += Math.PI * Math.pow(this.circles[i],2);
        }

        // set a suitable precision
        var limit = surface/1000;

        var step = surface/2;
        var res = [];
        while (step > limit)
        {
            var placement = this.compute.call (this, surface);
console.log ("placed",placement.length,"out of",this.circles.length,"for surface", surface);
            if (placement.length != this.circles.length)
            {
                surface += step;
            }
            else
            {
                res = placement;
                this.bounds = this.tmp_bounds;
                surface -= step;
            }
            step /= 2;
        }
        return res; 
    }
};

性能

这段代码没有进行优化,主要是为了提高可读性(或者我希望如此:))。

计算时间上升得非常快。
您可以安全地放置约20个圆形,但超过100个将使您的浏览器变得缓慢。

由于某种原因,在FireFox上比在IE11上更快。

装箱效率

该算法在相同大小的圆形上表现非常差(无法找到20个圆形的蜂巢图案),但在随机半径的广泛分布上表现良好。

美学

相同大小的圆形结果相当难看。
没有尝试将圆形集中在一起,因此如果算法认为两种可能性相等,则会随机选择一个。

我怀疑lambda参数可以稍微调整一下,以便在值相等的情况下进行更美观的选择。

可能的演化

使用“无限半径”技巧,可以定义任意的边界多边形。

如果您提供一个检查圆是否适合进入该多边形的函数,那么算法应该可以产生结果。

这个结果是否高效是另一个问题 :)。


哇,这太棒了。你熟悉d3吗?你能把它包装成一个d3布局吗?我已经颁发了赏金,因为时间不多了,我没有期望会有更多的答案。下周我会再发布一个赏金,并授予给你。感谢你花时间看这个问题。 - cerberos
1
从未使用过d3,但现在看起来是一个好时机:)。我可能会没有时间去玩这个小有趣的玩具,但我会看一看。 - kuroi neko
看起来很不错。我喜欢边界框描述为其他形状的交集,这样它就具有可扩展性。 - AmeliaBR
很棒的解决方案,但是如果宽度和高度不同,它仍然会使气泡适应一个正方形矩形。应该如何更改以符合原始要求? - Cristian Scutaru
我不确定我理解你的问题。基本算法只是在固定大小的盒子中适配圆形。第二个算法使用二分搜索来优化该盒子的大小,以便减少空间损失。您可以通过定义对角线比率(如电视或计算机屏幕)来控制该盒子的形状。您还需要什么?理论上,您可以为容器定义任意凸多边形形状,但我从未尝试过。这将需要对代码进行一些更改。 - kuroi neko

26

一种完全不同的方法...

如我在评论中提到的,可以将d3集群力布局改编为启发式方法,通过逐渐改变比例来将圆形适配到盒子中,直到实现紧密的拟合。

迄今为止的结果并不完美,因此我提供了几个版本:

选项1,在调整圆形重叠之前将盒子挤入圆形占用的空间中。 结果非常紧密地打包,但是圆形之间会发生轻微的重叠,被夹在盒子和彼此的墙壁之间,无法移动而不发生冲突:
https://jsfiddle.net/LeGfW/2/

选项1圆形装箱结果

选项2,在分开重叠的圆形后将盒子挤入。这避免了重叠,但是打包不是最佳的,因为我们从未使圆形相互推挤以强制它们扩展填充矩形的长尺寸:
https://jsfiddle.net/LeGfW/3/

选项2圆形装箱结果

选项3是一种折中的方式,调整重叠后再挤入盒子,但挤压因子基于宽度和高度维度上的平均空间而不是最小空间,因此它会持续挤压,直到宽度和高度都填满为止:
https://jsfiddle.net/LeGfW/5/

选项3圆形装箱结果

关键代码包括力布局的updateBubbles方法调用的和在updateBubbles的第一行调用的collide方法。 这是"选项3 "版本:

// Create a function for this tick round,
// with a new quadtree to detect collisions 
// between a given data element and all
// others in the layout, or the walls of the box.

//keep track of max and min positions from the quadtree
var bubbleExtent;
function collide(alpha) {
  var quadtree = d3.geom.quadtree(data);
  var maxRadius = Math.sqrt(dataMax);
  var scaledPadding = padding/scaleFactor;
  var boxWidth = width/scaleFactor;
  var boxHeight = height/scaleFactor;

    //re-set max/min values to min=+infinity, max=-infinity:
  bubbleExtent = [[Infinity, Infinity],[-Infinity, -Infinity]];

  return function(d) {

      //check if it is pushing out of box:
    var r = Math.sqrt(d.size) + scaledPadding,
        nx1 = d.x - r,
        nx2 = d.x + r,
        ny1 = d.y - r,
        ny2 = d.y + r;

      if (nx1 < 0) {
           d.x = r;
      }
      if (nx2 > boxWidth) {
           d.x = boxWidth - r;
      }
      if (ny1 < 0) {
           d.y = r;
      }
      if (ny2 > boxHeight) {
           d.y = boxHeight - r;
      }


    //check for collisions
    r = r + maxRadius, 
        //radius to center of any possible conflicting nodes
        nx1 = d.x - r,
        nx2 = d.x + r,
        ny1 = d.y - r,
        ny2 = d.y + r;

    quadtree.visit(function(quad, x1, y1, x2, y2) {
      if (quad.point && (quad.point !== d)) {
        var x = d.x - quad.point.x,
            y = d.y - quad.point.y,
            l = Math.sqrt(x * x + y * y),
            r = Math.sqrt(d.size) + Math.sqrt(quad.point.size)
                    + scaledPadding;
        if (l < r) {
          l = (l - r) / l * alpha;
          d.x -= x *= l;
          d.y -= y *= l;
          quad.point.x += x;
          quad.point.y += y;
        }
      }
      return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
    });

    //update max and min
    r = r-maxRadius; //return to radius for just this node
    bubbleExtent[0][0] = Math.min(bubbleExtent[0][0], 
                                  d.x - r);
    bubbleExtent[0][1] = Math.min(bubbleExtent[0][1], 
                                  d.y - r);
    bubbleExtent[1][0] = Math.max(bubbleExtent[1][0], 
                                  d.x + r);
    bubbleExtent[1][1] = Math.max(bubbleExtent[1][1], 
                                  d.y + r);

  };
}  

function updateBubbles() {

    bubbles
        .each( collide(0.5) ); //check for collisions   

    //update the scale to squeeze in the box 
    //to match the current extent of the bubbles
    var bubbleWidth = bubbleExtent[1][0] - bubbleExtent[0][0];
    var bubbleHeight = bubbleExtent[1][1] - bubbleExtent[0][1];

    scaleFactor = (height/bubbleHeight +
                           width/bubbleWidth)/2; //average
    /*
    console.log("Box dimensions:", [height, width]);
    console.log("Bubble dimensions:", [bubbleHeight, bubbleWidth]);
    console.log("ScaledBubble:", [scaleFactor*bubbleHeight,
                                 scaleFactor*bubbleWidth]);
    //*/

    rScale
        .range([0,  Math.sqrt(dataMax)*scaleFactor]);

    //shift the bubble cluster to the top left of the box
    bubbles
        .each( function(d){
            d.x -= bubbleExtent[0][0];
            d.y -= bubbleExtent[0][1];
        });

    //update positions and size according to current scale:
    bubbles
        .attr("r", function(d){return rScale(d.size);} )
        .attr("cx", function(d){return scaleFactor*d.x;})
        .attr("cy", function(d){return scaleFactor*d.y;})
}

好的图片使用! - ninjaPixel
目前我所见过的选项中,选项三是最好的。但不幸的是,它并不完全符合我的要求,因为它不能转换成 d3 布局,因为它以 d3.layout.pack() 开始,并使用力学布局和碰撞处理来“查找”最终位置。感谢您花费时间,我已经授予您赏金,以免浪费。 - cerberos
是的,力导向图中的反弹可能会对某些用途造成干扰。 - AmeliaBR
1
如果我关闭了提高的重力参数,结果会略微好一些:http://fiddle.jshell.net/LeGfW/6/。如果跳过初始圆形排列,只是将圆形放置在网格中,结果大约相等:http://fiddle.jshell.net/LeGfW/7/。 - AmeliaBR
1
你不需要弹跳效果 - 动画是可选的。直到位置最终确定之前,不要渲染。 - nrabinowitz
布局可以通过重复检查force.alpha()的值(每100毫秒一次)并在达到经验阈值时立即调用force.stop()来提前结束——在大多数情况下不会对结果产生显着影响。如果在初始打包后添加了相关的UI元素,则此方法非常有用。 - craPkit

5

虽然这远非最佳打包方案,但其他人可以尝试超越它。

已更新,但仍不太好

https://jsfiddle.net/LF9Yp/6/

关键代码如下:

var points = [[]]; //positioned circles, by row
function assignNextPosition(d,index) {
    console.log("fitting circle ", index, d.size);
    var i, j, n;
    var radiusPlus = rScale(d.size) + padding;
    if (!points[0].length) { //this is first object
       d.x = d.y = radiusPlus; 
       points[0].push(d);
       points[0].width = points[0].height = 2*radiusPlus;
       points[0].base = 0;
       return;
    }
    i = 0; n = points.length - 1; 
    var tooTight, lastRow, left, rp2, hyp;
    while ((tooTight = (width - points[i].width < 2*radiusPlus)
            ||( points[i+1]? 
                points[i+1].base - points[i].base < 2*radiusPlus 
                : false) ) 
          &&(i < n) ) i++;
           //skim through rows to see if any can fit this circle

    if (!tooTight) { console.log("fit on row ", i);
        //one of the rows had room
        lastRow = points[i];
        j=lastRow.length;

        if (i == 0) {
          //top row, position tight to last circle and wall
            d.y = radiusPlus;
            rp2 = (rScale(lastRow[j-1].size) + padding);
            d.x = lastRow[j-1].x + Math.sqrt(
                Math.pow( (radiusPlus + rp2), 2)
                - Math.pow( (radiusPlus - rp2),2) );
        }
        else {
           //position tight to three closest circles/wall
           //(left, top left and top right)
            //or (left, top left and right wall)
           var left = lastRow[j-1];
           d.x = left.x + rScale(left.size) + padding + radiusPlus;
           var prevRow = points[i - 1];       
           j = prevRow.length;
           while ((j--) && (prevRow[j].x > d.x));
           j = Math.max(j,0);
           if (j + 1 < prevRow.length) {
               console.log("fit between", prevRow[j], prevRow[j+1]);
               d.y = prevRow[j].y 
               + (Math.sqrt(Math.pow((radiusPlus + 
                           rScale(prevRow[j].size) +padding), 2) 
                           - Math.pow( (d.x - prevRow[j].x),2)
                       )||0);
              j++;
              d.y = Math.max(d.y, prevRow[j].y 
               + (Math.sqrt(Math.pow((radiusPlus + 
                           rScale(prevRow[j].size) +padding), 2) 
                           - Math.pow( (d.x - prevRow[j].x),2)
                       )||0)  );
           }
           else { //tuck tight against wall
               console.log("fit between", prevRow[j], "wall");
            d.x = width - radiusPlus;
            rp2 = (rScale(prevRow[j].size) + padding);
            d.y = prevRow[j].y + (Math.sqrt(
                Math.pow( (radiusPlus + rp2), 2)
                - Math.pow( (d.x - prevRow[j].x),2) )||0);
            if (i > 1)
                d.y = Math.max(d.y, points[i-2].height + radiusPlus);
           }
        }

        lastRow.push(d); 
        lastRow.width = d.x + radiusPlus;
        lastRow.height = Math.max(lastRow.height, 
                                  d.y + radiusPlus);
        lastRow.base = Math.min(lastRow.base,
                                d.y - radiusPlus);

    } else { console.log("new row ", points.length)
        prevRow = points[points.length -1];
        j=prevRow.length;
        while(j--) {
            var testY = prevRow[j].y + rScale(prevRow[j].size) + padding
                  + radiusPlus;
            if (testY + radiusPlus < prevRow.height) {
                //tuck row in gap
                d.x = prevRow[j].x;
                d.y = testY;
            }
        }
        if (!d.x) {//start row at left
          d.x = radiusPlus;
          d.y = prevRow.height + radiusPlus;
        }
        var newRow = [d];
        newRow.width = d.x + radiusPlus;
        newRow.height = Math.max(d.y + radiusPlus, prevRow.height);
        newRow.base = d.y - radiusPlus;
        points.push(newRow); 
    } 
            if (!d.y) console.log("error",d);
    if (d.y + radiusPlus > height) {
      //change rScale by the ratio this exceeds the height
      var scaleFactor = height/(d.y + radiusPlus);
      rScale.range([0, rScale.range()[1]*scaleFactor]);

      //recalculate all positions
      points.forEach(function(row, j){
            row.forEach(function(d, i) {
               d.x = (d.x - i*2*padding)*scaleFactor + i*2*padding;
               d.y = (d.y - i*2*padding)*scaleFactor + i*2*padding;
            });
            row.width *= scaleFactor;
      });

    }

}

你有没有考虑尝试实现 OP 引用的研究论文 - bits
同时,有人可以尝试利用物理引擎来减少如此复杂的编程。 - bits
我老实说没有仔细看过这篇论文,但它也是基于跟踪间隙而不是跟踪占用空间。它没有讨论如何有效地管理数据结构来实现这一点。但如果你能让四叉树帮助跟踪间隙,那么你就会想要使用他们的算法来进行高效的打包。 - AmeliaBR
在“物理”方法中,这是完全采取的另一种方向。对于d3,您可以从类似于此类的东西开始,该类使用力布局将圆紧密聚集在一起。您必须添加一个正确长宽比的边界框,然后缓慢地挤压墙壁(或者相反,增加每个圆的大小),直到不能再紧密地打包它们。 - AmeliaBR
1
@cellepo:在我的代码中,通过更改“rScale”和“scaleFactor”,调整最终大小,以便将气泡的压缩尺寸放大到填满整个矩形。 - AmeliaBR
显示剩余3条评论

1

如果您的主要关注点是在矩形内紧密地放置不同大小的圆,则不幸的是,您将不得不实现一个新的d3布局。我不知道是否已经有一个插件可以完成这个任务。

但是,如果您正在寻找任何一种旧的矩形内装载方法,那么您可以使用d3提供的现有圆形装载算法d3.layout.pack。当您为此布局指定边界时,您正在指定一个矩形的尺寸。然后,d3确定一个圆,该圆将围绕边界矩形,并使用该圆来可视化分层数据的根。因此,您可以提供一个“虚拟”根节点,您实际上不会呈现它,并且让您想要可视化的真实数据成为该节点的子项。

以下是代码示例,我还将其放在bl.ocks.org上,以便您可以在其中查看它的运行情况。

var w = 640,
    h = 480;

var data = {
  name : "root",
  children : [
    { name: '1', size: 100 }, { name: '2', size: 85 },
    { name: '3', size: 70 } , { name: '4', size: 55 },
    { name: '5', size: 40 } , { name: '6', size: 25 },
    { name: '7', size: 10 } ,
  ]
}

var canvas = d3.select("#canvas")
  .append("svg:svg")
  .attr('width', w)
  .attr('height', h);

var nodes = d3.layout.pack()
  .value(function(d) { return d.size; })
  .size([w, h])
  .nodes(data);

// Get rid of root node
nodes.shift();

canvas.selectAll('circles')
    .data(nodes)
  .enter().append('svg:circle')
    .attr('cx', function(d) { return d.x; })
    .attr('cy', function(d) { return d.y; })
    .attr('r', function(d) { return d.r; })
    .attr('fill', 'white')
    .attr('stroke', 'grey');

1
这并没有真正解决问题。所有它做的就是将圆形打包到一个未显示的父圆中。它没有利用矩形提供的额外空间,以允许子圆更大地缩放。 - HelpMeStackOverflowMyOnlyHope
@求助StackOverflow,你是我的唯一希望。我的回答基本上已经说明了这一点。 - seliopou

-1

有一种更好的方法可以做到这一点 - 使用米切尔的最佳适配算法。

这是一般模式:

function drawCircles() { 
  var w = (parseInt(d3.select(".circles-div").style('width'), 10)) * 0.34,
    h = 350;

  d3.csv('dataset.csv', function(error, data) {

    var maxRadius = 8, // maximum radius of circle
      padding = 3, // padding between circles; also minimum radius
      margin = {top: maxRadius, right: maxRadius, bottom: maxRadius, left: maxRadius},
      width = w - margin.left - margin.right,
      height = h - margin.top - margin.bottom;

     var color = d3.scale.linear()
      .domain([50,10])
      .range(['#666','#efefef'])
      .interpolate(d3.interpolateHcl);

    var logscale = d3.scale.linear()
        .range([0,8]);

    logscale.domain([0,500])

    var k = 1, // initial number of candidates to consider per circle
        m = 100, // initial number of circles to add per frame
        n = data.length, // remaining number of circles to add
        newCircle = bestCircleGenerator(maxRadius, padding);

    var svg = d3.select(".circles-div").append("svg")
        .attr("width", w)
        .attr("height", h)
      .append("g")
        .attr('class','bubbles')
        .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

    d3.timer(function() {
      for (var i = 0; i < m && --n >= 0; ++i) {

        var maxR = logscale(data[n]['Radius_value'])

        var circle = newCircle(k);

        svg.append("circle")
            .attr("cx", circle[0])
            .attr("cy", circle[1])
            .attr("r", 0)
            .style('fill', color(data[n]['Color_value']))
          .transition()
            .attr("r", logscale(data[n]['Radius_value']));

        if (k < 500) k *= 1.01, m *= .998;
      }
      return !n;
    });

    function bestCircleGenerator(maxRadius, padding) {

      var quadtree = d3.geom.quadtree().extent([[0, 0], [width, height]])([]),
      searchRadius = maxRadius * 2,
      maxRadius2 = maxRadius * maxRadius;

      return function(k) {

        var bestX, bestY, bestDistance = 0;

        for (var i = 0; i < k || bestDistance < padding; ++i) {
          var x = Math.random() * width,
              y = Math.random() * height,
              rx1 = x - searchRadius,
              rx2 = x + searchRadius,
              ry1 = y - searchRadius,
              ry2 = y + searchRadius,
              minDistance = maxRadius; // minimum distance for this candidate

          quadtree.visit(function(quad, x1, y1, x2, y2) {
            if (p = quad.point) {
              var p,
                  dx = x - p[0],
                  dy = y - p[1],
                  d2 = dx * dx + dy * dy,
                  r2 = p[2] * p[2];
              if (d2 < r2) return minDistance = 0, true; // within a circle
              var d = Math.sqrt(d2) - p[2];
              if (d < minDistance) minDistance = d;
            }
            return !minDistance || x1 > rx2 || x2 < rx1 || y1 > ry2 || y2 < ry1; // or outside search radius
          });

          if (minDistance > bestDistance) bestX = x, bestY = y, bestDistance = minDistance;
        }

        var best = [bestX, bestY, bestDistance - padding];
        quadtree.add(best);
        return best;
      };
    }

    });

  }

查看随机数据的示例。


1
这个算法如何可能与将预定义数量和尺寸的圆装进矩形有关呢? - eagle.dan.1349

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接