views:

245

answers:

4

Given a rectangle (w, h) and a pie slice with a radius less or equal to the smaller of both sides (w, h), a start angle and an end angle, how can I place the slice optimally in the rectangle so that it fills the room best (from an optical point of view, not mathematically speaking)?

I'm currently placing the pie slice's center in the center of the rectangle and use the half of the smaller of both rectangle sides as the radius. This leaves plenty of room for certain configurations.

Examples to make clear what I'm after, based on the precondition that the slice is drawn like a unit circle (i.e. 0 degrees on positive X axis, then running clock-wise):

  • A start angle of 0 and an end angle of PI would lead to a filled lower half of the rectangle and an empty upper half. A good solution here would be to move the center up by 1/4*h.
  • A start angle of 0 and an end angle of PI/2 would lead to a filled bottom right quarter of the rectangle. A good solution here would be to move the center point to the top left of the rectangle and to set the radius to the smaller of both rectangle sides.

This is fairly easy for the cases I've sketched but it becomes complicated when the start and end angles are arbitrary. I am searching for an algorithm which determines center of the slice and radius in a way that fills the rectangle best. Pseudo code would be great since I'm not a big mathematician.

A: 

Just consider a circle and forget the filling. The bounds will either be the center of the circle, the endpoints, or the points at 0, 90, 180, or 270 degrees (if they exist in this slice). The maxima and minima of these seven points will determine your bounding rectangle.

As far as placing it in the center, calculate the average of the max and min for both the rectangle and the pie slice, and add or subtract the difference of these to whichever one you want to move.

tom10
Or the center...
@algorithmist - thanks for the correction. I've fixed my answer.
tom10
If your angle was 100°, your width calculation would either be a little under or a lot over. Same goes for height and an angle of 190°. You have to do the trigonometry.
Seth
If we're talking 0--100, then the leftmost point is the second endpoint, the rightmost is 0/the first endpoint, the topmost is 90, and the bottommost is the center. I don't understand your criticism---my understanding of the OP is that the slice cannot be rotated.
@Seth and algorithmist - I don't think it matters where the start and endpoints are, etc. For all curves the extrema are either at critical points (where the derivative is zero or doesn't exist) or at end points.
tom10
+2  A: 

The extrema of the bounding box of your arc are in the following format:

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

The values x0, x1, y0 and y1 are found by taking the minimum and maximum values of up to 7 points: any tangential points that are spanned (i.e. 0, 90, 180 and 270 degrees) and the end points of the two line segments.

Given the extrema of the axis-aligned bounding box of the arc (x0, y0), (x1, y1) the radius and center point can be calculated as follows:

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

Here is an implementation written in 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)

Example output:

alt text

alt text

Judge Maygarden
These calculations will put the pie slice in the upper left corner instead of centering it.
clahey
Apparently, I have completely misunderstood your question...
Judge Maygarden
Where did you specify that the arc should be centered? Maximizing the area of the arc and placing it in the center of the rectangle are contradictory.
Judge Maygarden
I don't see how maximizing the area of the arc and placing it in the center of the rectangle are contradictory. You just make the biggest arc that would fit and put half of the extra space on either side.
clahey
I've been thinking about what it might mean to center the center of mass of the arc and in that case, it may very well lead to a smaller arc. I am curious what the center of mass of a filled arc is though.
clahey
Judge Maygarden
I updated the code sample with a function to center the arc's AABB within the rectangle. This does not place the center of mass in the center of the rectangle, just the extents of the arc.
Judge Maygarden
Judge, thanks alot. This is EXACTLY what I was looking for. Your support and commitment is greatly appreciated! I don't know Lua yet but I think I get the points.
Lisa
A: 

I would divide the problem into three steps:

  1. Find the bounding box of a unit pie slice (or if a radius is given the actual pie slice centered at (0, 0)).
  2. Fit the bounding box in your rectangle.
  3. Use the information about fitting the bounding box to adjust the center and radius of the pie slice.

When I have time, I may flush this out with more details.

Kathy Van Stone
+1  A: 

Instead of pseudo code, I used python, but it should be usable. For this algorithm, I assume that startAngle < endAngle and that both are within [-2 * PI, 2 * PI]. If you want to use both within [0, 2 * PI] and let startAngle > endAngle, I would do:

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

So, the algorithm that comes to mind is to calculate the bounds of the unit arc and then scale to fit your rectangle.

The first is the harder part. You need to calculate 4 numbers:

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

Of course, angle is a range, so it's not as simple as this. However, you really only have to include up to 11 points in this calculation. The start angle, the end angle, and potentially, the cardinal directions (there are 9 of these going from -2 * PI to 2 * PI.) I'm going to define boundingBoxes as lists of 4 elements, ordered [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)

Now you've computed the bounding box of an arc with center of 0,0 and radius of 1. To fill the box, we're going to have to solve a linear equation:

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

And we have to solve for xRadius and yRadius. You'll note there are two radiuses here. The reason for that is that in order to fill the rectangle, we have to multiple by different amounts in the two directions. Since your algorithm asks for only one radius, we will just pick the lower of the two values.

Solving the equation gives:

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

Here, you have to check for boundingBox[1] - boundingBox[0] being 0 and set xRadius to infinity in that case. This will give the correct result as yRadius will be smaller. If you don't have an infinity available, you can just set it to 0 and in the MIN function, check for 0 and use the other value in that case. xRadius and yRadius can't both be 0 because both sin and cos would have to be 0 for all angles included above for that to be the case.

Now we have to place the center of the arc. We want it centered in both directions. Now we'll create another linear equation:

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

Solving for x and y, the center of the arc, gives

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

This should give you the center of the arc and the radius needed to put the largest circle in the given rectangle.

I haven't tested any of this code, so this algorithm may have huge holes, or perhaps tiny ones caused by typos. I'd love to know if this algoritm works.

edit:

Putting all of the code together gives:

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

edit:

One issue here is that sin[2 * PI] is not going to be exactly 0 because of rounding errors. I think the solution is to get rid of the CheckAngle calls and replace them with something like:

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)

You still need IncludeAngle(startAngle) and IncludeAngle(endAngle)

clahey
clahey, thanks alot. I've voted for Judge's answer since he has also thought of the centering, but your solution looks good to me as well, even though the CheckCardinal method has some glitches (check the parameter ordering and the modulo operation, should probably add a Math.abs(...) around it?!).
Lisa
Fixed the parameter order. I just checked and in python, at least, that modulo operation is always going to be positive. Other languages may have different rules if the first number is negative and the second positive.
clahey