Search code examples
c#unity-game-enginetkintergeometryrectangles

Arrange rectangles in a spiral



I have a problem with arranging different recangles of different sizes spirally starting with the first rectangle in the middle (0/0). The anchor point of an rectangle is always in the top right corner. Here is a short pseudo code example of my current work. I have problems with the direction swap and the x-Axis correction during the upwards placement.

float x = 0;
float y = 0;

float width = 0;
float height = 0;

float nextWidth = 0;
float nextHeight = 0;

string next = "right";

for (int i = 0; i < rectangles.Length; i++)
{
    rectangles[i].Position = new Vector2(x,y);
    width = rectangles[i].Size.x;
    height = rectangles[i].Size.y;


    if (i < rectangles.Length - 1)
    {
        nextWidth = rectangles[i+1].Size.x;
        nextHeight = rectangles[i+1].Size.y;    
    }

    switch(next)
    {
        case "right":
            x += nextWidth;
            if (?)
            {
                next = "down";
            }

            break;

        case "down":
            x += nextWidth - width;
            y -= height;

            if (?)
            {
                next = "left";
            }

            break;

        case "left":
            x -= width;

            if (?)
            {
                next = "up";
            }

            break;

        case "up":
            //Still positioning problem with x-Axis
            y += nextHeight;

            if (?)
            {
                next = "right";
            }

            break;
    }
}

For a better understanding of my project I added a sketch:

enter image description here

I hope you understand what I try to do. Thanks for your help.

EDIT: On the basis of the solution provided below by Reblochon Masque:

City districts


