Commit baa22754 authored by Guillaume Seguin's avatar Guillaume Seguin

Import alternative packing algorithm

parent 44ac2405
## Imported from python-rectangle-packer commit 32fce1aaba
## https://github.com/maxretter/python-rectangle-packer
##
## Python Rectangle Packer - Packs rectangles around a central point
## Copyright (C) 2013 Max Retter
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation, either version 3 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program. If not, see <http://www.gnu.org/licenses/>.
import math
import Polygon
import Polygon.Utils
class Vector2(object):
"""Simple 2d vector / point class."""
def __init__(self, x=0, y=0):
self.x = float(x)
self.y = float(y)
def __eq__(self, other):
return self.x == other.x and self.y == other.y
def add(self, other):
return Vector2(self.x + other.x, self.y + other.y)
def sub(self, other):
return Vector2(self.x - other.x, self.y - other.y)
def scale(self, factor):
return Vector2(self.x * factor, self.y * factor)
def magnitude(self):
return math.sqrt(self.dot_product(self))
def unit(self):
"""Build unit vector."""
return self.scale(1 / self.magnitude())
def dot_product(self, other):
return self.x * other.x + self.y * other.y
def distance(self, other):
"""Distance forumla for other point."""
return math.sqrt(
(other.x - self.x) ** 2 +
(other.y - self.y) ** 2
)
class Rect(object):
"""Simple rectangle object."""
def __init__(self, width, height, data={}):
self.width = width
self.height = height
self.data = data
## upper left
self.position = Vector2()
def half(self):
"""Half width and height."""
return Vector2(
self.width / 2,
self.height / 2
)
def expand(self, width, height):
"""Builds a new rectangle based on this one with given offsets."""
expanded = Rect(self.width + width, self.height + height)
expanded.set_center(self.center())
return expanded
def point_list(self):
top = self.position.y
right = self.position.x + self.width
bottom = self.position.y + self.height
left = self.position.x
return PointList([
(left, top),
(right, top),
(right, bottom),
(left, bottom),
])
def center(self):
"""Center of rect calculated from position and dimensions."""
return self.position.add(self.half())
def set_center(self, center):
"""Set the position based on a new center point."""
self.position = center.sub(self.half())
def area(self):
"""Area: length * width."""
return self.width * self.height
class PointList(object):
"""Methods for transforming a list of points."""
def __init__(self, points=[]):
self.points = points
self._polygon = None
def polygon(self):
"""Builds a polygon from the set of points."""
if not self._polygon:
self._polygon = Polygon.Polygon(self.points)
return self._polygon
def segments(self):
"""Returns a list of LineSegment objects."""
segs = []
for i, point in enumerate(self.points[1:]):
index = i + 1
segs.append(LineSegment(
Vector2(self.points[index-1][0], self.points[index-1][1]),
Vector2(self.points[index][0], self.points[index][1])
))
segs.append(LineSegment(
Vector2(self.points[-1][0], self.points[-1][1]),
Vector2(self.points[0][0], self.points[0][1]),
))
return segs
class LineSegment(object):
def __init__(self, start, end):
self.start = start
self.end = end
def length(self):
"""Length of segment vector."""
return self.end.sub(self.start).magnitude()
def closest_point_to_point(self, point):
"""Point along segment that is closest to given point."""
segment_vector = self.end.sub(self.start)
point_vector = point.sub(self.start)
seg_mag = segment_vector.magnitude()
## project point_vector on segment_vector
projection = segment_vector.dot_product(point_vector)
## scalar value used to interpolate new point along segment_vector
scalar = projection / seg_mag ** 2
## clamp on [0,1]
scalar = 1.0 if scalar > 1.0 else scalar
scalar = 0.0 if scalar < 0.0 else scalar
## interpolate scalar along segment and add start point back in
return self.start.add(segment_vector.unit().scale(scalar * seg_mag))
def closest_distance_to_point(self, point):
"""Helper method too automatically return distance."""
closest_point = self.closest_point_to_point(point)
return closest_point.distance(point)
class Packer(object):
def __init__(self):
self._rects = []
def add_rect(self, width, height, data={}):
self._rects.append(Rect(width, height, data))
def pack(self, padding=0, center=Vector2()):
## init everything
placed_rects = []
sorted_rects = sorted(self._rects, key=lambda rect: -rect.area())
## double padding due to halfing later on
padding *= 2
for rect in sorted_rects:
if not placed_rects:
## first rect, right on target.
rect.set_center(center)
else:
## Expand each rectangle based on new rect size and padding
## get a list of points
## build a polygon
point_lists = [
pr.expand(rect.width + padding, rect.height + padding).point_list().polygon()
for pr in placed_rects
]
## take the union of all the polygons (relies on + operator override)
## the [0] at the end returns the first "contour", which is the only one we need
bounding_points = PointList(sum(
point_lists[1:],
point_lists[0]
)[0])
## find the closest segment
closest_segments = sorted(
bounding_points.segments(),
key=lambda segment: segment.closest_distance_to_point(center)
)
## get the closest point
place_point = closest_segments[0].closest_point_to_point(center)
## set the rect position
rect.set_center(place_point)
placed_rects.append(rect)
return placed_rects
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment