给定一个矩形(w,h)和一个半径小于或等于两边(w,h)中较小的扇形切片,一个起始角和一个终止角,如何将切片最佳地放置在矩形,以便它最好地填充房间(从光学的角度来看,不是从数学角度来说)?
我目前正在将饼形切片的中心放在矩形的中心,并使用两个矩形边中较小的一半作为半径。这为某些配置留下了足够的空间。
根据切片被绘制成单位圆的前提条件(即在正X轴上为0度,然后以顺时针方向运行)来明确我所追求的内容的示例:
对于我绘制的情况来说,这是相当容易的,但是当开始和结束角度是任意的时,它会变得复杂。我正在寻找一种算法,该算法以最佳填充矩形的方式确定切片中心和半径。由于我不是一位伟大的数学家,伪代码会很棒。
答案 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)
示例输出:
答案 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 * PI
到2 * 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
并在这种情况下使用其他值。 xRadius
和yRadius
不能同时为0
,因为对于上面包含的所有角度,sin
和cos
都必须为0
是这样的。
现在我们必须放置弧的中心。我们希望它集中在两个方向。现在我们将创建另一个线性方程:
(boundingBox[0] + boundingBox[1]) / 2 * radius + x = xCenter = w/2
(boundingBox[2] + boundingBox[3]) / 2 * radius + y = yCenter = h/2
求解弧的中心x
和y
,给出
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)
我会将问题分为三个步骤:
如果我有时间,我可以详细说明这一点。