最佳地将饼图切片放在矩形中

时间:2010-04-07 19:50:37

标签: algorithm graphics

给定一个矩形(w,h)和一个半径小于或等于两边(w,h)中较小的扇形切片,一个起始角和一个终止角,如何将切片最佳地放置在矩形,以便它最好地填充房间(从光学的角度来看,不是从数学角度来说)?

我目前正在将饼形切片的中心放在矩形的中心,并使用两个矩形边中较小的一半作为半径。这为某些配置留下了足够的空间。

根据切片被绘制成单位圆的前提条件(即在正X轴上为0度,然后以顺时针方向运行)来明确我所追求的内容的示例:

  • 起始角度为0且PI的结束角度将导致矩形的下半部分填充并且上半部分为空。这里一个很好的解决方案是将中心向上移动1/4 * h。
  • 起始角度为0,结束角度为PI / 2将导致矩形的右四分之一填充。这里一个很好的解决方案是将中心点移动到矩形的左上角,并将半径设置为两个矩形边中较小的一侧。

对于我绘制的情况来说,这是相当容易的,但是当开始和结束角度是任意的时,它会变得复杂。我正在寻找一种算法,该算法以最佳填充矩形的方式确定切片中心和半径。由于我不是一位伟大的数学家,伪代码会很棒。

4 个答案:

答案 0 :(得分:2)

弧的边界框的极值采用以下格式:

x + x0 * r = 0
x + x1 * r = w
y + y0 * r = 0
y + y1 * r = h

通过获取最多7个点的最小值和最大值来找到值x0,x1,y0和y1:任何跨越的切线点(即0度,90度,180度和270度)和终点两个线段。

考虑到弧的轴对齐边界框(x0,y0),(x1,y1)的极值,半径和中心点可以如下计算:

r = min(w/(x1-x0), h/(y1-y0)
x = -x0 * r
y = -y0 * r

这是用Lua编写的实现:

-- ensures the angle is in the range [0, 360)
function wrap(angle)
    local x = math.fmod(angle, 2 * math.pi)
    if x < 0 then
        x = x + 2 * math.pi
    end
    return x
end

function place_arc(t0, t1, w, h)
    -- find the x-axis extrema
    local x0 = 1
    local x1 = -1
    local xlist = {}
    table.insert(xlist, 0)
    table.insert(xlist, math.cos(t0))
    table.insert(xlist, math.cos(t1))
    if wrap(t0) > wrap(t1) then
        table.insert(xlist, 1)
    end
    if wrap(t0-math.pi) > wrap(t1-math.pi) then
        table.insert(xlist, -1)
    end
    for _, x in ipairs(xlist) do
        if x < x0 then x0 = x end
        if x > x1 then x1 = x end
    end

    -- find the y-axis extrema
    local ylist = {}
    local y0 = 1
    local y1 = -1
    table.insert(ylist, 0)
    table.insert(ylist, math.sin(t0))
    table.insert(ylist, math.sin(t1))
    if wrap(t0-0.5*math.pi) > wrap(t1-0.5*math.pi) then
        table.insert(ylist, 1)
    end
    if wrap(t0-1.5*math.pi) > wrap(t1-1.5*math.pi) then
        table.insert(ylist, -1)
    end
    for _, y in ipairs(ylist) do
        if y < y0 then y0 = y end
        if y > y1 then y1 = y end
    end

    -- calculate the maximum radius the fits in the bounding box
    local r = math.min(w / (x1 - x0), h / (y1 - y0))

    -- find x & y from the radius and minimum extrema
    local x = -x0 * r
    local y = -y0 * r

    -- calculate the final axis-aligned bounding-box (AABB)
    local aabb = {
        x0 = x + x0 * r,
        y0 = y + y0 * r,
        x1 = x + x1 * r,
        y1 = y + y1 * r
    }

    return x, y, r, aabb
end

function center_arc(x, y, aabb, w, h)
    dx = (w - aabb.x1) / 2
    dy = (h - aabb.y1) / 2
    return x + dx, y + dy
end

t0 = math.rad(60)
t1 = math.rad(300)
w = 320
h = 240
x, y, r, aabb = place_arc(t0, t1, w, h)
x, y = center_arc(x, y, aabb, w, h)
print(x, y, r)

示例输出:

alt text http://i42.tinypic.com/3338iuu.png

alt text http://i43.tinypic.com/dztohu.png

答案 1 :(得分:1)

我使用python而不是伪代码,但它应该是可用的。对于此算法,我假设startAngle < endAngle并且两者都在[-2 * PI, 2 * PI]内。如果您想在[0, 2 * PI]中同时使用,请让startAngle&gt; endAngle,我愿意:

if (startAngle > endAngle):
   startAngle = startAngle - 2 * PI

因此,想到的算法是计算单位圆弧的边界,然后缩放以适合您的矩形。

第一个是更难的部分。你需要计算4个数字:

Left: MIN(cos(angle), 0)
Right: MAX(cos(angle), 0)
Top: MIN(sin(angle),0)
Bottom: MAX(sin(angle),0)

当然,角度是一个范围,所以它并不像这样简单。但是,在此计算中,您实际上只需要包含多达11个点。起始角度,结束角度以及可能的基本方向(其中有9个从-2 * PI2 * PI。)我将boundingBoxes定义为4个列表元素,有序[left, right, top, bottom]

def IncludeAngle(boundingBox, angle)
   x = cos(angle)
   y = sin(angle)
   if (x < boundingBox[0]):
      boundingBox[0] = x
   if (x > boundingBox[1]):
      boundingBox[1] = x
   if (y < boundingBox[2]):
      boundingBox[2] = y
   if (y > boundingBox[3]):
      boundingBox[3] = y
def CheckAngle(boundingBox, startAngle, endAngle, angle):
   if (startAngle <= angle and endAngle >= angle):
      IncludeAngle(boundingBox, angle)

boundingBox = [0, 0, 0, 0]
IncludeAngle(boundingBox, startAngle)
IncludeAngle(boundingBox, endAngle)
CheckAngle(boundingBox, startAngle, endAngle, -2 * PI)
CheckAngle(boundingBox, startAngle, endAngle, -3 * PI / 2)
CheckAngle(boundingBox, startAngle, endAngle, -PI)
CheckAngle(boundingBox, startAngle, endAngle, -PI / 2)
CheckAngle(boundingBox, startAngle, endAngle, 0)
CheckAngle(boundingBox, startAngle, endAngle, PI / 2)
CheckAngle(boundingBox, startAngle, endAngle, PI)
CheckAngle(boundingBox, startAngle, endAngle, 3 * PI / 2)
CheckAngle(boundingBox, startAngle, endAngle, 2 * PI)

现在,您已计算出弧心的边界框,其中心为0,0,半径为1。为了填补这个框,我们将不得不求解一个线性方程式:

boundingBox[0] * xRadius + xOffset = 0
boundingBox[1] * xRadius + xOffset = w
boundingBox[2] * yRadius + yOffset = 0
boundingBox[3] * yRadius + yOffset = h

我们必须解决xRadius和yRadius问题。你会注意到这里有两个半径。原因在于,为了填充矩形,我们必须在两个方向上以不同的量进行多次。由于您的算法仅询问一个半径,我们将只选择两个值中的较低者。

求解等式给出:

xRadius = w / (boundingBox[1] - boundingBox[0])
yRadius = h / (boundingBox[2] - boundingBox[3])
radius = MIN(xRadius, yRadius)

在这种情况下,您必须检查boundingBox[1] - boundingBox[0]0并将xRadius设置为无穷大。这将给出正确的结果,因为yRadius会更小。如果您没有无限可用,则只需将其设置为0,然后在MIN函数中检查0并在这种情况下使用其他值。 xRadiusyRadius不能同时为0,因为对于上面包含的所有角度,sincos都必须为0是这样的。

现在我们必须放置弧的中心。我们希望它集中在两个方向。现在我们将创建另一个线性方程:

(boundingBox[0] + boundingBox[1]) / 2 * radius + x = xCenter = w/2
(boundingBox[2] + boundingBox[3]) / 2 * radius + y = yCenter = h/2

求解弧的中心xy,给出

x = w/2 - (boundingBox[0] + boundingBox[1]) * radius / 2
y = h/2 - (boundingBox[3] + boundingBox[2]) * radius / 2

这应该为您提供弧的中心和将最大圆放在给定矩形中所需的半径。

我还没有测试过这些代码,所以这个算法可能有很大的漏洞,或者也可能是由拼写错误导致的小漏洞。我很想知道这个算法是否有效。

编辑:

将所有代码放在一起会产生:

def IncludeAngle(boundingBox, angle)
   x = cos(angle)
   y = sin(angle)
   if (x < boundingBox[0]):
      boundingBox[0] = x
   if (x > boundingBox[1]):
      boundingBox[1] = x
   if (y < boundingBox[2]):
      boundingBox[2] = y
   if (y > boundingBox[3]):
      boundingBox[3] = y
def CheckAngle(boundingBox, startAngle, endAngle, angle):
   if (startAngle <= angle and endAngle >= angle):
      IncludeAngle(boundingBox, angle)

boundingBox = [0, 0, 0, 0]
IncludeAngle(boundingBox, startAngle)
IncludeAngle(boundingBox, endAngle)
CheckAngle(boundingBox, startAngle, endAngle, -2 * PI)
CheckAngle(boundingBox, startAngle, endAngle, -3 * PI / 2)
CheckAngle(boundingBox, startAngle, endAngle, -PI)
CheckAngle(boundingBox, startAngle, endAngle, -PI / 2)
CheckAngle(boundingBox, startAngle, endAngle, 0)
CheckAngle(boundingBox, startAngle, endAngle, PI / 2)
CheckAngle(boundingBox, startAngle, endAngle, PI)
CheckAngle(boundingBox, startAngle, endAngle, 3 * PI / 2)
CheckAngle(boundingBox, startAngle, endAngle, 2 * PI)

if (boundingBox[1] == boundingBox[0]):
   xRadius = 0
else:
   xRadius = w / (boundingBox[1] - boundingBox[0])
if (boundingBox[3] == boundingBox[2]):
   yRadius = 0
else:
   yRadius = h / (boundingBox[3] - boundingBox[2])

if xRadius == 0:
   radius = yRadius
elif yRadius == 0:
   radius = xRadius
else:
   radius = MIN(xRadius, yRadius)

x = w/2 - (boundingBox[0] + boundingBox[1]) * radius / 2
y = h/2 - (boundingBox[3] + boundingBox[2]) * radius / 2

编辑:

这里的一个问题是由于舍入错误,sin[2 * PI]不会完全是0。我认为解决方案是摆脱CheckAngle调用并用以下内容替换它们:

def CheckCardinal(boundingBox, startAngle, endAngle, cardinal):
   if startAngle < cardinal * PI / 2 and endAngle > cardinal * PI / 2:
      cardinal = cardinal % 4
      if cardinal == 0:
         boundingBox[1] = 1
      if cardinal == 1:
         boundingBox[3] = 1
      if cardinal == 2:
         boundingBox[0] = -1
      if cardinal == 3:
         boundingBox[2] = -1
CheckCardinal(boundingBox, startAngle, endAngle, -4)
CheckCardinal(boundingBox, startAngle, endAngle, -3)
CheckCardinal(boundingBox, startAngle, endAngle, -2)
CheckCardinal(boundingBox, startAngle, endAngle, -1)
CheckCardinal(boundingBox, startAngle, endAngle, 0)
CheckCardinal(boundingBox, startAngle, endAngle, 1)
CheckCardinal(boundingBox, startAngle, endAngle, 2)
CheckCardinal(boundingBox, startAngle, endAngle, 3)
CheckCardinal(boundingBox, startAngle, endAngle, 4)

您仍然需要IncludeAngle(startAngle)IncludeAngle(endAngle)

答案 2 :(得分:0)

考虑一个圆圈,忘记填充。边界将是圆的中心,端点或0度,90度,180度或270度的点(如果它们存在于此切片中)。这七个点的最大值和最小值将决定您的边界矩形。

将其放置在中心位置,计算矩形和饼图切片的最大值和最小值的平均值,并将这些值的差值加到或减去要移动的任何一个。

答案 3 :(得分:0)

我会将问题分为三个步骤:

  1. 找到单位饼图切片的边界框(或者如果给定半径为实际饼图切片,以(0,0)为中心)。
  2. 在矩形中安装边界框。
  3. 使用有关拟合边界框的信息来调整饼图切片的中心和半径。
  4. 如果我有时间,我可以详细说明这一点。