将矩形排列成螺旋状

Arrange rectangles in a spiral


我在从中间的第一个矩形 (0/0) 开始螺旋排列不同大小的不同 recangles 时遇到问题。矩形的锚点总是在右上角。这是我当前工作的一个简短的伪代码示例。我在向上放置期间遇到方向交换和 x 轴校正问题。

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;
    }
}

为了更好地理解我的项目,我添加了一个草图:

我希望你明白我想做什么。感谢您的帮助。

编辑: 基于 Reblochon Masque 提供的以下解决方案:

这不是一个需要解决的完全微不足道的问题;矩形的方向改变了它们的锚点的位置,以及它们被分配到的一侧;您需要跟踪边界随着每个新矩形的添加而移动,请记住有两个重要的边界需要考虑:当前回合的边界和下一回合的边界。

来自 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.

提议的代码在python;它附有一个小型 GUI 客户端,github 上发布了一个稍微好一点的客户端。也许这会提供一些灵感来完成您的 c# 项目。

"""
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()

提供的客户端截图: