--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/printrun-src/printrun/packer.py Fri Jun 03 09:16:07 2016 +0200 @@ -0,0 +1,230 @@ +# 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