Arrange rectangles in a spiral Arrange rectangles in a spiral tkinter tkinter

Arrange rectangles in a spiral


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 randomimport tkinter as tkWIDTH, HEIGHT = 800, 800CENTER = WIDTH // 2, HEIGHT // 2class 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_pointsif __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