Solution

  • This is not a completely trivial problem to solve; the orientation of the rectangles changes wrt the position of their anchor point, and the side they are assigned to; you need to keep track of the boundaries as they move with the addition of each new rectangle, keeping in mind that there are two important boundaries to account for: the boundary for the current turn, and the boundary for the next turn.

    Screenshots from the GUI posted on github:

    Showing anchor points, current and outer boundaries, and centers connected in sequence

    enter image description here

    Showing the entire gui and around 900 rectangles.
    note: the left grey bar allows you to click and choose a rectangle size to add it to the spiral.

    enter image description here

    same as above, with current and outer boundaries, and centers connected in sequence, overlaid.

    enter image description here

    The proposed code is in python; there is a small GUI client attached to it, and a slightly better one posted on github. Maybe this will provide some measure of inspiration to complete your c# project.

    """
    The coordinates at which a rectangle is anchored on canvas
    
    """
    
    
    import random
    import tkinter as tk
    
    WIDTH, HEIGHT = 800, 800
    CENTER = WIDTH // 2, HEIGHT // 2
    
    
    class Anchor:
        def __init__(self, x=0, y=0):
            self.x = x
            self.y = y
    
        def __add__(self, other):
            return Anchor(self.x + other[0], self.y + other[1])
    
        def __sub__(self, other):
            return Anchor(self.x - other[0], self.y - other[1])
    
        def __iter__(self):
            yield self.x
            yield self.y
    
        def __getitem__(self, idx):
            a = (self.x, self.y)
            return a[idx]
    
        def get_mid(self, other):
            ox, oy = other[0], other[1]
            return Anchor((self.x + ox) // 2, (self.y + oy) // 2)
    
        def clone(self):
            return Anchor(self.x, self.y)
    
        def __str__(self):
            return f'Anchor({self.x}, {self.y})'
    
    
    
    """
    a Rectangle
    
    """
    
    class Rectangle:
        def __init__(self, width, height):
            self.width = width
            self.height = height
            self.bbox = None
            self.norm_bbox = None
            self.calc_bbox(Anchor())
            self.normalize_bbox()
    
        def calc_bbox(self, anchor):
            x, y = anchor
            self.bbox = [Anchor(anchor.x, anchor.y), (x + self.width, y + self.height)]
            self.normalize_bbox()
    
        def normalize_bbox(self):
            """
            set the anchor point to the top left corner of the bbox
            :return:
            """
            p0, p1 = self.bbox
            x0, y0 = p0
            x1, y1 = p1
            self.norm_bbox = [Anchor(min(x0, x1), min(y0, y1)), Anchor(max(x0, x1), max(y0, y1))]
    
        def get_center(self):
            tl, br = self.bbox
            return tl.get_mid(br)
    
        def __str__(self):
            res = f'Rectangle of width= {self.width}, height= {self.height}, bbox at: ' \
                  f'{", ".join(str(elt) for elt in self.bbox)}'
            return res
    
    
    
    """
    # Spiral Of Squares:
    
    """
    
    
    class Spiral:
        """
        states:
        'right' --> add to the right side, going down
        'down'  --> add to the bottom side, going left
        'left'  --> add to the left side, going up
        'up'    --> add to the top side, going right
    
        """
    
        def __init__(self, anchor=CENTER, xoffset: int=5, yoffset: int=5):
            self.anchor = Anchor(*anchor)
            lr, td = self.anchor.x, self.anchor.y
            self.boundaries = {'right': lr, 'down': td, 'left': lr, 'up': td}
            self.current_x, self.current_y = self.anchor
            self.inner_boundaries = {'right': lr, 'down': td, 'left': lr, 'up': td}
            self.add_to = None
            self.xoffset = xoffset
            self.yoffset = yoffset
            self.rectangles = []
            self.anchor_points = [self.anchor.clone()]
    
        def add_rectangle(self, rect):
            self.rectangles.append(rect)
            if len(self.rectangles) == 1:
                self.place_first(rect)
            else:
                self.place(rect)
                self.calc_next_add_to_side()
    
        def place_first(self, rect):
            """
            places the first rectangle at current anchor
            updates the anchor
            """
            self.inner_boundaries = {'right': self.anchor.x + rect.width, 'down': self.anchor.y + rect.height,
                                     'left': self.anchor.x, 'up': self.anchor.y}
            self.boundaries = {k: v for k, v in self.inner_boundaries.items()}
            rect.calc_bbox(self.anchor.clone())
            self.anchor = self.anchor + (rect.width + self.xoffset, 0)
            self.add_to = 'right'
            self.anchor_points.append(self.anchor.clone())
    
        def place(self, rect):
            """
            places a rectangle at the current anchor, taking offsets and side into account,
            and minding the orientation of the rectangle wrt anchor point
            """
            w, h = rect.width, rect.height
            anchor = self.anchor.clone()
    
            if self.add_to == 'right':
                rect.calc_bbox(anchor)
                self.boundaries['right'] = max(self.boundaries['right'], self.inner_boundaries['right'] + w + self.xoffset)
                if self.boundaries['down'] < anchor.y + h:
                    self.boundaries['down'] = anchor.y + h
    
            if self.add_to == 'down':
                anchor = anchor + (-w, 0)
                rect.calc_bbox(anchor)
                self.anchor = self.anchor + (-w, 0)
                self.boundaries['down'] = max(self.boundaries['down'], self.inner_boundaries['down'] + h + self.yoffset)
                if self.boundaries['left'] > self.anchor.x:  # -w already accounted for
                    self.boundaries['left'] = self.anchor.x
    
            if self.add_to == 'left':
                anchor = anchor + (-w, -h)
                rect.calc_bbox(anchor)
                self.anchor = self.anchor + (-w, -h)
                self.boundaries['left'] = min(self.boundaries['left'], self.inner_boundaries['left'] - w - self.xoffset)
                if self.boundaries['up'] > self.anchor.y - h:
                    self.boundaries['up'] = self.anchor.y
    
            if self.add_to == 'up':
                anchor = anchor + (0, -h)
                rect.calc_bbox(anchor)
                self.anchor = self.anchor + (w, -h)
                self.boundaries['up'] = min(self.boundaries['up'], self.inner_boundaries['up'] - h - self.yoffset)
                if self.boundaries['right'] < self.anchor.x + w:
                    self.boundaries['right'] = self.anchor.x
    
        def calc_next_add_to_side(self):
            """
            calculates the next anchor position.
            cyclically updates the inner boundary for the next turn; this is out of phase
            so it doesn't affect the current turn.
            """
    
            w, h = self.rectangles[-1].width, self.rectangles[-1].height
            current_x, current_y = self.anchor
    
            if self.add_to == 'right':
                if current_y + h < self.inner_boundaries['down']:   # not overstep border
                    current_x = self.inner_boundaries['right'] + self.xoffset
                    current_y += h + self.yoffset
                else:                                               # oversteps -> change direction
                    self.add_to = 'down'
                    current_x += self.xoffset
                    current_x = self.inner_boundaries['right']
                    current_y = self.inner_boundaries['down'] + self.yoffset
                self.inner_boundaries['left'] = self.boundaries['left']
    
            elif self.add_to == 'down':
                if current_x > self.inner_boundaries['left']:
                    current_x -= self.xoffset
                else:
                    self.add_to = 'left'
                    current_x = self.inner_boundaries['left'] - self.xoffset
                    current_y = self.inner_boundaries['down']
                self.inner_boundaries['up'] = self.boundaries['up']
    
            elif self.add_to == 'left':
                if current_y > self.inner_boundaries['up']:
                    current_x = self.inner_boundaries['left'] - self.xoffset
                    current_y -= self.yoffset
                else:
                    self.add_to = 'up'
                    current_x = self.inner_boundaries['left']
                    current_y = self.inner_boundaries['up'] - self.yoffset
                self.inner_boundaries['right'] = self.boundaries['right']
    
            elif self.add_to == 'up':
                if current_x < self.inner_boundaries['right']:
                    current_x = current_x + self.xoffset
                    current_y = self.inner_boundaries['up'] - self.yoffset
                else:
                    self.add_to = 'right'
                    current_x = self.inner_boundaries['right'] + self.xoffset
                    current_y = self.inner_boundaries['up']
                self.inner_boundaries['down'] = self.boundaries['down']
    
            self.anchor = Anchor(current_x, current_y)
            self.anchor_points.append(self.anchor.clone())
    
        def get_current_boundaries(self):
            return self.inner_boundaries
    
        def get_boundaries(self):
            return self.boundaries
    
        def get_anchor_points(self):
            return self.anchor_points
    
        def get_center_points(self):
            center_points = []
            for rect in self.rectangles:
                center = rect.get_center()
                center_points.append(center)
            return center_points
    
    
    if __name__ == '__main__':
    
        cr = 0
        if cr:
            num_rect = 18
        else:
            num_rect = 121
        rectangles = [Rectangle(random.randrange(30, 60), random.randrange(30, 60)) for _ in range(num_rect)]
    
        spiral = Spiral()
        for rect in rectangles:
            spiral.add_rectangle(rect)
    
        root = tk.Tk()
        canvas = tk.Canvas(root, width=WIDTH, height=HEIGHT, bg='beige')
        canvas.pack(expand=True, fill='both')
    
        if cr:
            for idx, (rect, color) in enumerate(zip(spiral.rectangles, ['blue', 'red', 'green', 'black', 'cyan', 'grey', 'purple',\
                    'lightgreen', 'lightblue', 'gold', 'black', 'blue', 'red', 'green', 'black', 'cyan', 'grey', 'purple'])):
                tl, br = rect.norm_bbox
                canvas.create_rectangle(*tl, *br, fill='white', outline=color, width=2)
                x, y = tl
                canvas.create_oval(x + 2, y + 2, x - 2, y - 1)
                print(*rect.get_center())
                canvas.create_text(*rect.get_center(), text=str(idx))
        else:
            for idx, rect in enumerate(spiral.rectangles):
                tl, br = rect.norm_bbox
                canvas.create_rectangle(*tl, *br, fill='white', outline='black', width=2)
                x, y = tl
                canvas.create_oval(x + 2, y + 2, x - 2, y - 1)
                print(*rect.get_center())
                canvas.create_text(*rect.get_center(), text=str(idx))
    
        root.mainloop()
    

    Screenshot from the client provided:

    enter image description here