将矩形排列成螺旋状
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()
提供的客户端截图:
我在从中间的第一个矩形 (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()
提供的客户端截图: