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
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.
same as above, with current and outer boundaries, and centers connected in sequence, overlaid.
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: