forked from LeenkxTeam/LNXSDK
Update Files
This commit is contained in:
23
leenkx/blender/lnx/lightmapper/utility/rectpack/__init__.py
Normal file
23
leenkx/blender/lnx/lightmapper/utility/rectpack/__init__.py
Normal file
@ -0,0 +1,23 @@
|
||||
from .guillotine import GuillotineBssfSas, GuillotineBssfLas, \
|
||||
GuillotineBssfSlas, GuillotineBssfLlas, GuillotineBssfMaxas, \
|
||||
GuillotineBssfMinas, GuillotineBlsfSas, GuillotineBlsfLas, \
|
||||
GuillotineBlsfSlas, GuillotineBlsfLlas, GuillotineBlsfMaxas, \
|
||||
GuillotineBlsfMinas, GuillotineBafSas, GuillotineBafLas, \
|
||||
GuillotineBafSlas, GuillotineBafLlas, GuillotineBafMaxas, \
|
||||
GuillotineBafMinas
|
||||
|
||||
from .maxrects import MaxRectsBl, MaxRectsBssf, MaxRectsBaf, MaxRectsBlsf
|
||||
|
||||
from .skyline import SkylineMwf, SkylineMwfl, SkylineBl, \
|
||||
SkylineBlWm, SkylineMwfWm, SkylineMwflWm
|
||||
|
||||
from .packer import SORT_AREA, SORT_PERI, SORT_DIFF, SORT_SSIDE, \
|
||||
SORT_LSIDE, SORT_RATIO, SORT_NONE
|
||||
|
||||
from .packer import PackerBNF, PackerBFF, PackerBBF, PackerOnlineBNF, \
|
||||
PackerOnlineBFF, PackerOnlineBBF, PackerGlobal, newPacker, \
|
||||
PackingMode, PackingBin, float2dec
|
||||
|
||||
|
||||
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
148
leenkx/blender/lnx/lightmapper/utility/rectpack/enclose.py
Normal file
148
leenkx/blender/lnx/lightmapper/utility/rectpack/enclose.py
Normal file
@ -0,0 +1,148 @@
|
||||
import heapq # heapq.heappush, heapq.heappop
|
||||
from .packer import newPacker, PackingMode, PackingBin, SORT_LSIDE
|
||||
from .skyline import SkylineBlWm
|
||||
|
||||
|
||||
|
||||
class Enclose(object):
|
||||
|
||||
def __init__(self, rectangles=[], max_width=None, max_height=None, rotation=True):
|
||||
"""
|
||||
Arguments:
|
||||
rectangles (list): Rectangle to be enveloped
|
||||
[(width1, height1), (width2, height2), ...]
|
||||
max_width (number|None): Enveloping rectangle max allowed width.
|
||||
max_height (number|None): Enveloping rectangle max allowed height.
|
||||
rotation (boolean): Enable/Disable rectangle rotation.
|
||||
"""
|
||||
# Enclosing rectangle max width
|
||||
self._max_width = max_width
|
||||
|
||||
# Encloseing rectangle max height
|
||||
self._max_height = max_height
|
||||
|
||||
# Enable or disable rectangle rotation
|
||||
self._rotation = rotation
|
||||
|
||||
# Default packing algorithm
|
||||
self._pack_algo = SkylineBlWm
|
||||
|
||||
# rectangles to enclose [(width, height), (width, height, ...)]
|
||||
self._rectangles = []
|
||||
for r in rectangles:
|
||||
self.add_rect(*r)
|
||||
|
||||
def _container_candidates(self):
|
||||
"""Generate container candidate list
|
||||
|
||||
Returns:
|
||||
tuple list: [(width1, height1), (width2, height2), ...]
|
||||
"""
|
||||
if not self._rectangles:
|
||||
return []
|
||||
|
||||
if self._rotation:
|
||||
sides = sorted(side for rect in self._rectangles for side in rect)
|
||||
max_height = sum(max(r[0], r[1]) for r in self._rectangles)
|
||||
min_width = max(min(r[0], r[1]) for r in self._rectangles)
|
||||
max_width = max_height
|
||||
else:
|
||||
sides = sorted(r[0] for r in self._rectangles)
|
||||
max_height = sum(r[1] for r in self._rectangles)
|
||||
min_width = max(r[0] for r in self._rectangles)
|
||||
max_width = sum(sides)
|
||||
|
||||
if self._max_width and self._max_width < max_width:
|
||||
max_width = self._max_width
|
||||
|
||||
if self._max_height and self._max_height < max_height:
|
||||
max_height = self._max_height
|
||||
|
||||
assert(max_width>min_width)
|
||||
|
||||
# Generate initial container widths
|
||||
candidates = [max_width, min_width]
|
||||
|
||||
width = 0
|
||||
for s in reversed(sides):
|
||||
width += s
|
||||
candidates.append(width)
|
||||
|
||||
width = 0
|
||||
for s in sides:
|
||||
width += s
|
||||
candidates.append(width)
|
||||
|
||||
candidates.append(max_width)
|
||||
candidates.append(min_width)
|
||||
|
||||
# Remove duplicates and widths too big or small
|
||||
seen = set()
|
||||
seen_add = seen.add
|
||||
candidates = [x for x in candidates if not(x in seen or seen_add(x))]
|
||||
candidates = [x for x in candidates if not(x>max_width or x<min_width)]
|
||||
|
||||
# Remove candidates too small to fit all the rectangles
|
||||
min_area = sum(r[0]*r[1] for r in self._rectangles)
|
||||
return [(c, max_height) for c in candidates if c*max_height>=min_area]
|
||||
|
||||
def _refine_candidate(self, width, height):
|
||||
"""
|
||||
Use bottom-left packing algorithm to find a lower height for the
|
||||
container.
|
||||
|
||||
Arguments:
|
||||
width
|
||||
height
|
||||
|
||||
Returns:
|
||||
tuple (width, height, PackingAlgorithm):
|
||||
"""
|
||||
packer = newPacker(PackingMode.Offline, PackingBin.BFF,
|
||||
pack_algo=self._pack_algo, sort_algo=SORT_LSIDE,
|
||||
rotation=self._rotation)
|
||||
packer.add_bin(width, height)
|
||||
|
||||
for r in self._rectangles:
|
||||
packer.add_rect(*r)
|
||||
|
||||
packer.pack()
|
||||
|
||||
# Check all rectangles where packed
|
||||
if len(packer[0]) != len(self._rectangles):
|
||||
return None
|
||||
|
||||
# Find highest rectangle
|
||||
new_height = max(packer[0], key=lambda x: x.top).top
|
||||
return(width, new_height, packer)
|
||||
|
||||
def generate(self):
|
||||
|
||||
# Generate initial containers
|
||||
candidates = self._container_candidates()
|
||||
if not candidates:
|
||||
return None
|
||||
|
||||
# Refine candidates and return the one with the smaller area
|
||||
containers = [self._refine_candidate(*c) for c in candidates]
|
||||
containers = [c for c in containers if c]
|
||||
if not containers:
|
||||
return None
|
||||
|
||||
width, height, packer = min(containers, key=lambda x: x[0]*x[1])
|
||||
|
||||
packer.width = width
|
||||
packer.height = height
|
||||
return packer
|
||||
|
||||
def add_rect(self, width, height):
|
||||
"""
|
||||
Add anoter rectangle to be enclosed
|
||||
|
||||
Arguments:
|
||||
width (number): Rectangle width
|
||||
height (number): Rectangle height
|
||||
"""
|
||||
self._rectangles.append((width, height))
|
||||
|
||||
|
344
leenkx/blender/lnx/lightmapper/utility/rectpack/geometry.py
Normal file
344
leenkx/blender/lnx/lightmapper/utility/rectpack/geometry.py
Normal file
@ -0,0 +1,344 @@
|
||||
from math import sqrt
|
||||
|
||||
|
||||
|
||||
class Point(object):
|
||||
|
||||
__slots__ = ('x', 'y')
|
||||
|
||||
def __init__(self, x, y):
|
||||
self.x = x
|
||||
self.y = y
|
||||
|
||||
def __eq__(self, other):
|
||||
return (self.x == other.x and self.y == other.y)
|
||||
|
||||
def __repr__(self):
|
||||
return "P({}, {})".format(self.x, self.y)
|
||||
|
||||
def distance(self, point):
|
||||
"""
|
||||
Calculate distance to another point
|
||||
"""
|
||||
return sqrt((self.x-point.x)**2+(self.y-point.y)**2)
|
||||
|
||||
def distance_squared(self, point):
|
||||
return (self.x-point.x)**2+(self.y-point.y)**2
|
||||
|
||||
|
||||
class Segment(object):
|
||||
|
||||
__slots__ = ('start', 'end')
|
||||
|
||||
def __init__(self, start, end):
|
||||
"""
|
||||
Arguments:
|
||||
start (Point): Segment start point
|
||||
end (Point): Segment end point
|
||||
"""
|
||||
assert(isinstance(start, Point) and isinstance(end, Point))
|
||||
self.start = start
|
||||
self.end = end
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, self.__class__):
|
||||
None
|
||||
return self.start==other.start and self.end==other.end
|
||||
|
||||
def __repr__(self):
|
||||
return "S({}, {})".format(self.start, self.end)
|
||||
|
||||
@property
|
||||
def length_squared(self):
|
||||
"""Faster than length and useful for some comparisons"""
|
||||
return self.start.distance_squared(self.end)
|
||||
|
||||
@property
|
||||
def length(self):
|
||||
return self.start.distance(self.end)
|
||||
|
||||
@property
|
||||
def top(self):
|
||||
return max(self.start.y, self.end.y)
|
||||
|
||||
@property
|
||||
def bottom(self):
|
||||
return min(self.start.y, self.end.y)
|
||||
|
||||
@property
|
||||
def right(self):
|
||||
return max(self.start.x, self.end.x)
|
||||
|
||||
@property
|
||||
def left(self):
|
||||
return min(self.start.x, self.end.x)
|
||||
|
||||
|
||||
class HSegment(Segment):
|
||||
"""Horizontal Segment"""
|
||||
|
||||
def __init__(self, start, length):
|
||||
"""
|
||||
Create an Horizontal segment given its left most end point and its
|
||||
length.
|
||||
|
||||
Arguments:
|
||||
- start (Point): Starting Point
|
||||
- length (number): segment length
|
||||
"""
|
||||
assert(isinstance(start, Point) and not isinstance(length, Point))
|
||||
super(HSegment, self).__init__(start, Point(start.x+length, start.y))
|
||||
|
||||
@property
|
||||
def length(self):
|
||||
return self.end.x-self.start.x
|
||||
|
||||
|
||||
class VSegment(Segment):
|
||||
"""Vertical Segment"""
|
||||
|
||||
def __init__(self, start, length):
|
||||
"""
|
||||
Create a Vertical segment given its bottom most end point and its
|
||||
length.
|
||||
|
||||
Arguments:
|
||||
- start (Point): Starting Point
|
||||
- length (number): segment length
|
||||
"""
|
||||
assert(isinstance(start, Point) and not isinstance(length, Point))
|
||||
super(VSegment, self).__init__(start, Point(start.x, start.y+length))
|
||||
|
||||
@property
|
||||
def length(self):
|
||||
return self.end.y-self.start.y
|
||||
|
||||
|
||||
|
||||
class Rectangle(object):
|
||||
"""Basic rectangle primitive class.
|
||||
x, y-> Lower right corner coordinates
|
||||
width -
|
||||
height -
|
||||
"""
|
||||
__slots__ = ('width', 'height', 'x', 'y', 'rid')
|
||||
|
||||
def __init__(self, x, y, width, height, rid = None):
|
||||
"""
|
||||
Args:
|
||||
x (int, float):
|
||||
y (int, float):
|
||||
width (int, float):
|
||||
height (int, float):
|
||||
rid (int):
|
||||
"""
|
||||
assert(height >=0 and width >=0)
|
||||
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.rid = rid
|
||||
|
||||
@property
|
||||
def bottom(self):
|
||||
"""
|
||||
Rectangle bottom edge y coordinate
|
||||
"""
|
||||
return self.y
|
||||
|
||||
@property
|
||||
def top(self):
|
||||
"""
|
||||
Rectangle top edge y coordiante
|
||||
"""
|
||||
return self.y+self.height
|
||||
|
||||
@property
|
||||
def left(self):
|
||||
"""
|
||||
Rectangle left ednge x coordinate
|
||||
"""
|
||||
return self.x
|
||||
|
||||
@property
|
||||
def right(self):
|
||||
"""
|
||||
Rectangle right edge x coordinate
|
||||
"""
|
||||
return self.x+self.width
|
||||
|
||||
@property
|
||||
def corner_top_l(self):
|
||||
return Point(self.left, self.top)
|
||||
|
||||
@property
|
||||
def corner_top_r(self):
|
||||
return Point(self.right, self.top)
|
||||
|
||||
@property
|
||||
def corner_bot_r(self):
|
||||
return Point(self.right, self.bottom)
|
||||
|
||||
@property
|
||||
def corner_bot_l(self):
|
||||
return Point(self.left, self.bottom)
|
||||
|
||||
def __lt__(self, other):
|
||||
"""
|
||||
Compare rectangles by area (used for sorting)
|
||||
"""
|
||||
return self.area() < other.area()
|
||||
|
||||
def __eq__(self, other):
|
||||
"""
|
||||
Equal rectangles have same area.
|
||||
"""
|
||||
if not isinstance(other, self.__class__):
|
||||
return False
|
||||
|
||||
return (self.width == other.width and \
|
||||
self.height == other.height and \
|
||||
self.x == other.x and \
|
||||
self.y == other.y)
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.x, self.y, self.width, self.height))
|
||||
|
||||
def __iter__(self):
|
||||
"""
|
||||
Iterate through rectangle corners
|
||||
"""
|
||||
yield self.corner_top_l
|
||||
yield self.corner_top_r
|
||||
yield self.corner_bot_r
|
||||
yield self.corner_bot_l
|
||||
|
||||
def __repr__(self):
|
||||
return "R({}, {}, {}, {})".format(self.x, self.y, self.width, self.height)
|
||||
|
||||
def area(self):
|
||||
"""
|
||||
Rectangle area
|
||||
"""
|
||||
return self.width * self.height
|
||||
|
||||
def move(self, x, y):
|
||||
"""
|
||||
Move Rectangle to x,y coordinates
|
||||
|
||||
Arguments:
|
||||
x (int, float): X coordinate
|
||||
y (int, float): Y coordinate
|
||||
"""
|
||||
self.x = x
|
||||
self.y = y
|
||||
|
||||
def contains(self, rect):
|
||||
"""
|
||||
Tests if another rectangle is contained by this one
|
||||
|
||||
Arguments:
|
||||
rect (Rectangle): The other rectangle
|
||||
|
||||
Returns:
|
||||
bool: True if it is container, False otherwise
|
||||
"""
|
||||
return (rect.y >= self.y and \
|
||||
rect.x >= self.x and \
|
||||
rect.y+rect.height <= self.y+self.height and \
|
||||
rect.x+rect.width <= self.x+self.width)
|
||||
|
||||
def intersects(self, rect, edges=False):
|
||||
"""
|
||||
Detect intersections between this and another Rectangle.
|
||||
|
||||
Parameters:
|
||||
rect (Rectangle): The other rectangle.
|
||||
edges (bool): True to consider rectangles touching by their
|
||||
edges or corners to be intersecting.
|
||||
(Should have been named include_touching)
|
||||
|
||||
Returns:
|
||||
bool: True if the rectangles intersect, False otherwise
|
||||
"""
|
||||
if edges:
|
||||
if (self.bottom > rect.top or self.top < rect.bottom or\
|
||||
self.left > rect.right or self.right < rect.left):
|
||||
return False
|
||||
else:
|
||||
if (self.bottom >= rect.top or self.top <= rect.bottom or
|
||||
self.left >= rect.right or self.right <= rect.left):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def intersection(self, rect, edges=False):
|
||||
"""
|
||||
Returns the rectangle resulting of the intersection between this and another
|
||||
rectangle. If the rectangles are only touching by their edges, and the
|
||||
argument 'edges' is True the rectangle returned will have an area of 0.
|
||||
Returns None if there is no intersection.
|
||||
|
||||
Arguments:
|
||||
rect (Rectangle): The other rectangle.
|
||||
edges (bool): If True Rectangles touching by their edges are
|
||||
considered to be intersection. In this case a rectangle of
|
||||
0 height or/and width will be returned.
|
||||
|
||||
Returns:
|
||||
Rectangle: Intersection.
|
||||
None: There was no intersection.
|
||||
"""
|
||||
if not self.intersects(rect, edges=edges):
|
||||
return None
|
||||
|
||||
bottom = max(self.bottom, rect.bottom)
|
||||
left = max(self.left, rect.left)
|
||||
top = min(self.top, rect.top)
|
||||
right = min(self.right, rect.right)
|
||||
|
||||
return Rectangle(left, bottom, right-left, top-bottom)
|
||||
|
||||
def join(self, other):
|
||||
"""
|
||||
Try to join a rectangle to this one, if the result is also a rectangle
|
||||
and the operation is successful and this rectangle is modified to the union.
|
||||
|
||||
Arguments:
|
||||
other (Rectangle): Rectangle to join
|
||||
|
||||
Returns:
|
||||
bool: True when successfully joined, False otherwise
|
||||
"""
|
||||
if self.contains(other):
|
||||
return True
|
||||
|
||||
if other.contains(self):
|
||||
self.x = other.x
|
||||
self.y = other.y
|
||||
self.width = other.width
|
||||
self.height = other.height
|
||||
return True
|
||||
|
||||
if not self.intersects(other, edges=True):
|
||||
return False
|
||||
|
||||
# Other rectangle is Up/Down from this
|
||||
if self.left == other.left and self.width == other.width:
|
||||
y_min = min(self.bottom, other.bottom)
|
||||
y_max = max(self.top, other.top)
|
||||
self.y = y_min
|
||||
self.height = y_max-y_min
|
||||
return True
|
||||
|
||||
# Other rectangle is Right/Left from this
|
||||
if self.bottom == other.bottom and self.height == other.height:
|
||||
x_min = min(self.left, other.left)
|
||||
x_max = max(self.right, other.right)
|
||||
self.x = x_min
|
||||
self.width = x_max-x_min
|
||||
return True
|
||||
|
||||
return False
|
||||
|
368
leenkx/blender/lnx/lightmapper/utility/rectpack/guillotine.py
Normal file
368
leenkx/blender/lnx/lightmapper/utility/rectpack/guillotine.py
Normal file
@ -0,0 +1,368 @@
|
||||
from .pack_algo import PackingAlgorithm
|
||||
from .geometry import Rectangle
|
||||
import itertools
|
||||
import operator
|
||||
|
||||
|
||||
class Guillotine(PackingAlgorithm):
|
||||
"""Implementation of several variants of Guillotine packing algorithm
|
||||
|
||||
For a more detailed explanation of the algorithm used, see:
|
||||
Jukka Jylanki - A Thousand Ways to Pack the Bin (February 27, 2010)
|
||||
"""
|
||||
def __init__(self, width, height, rot=True, merge=True, *args, **kwargs):
|
||||
"""
|
||||
Arguments:
|
||||
width (int, float):
|
||||
height (int, float):
|
||||
merge (bool): Optional keyword argument
|
||||
"""
|
||||
self._merge = merge
|
||||
super(Guillotine, self).__init__(width, height, rot, *args, **kwargs)
|
||||
|
||||
|
||||
def _add_section(self, section):
|
||||
"""Adds a new section to the free section list, but before that and if
|
||||
section merge is enabled, tries to join the rectangle with all existing
|
||||
sections, if successful the resulting section is again merged with the
|
||||
remaining sections until the operation fails. The result is then
|
||||
appended to the list.
|
||||
|
||||
Arguments:
|
||||
section (Rectangle): New free section.
|
||||
"""
|
||||
section.rid = 0
|
||||
plen = 0
|
||||
|
||||
while self._merge and self._sections and plen != len(self._sections):
|
||||
plen = len(self._sections)
|
||||
self._sections = [s for s in self._sections if not section.join(s)]
|
||||
self._sections.append(section)
|
||||
|
||||
|
||||
def _split_horizontal(self, section, width, height):
|
||||
"""For an horizontal split the rectangle is placed in the lower
|
||||
left corner of the section (section's xy coordinates), the top
|
||||
most side of the rectangle and its horizontal continuation,
|
||||
marks the line of division for the split.
|
||||
+-----------------+
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
+-------+---------+
|
||||
|#######| |
|
||||
|#######| |
|
||||
|#######| |
|
||||
+-------+---------+
|
||||
If the rectangle width is equal to the the section width, only one
|
||||
section is created over the rectangle. If the rectangle height is
|
||||
equal to the section height, only one section to the right of the
|
||||
rectangle is created. If both width and height are equal, no sections
|
||||
are created.
|
||||
"""
|
||||
# First remove the section we are splitting so it doesn't
|
||||
# interfere when later we try to merge the resulting split
|
||||
# rectangles, with the rest of free sections.
|
||||
#self._sections.remove(section)
|
||||
|
||||
# Creates two new empty sections, and returns the new rectangle.
|
||||
if height < section.height:
|
||||
self._add_section(Rectangle(section.x, section.y+height,
|
||||
section.width, section.height-height))
|
||||
|
||||
if width < section.width:
|
||||
self._add_section(Rectangle(section.x+width, section.y,
|
||||
section.width-width, height))
|
||||
|
||||
|
||||
def _split_vertical(self, section, width, height):
|
||||
"""For a vertical split the rectangle is placed in the lower
|
||||
left corner of the section (section's xy coordinates), the
|
||||
right most side of the rectangle and its vertical continuation,
|
||||
marks the line of division for the split.
|
||||
+-------+---------+
|
||||
| | |
|
||||
| | |
|
||||
| | |
|
||||
| | |
|
||||
+-------+ |
|
||||
|#######| |
|
||||
|#######| |
|
||||
|#######| |
|
||||
+-------+---------+
|
||||
If the rectangle width is equal to the the section width, only one
|
||||
section is created over the rectangle. If the rectangle height is
|
||||
equal to the section height, only one section to the right of the
|
||||
rectangle is created. If both width and height are equal, no sections
|
||||
are created.
|
||||
"""
|
||||
# When a section is split, depending on the rectangle size
|
||||
# two, one, or no new sections will be created.
|
||||
if height < section.height:
|
||||
self._add_section(Rectangle(section.x, section.y+height,
|
||||
width, section.height-height))
|
||||
|
||||
if width < section.width:
|
||||
self._add_section(Rectangle(section.x+width, section.y,
|
||||
section.width-width, section.height))
|
||||
|
||||
|
||||
def _split(self, section, width, height):
|
||||
"""
|
||||
Selects the best split for a section, given a rectangle of dimmensions
|
||||
width and height, then calls _split_vertical or _split_horizontal,
|
||||
to do the dirty work.
|
||||
|
||||
Arguments:
|
||||
section (Rectangle): Section to split
|
||||
width (int, float): Rectangle width
|
||||
height (int, float): Rectangle height
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def _section_fitness(self, section, width, height):
|
||||
"""The subclass for each one of the Guillotine selection methods,
|
||||
BAF, BLSF.... will override this method, this is here only
|
||||
to asure a valid value return if the worst happens.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def _select_fittest_section(self, w, h):
|
||||
"""Calls _section_fitness for each of the sections in free section
|
||||
list. Returns the section with the minimal fitness value, all the rest
|
||||
is boilerplate to make the fitness comparison, to rotatate the rectangles,
|
||||
and to take into account when _section_fitness returns None because
|
||||
the rectangle couldn't be placed.
|
||||
|
||||
Arguments:
|
||||
w (int, float): Rectangle width
|
||||
h (int, float): Rectangle height
|
||||
|
||||
Returns:
|
||||
(section, was_rotated): Returns the tuple
|
||||
section (Rectangle): Section with best fitness
|
||||
was_rotated (bool): The rectangle was rotated
|
||||
"""
|
||||
fitn = ((self._section_fitness(s, w, h), s, False) for s in self._sections
|
||||
if self._section_fitness(s, w, h) is not None)
|
||||
fitr = ((self._section_fitness(s, h, w), s, True) for s in self._sections
|
||||
if self._section_fitness(s, h, w) is not None)
|
||||
|
||||
if not self.rot:
|
||||
fitr = []
|
||||
|
||||
fit = itertools.chain(fitn, fitr)
|
||||
|
||||
try:
|
||||
_, sec, rot = min(fit, key=operator.itemgetter(0))
|
||||
except ValueError:
|
||||
return None, None
|
||||
|
||||
return sec, rot
|
||||
|
||||
|
||||
def add_rect(self, width, height, rid=None):
|
||||
"""
|
||||
Add rectangle of widthxheight dimensions.
|
||||
|
||||
Arguments:
|
||||
width (int, float): Rectangle width
|
||||
height (int, float): Rectangle height
|
||||
rid: Optional rectangle user id
|
||||
|
||||
Returns:
|
||||
Rectangle: Rectangle with placemente coordinates
|
||||
None: If the rectangle couldn be placed.
|
||||
"""
|
||||
assert(width > 0 and height >0)
|
||||
|
||||
# Obtain the best section to place the rectangle.
|
||||
section, rotated = self._select_fittest_section(width, height)
|
||||
if not section:
|
||||
return None
|
||||
|
||||
if rotated:
|
||||
width, height = height, width
|
||||
|
||||
# Remove section, split and store results
|
||||
self._sections.remove(section)
|
||||
self._split(section, width, height)
|
||||
|
||||
# Store rectangle in the selected position
|
||||
rect = Rectangle(section.x, section.y, width, height, rid)
|
||||
self.rectangles.append(rect)
|
||||
return rect
|
||||
|
||||
def fitness(self, width, height):
|
||||
"""
|
||||
In guillotine algorithm case, returns the min of the fitness of all
|
||||
free sections, for the given dimension, both normal and rotated
|
||||
(if rotation enabled.)
|
||||
"""
|
||||
assert(width > 0 and height > 0)
|
||||
|
||||
# Get best fitness section.
|
||||
section, rotated = self._select_fittest_section(width, height)
|
||||
if not section:
|
||||
return None
|
||||
|
||||
# Return fitness of returned section, with correct dimmensions if the
|
||||
# the rectangle was rotated.
|
||||
if rotated:
|
||||
return self._section_fitness(section, height, width)
|
||||
else:
|
||||
return self._section_fitness(section, width, height)
|
||||
|
||||
def reset(self):
|
||||
super(Guillotine, self).reset()
|
||||
self._sections = []
|
||||
self._add_section(Rectangle(0, 0, self.width, self.height))
|
||||
|
||||
|
||||
|
||||
class GuillotineBaf(Guillotine):
|
||||
"""Implements Best Area Fit (BAF) section selection criteria for
|
||||
Guillotine algorithm.
|
||||
"""
|
||||
def _section_fitness(self, section, width, height):
|
||||
if width > section.width or height > section.height:
|
||||
return None
|
||||
return section.area()-width*height
|
||||
|
||||
|
||||
class GuillotineBlsf(Guillotine):
|
||||
"""Implements Best Long Side Fit (BLSF) section selection criteria for
|
||||
Guillotine algorithm.
|
||||
"""
|
||||
def _section_fitness(self, section, width, height):
|
||||
if width > section.width or height > section.height:
|
||||
return None
|
||||
return max(section.width-width, section.height-height)
|
||||
|
||||
|
||||
class GuillotineBssf(Guillotine):
|
||||
"""Implements Best Short Side Fit (BSSF) section selection criteria for
|
||||
Guillotine algorithm.
|
||||
"""
|
||||
def _section_fitness(self, section, width, height):
|
||||
if width > section.width or height > section.height:
|
||||
return None
|
||||
return min(section.width-width, section.height-height)
|
||||
|
||||
|
||||
class GuillotineSas(Guillotine):
|
||||
"""Implements Short Axis Split (SAS) selection rule for Guillotine
|
||||
algorithm.
|
||||
"""
|
||||
def _split(self, section, width, height):
|
||||
if section.width < section.height:
|
||||
return self._split_horizontal(section, width, height)
|
||||
else:
|
||||
return self._split_vertical(section, width, height)
|
||||
|
||||
|
||||
|
||||
class GuillotineLas(Guillotine):
|
||||
"""Implements Long Axis Split (LAS) selection rule for Guillotine
|
||||
algorithm.
|
||||
"""
|
||||
def _split(self, section, width, height):
|
||||
if section.width >= section.height:
|
||||
return self._split_horizontal(section, width, height)
|
||||
else:
|
||||
return self._split_vertical(section, width, height)
|
||||
|
||||
|
||||
|
||||
class GuillotineSlas(Guillotine):
|
||||
"""Implements Short Leftover Axis Split (SLAS) selection rule for
|
||||
Guillotine algorithm.
|
||||
"""
|
||||
def _split(self, section, width, height):
|
||||
if section.width-width < section.height-height:
|
||||
return self._split_horizontal(section, width, height)
|
||||
else:
|
||||
return self._split_vertical(section, width, height)
|
||||
|
||||
|
||||
|
||||
class GuillotineLlas(Guillotine):
|
||||
"""Implements Long Leftover Axis Split (LLAS) selection rule for
|
||||
Guillotine algorithm.
|
||||
"""
|
||||
def _split(self, section, width, height):
|
||||
if section.width-width >= section.height-height:
|
||||
return self._split_horizontal(section, width, height)
|
||||
else:
|
||||
return self._split_vertical(section, width, height)
|
||||
|
||||
|
||||
|
||||
class GuillotineMaxas(Guillotine):
|
||||
"""Implements Max Area Axis Split (MAXAS) selection rule for Guillotine
|
||||
algorithm. Maximize the larger area == minimize the smaller area.
|
||||
Tries to make the rectangles more even-sized.
|
||||
"""
|
||||
def _split(self, section, width, height):
|
||||
if width*(section.height-height) <= height*(section.width-width):
|
||||
return self._split_horizontal(section, width, height)
|
||||
else:
|
||||
return self._split_vertical(section, width, height)
|
||||
|
||||
|
||||
|
||||
class GuillotineMinas(Guillotine):
|
||||
"""Implements Min Area Axis Split (MINAS) selection rule for Guillotine
|
||||
algorithm.
|
||||
"""
|
||||
def _split(self, section, width, height):
|
||||
if width*(section.height-height) >= height*(section.width-width):
|
||||
return self._split_horizontal(section, width, height)
|
||||
else:
|
||||
return self._split_vertical(section, width, height)
|
||||
|
||||
|
||||
|
||||
# Guillotine algorithms GUILLOTINE-RECT-SPLIT, Selecting one
|
||||
# Axis split, and one selection criteria.
|
||||
class GuillotineBssfSas(GuillotineBssf, GuillotineSas):
|
||||
pass
|
||||
class GuillotineBssfLas(GuillotineBssf, GuillotineLas):
|
||||
pass
|
||||
class GuillotineBssfSlas(GuillotineBssf, GuillotineSlas):
|
||||
pass
|
||||
class GuillotineBssfLlas(GuillotineBssf, GuillotineLlas):
|
||||
pass
|
||||
class GuillotineBssfMaxas(GuillotineBssf, GuillotineMaxas):
|
||||
pass
|
||||
class GuillotineBssfMinas(GuillotineBssf, GuillotineMinas):
|
||||
pass
|
||||
class GuillotineBlsfSas(GuillotineBlsf, GuillotineSas):
|
||||
pass
|
||||
class GuillotineBlsfLas(GuillotineBlsf, GuillotineLas):
|
||||
pass
|
||||
class GuillotineBlsfSlas(GuillotineBlsf, GuillotineSlas):
|
||||
pass
|
||||
class GuillotineBlsfLlas(GuillotineBlsf, GuillotineLlas):
|
||||
pass
|
||||
class GuillotineBlsfMaxas(GuillotineBlsf, GuillotineMaxas):
|
||||
pass
|
||||
class GuillotineBlsfMinas(GuillotineBlsf, GuillotineMinas):
|
||||
pass
|
||||
class GuillotineBafSas(GuillotineBaf, GuillotineSas):
|
||||
pass
|
||||
class GuillotineBafLas(GuillotineBaf, GuillotineLas):
|
||||
pass
|
||||
class GuillotineBafSlas(GuillotineBaf, GuillotineSlas):
|
||||
pass
|
||||
class GuillotineBafLlas(GuillotineBaf, GuillotineLlas):
|
||||
pass
|
||||
class GuillotineBafMaxas(GuillotineBaf, GuillotineMaxas):
|
||||
pass
|
||||
class GuillotineBafMinas(GuillotineBaf, GuillotineMinas):
|
||||
pass
|
||||
|
||||
|
||||
|
244
leenkx/blender/lnx/lightmapper/utility/rectpack/maxrects.py
Normal file
244
leenkx/blender/lnx/lightmapper/utility/rectpack/maxrects.py
Normal file
@ -0,0 +1,244 @@
|
||||
from .pack_algo import PackingAlgorithm
|
||||
from .geometry import Rectangle
|
||||
import itertools
|
||||
import collections
|
||||
import operator
|
||||
|
||||
|
||||
first_item = operator.itemgetter(0)
|
||||
|
||||
|
||||
|
||||
class MaxRects(PackingAlgorithm):
|
||||
|
||||
def __init__(self, width, height, rot=True, *args, **kwargs):
|
||||
super(MaxRects, self).__init__(width, height, rot, *args, **kwargs)
|
||||
|
||||
def _rect_fitness(self, max_rect, width, height):
|
||||
"""
|
||||
Arguments:
|
||||
max_rect (Rectangle): Destination max_rect
|
||||
width (int, float): Rectangle width
|
||||
height (int, float): Rectangle height
|
||||
|
||||
Returns:
|
||||
None: Rectangle couldn't be placed into max_rect
|
||||
integer, float: fitness value
|
||||
"""
|
||||
if width <= max_rect.width and height <= max_rect.height:
|
||||
return 0
|
||||
else:
|
||||
return None
|
||||
|
||||
def _select_position(self, w, h):
|
||||
"""
|
||||
Find max_rect with best fitness for placing a rectangle
|
||||
of dimentsions w*h
|
||||
|
||||
Arguments:
|
||||
w (int, float): Rectangle width
|
||||
h (int, float): Rectangle height
|
||||
|
||||
Returns:
|
||||
(rect, max_rect)
|
||||
rect (Rectangle): Placed rectangle or None if was unable.
|
||||
max_rect (Rectangle): Maximal rectangle were rect was placed
|
||||
"""
|
||||
if not self._max_rects:
|
||||
return None, None
|
||||
|
||||
# Normal rectangle
|
||||
fitn = ((self._rect_fitness(m, w, h), w, h, m) for m in self._max_rects
|
||||
if self._rect_fitness(m, w, h) is not None)
|
||||
|
||||
# Rotated rectangle
|
||||
fitr = ((self._rect_fitness(m, h, w), h, w, m) for m in self._max_rects
|
||||
if self._rect_fitness(m, h, w) is not None)
|
||||
|
||||
if not self.rot:
|
||||
fitr = []
|
||||
|
||||
fit = itertools.chain(fitn, fitr)
|
||||
|
||||
try:
|
||||
_, w, h, m = min(fit, key=first_item)
|
||||
except ValueError:
|
||||
return None, None
|
||||
|
||||
return Rectangle(m.x, m.y, w, h), m
|
||||
|
||||
def _generate_splits(self, m, r):
|
||||
"""
|
||||
When a rectangle is placed inside a maximal rectangle, it stops being one
|
||||
and up to 4 new maximal rectangles may appear depending on the placement.
|
||||
_generate_splits calculates them.
|
||||
|
||||
Arguments:
|
||||
m (Rectangle): max_rect rectangle
|
||||
r (Rectangle): rectangle placed
|
||||
|
||||
Returns:
|
||||
list : list containing new maximal rectangles or an empty list
|
||||
"""
|
||||
new_rects = []
|
||||
|
||||
if r.left > m.left:
|
||||
new_rects.append(Rectangle(m.left, m.bottom, r.left-m.left, m.height))
|
||||
if r.right < m.right:
|
||||
new_rects.append(Rectangle(r.right, m.bottom, m.right-r.right, m.height))
|
||||
if r.top < m.top:
|
||||
new_rects.append(Rectangle(m.left, r.top, m.width, m.top-r.top))
|
||||
if r.bottom > m.bottom:
|
||||
new_rects.append(Rectangle(m.left, m.bottom, m.width, r.bottom-m.bottom))
|
||||
|
||||
return new_rects
|
||||
|
||||
def _split(self, rect):
|
||||
"""
|
||||
Split all max_rects intersecting the rectangle rect into up to
|
||||
4 new max_rects.
|
||||
|
||||
Arguments:
|
||||
rect (Rectangle): Rectangle
|
||||
|
||||
Returns:
|
||||
split (Rectangle list): List of rectangles resulting from the split
|
||||
"""
|
||||
max_rects = collections.deque()
|
||||
|
||||
for r in self._max_rects:
|
||||
if r.intersects(rect):
|
||||
max_rects.extend(self._generate_splits(r, rect))
|
||||
else:
|
||||
max_rects.append(r)
|
||||
|
||||
# Add newly generated max_rects
|
||||
self._max_rects = list(max_rects)
|
||||
|
||||
def _remove_duplicates(self):
|
||||
"""
|
||||
Remove every maximal rectangle contained by another one.
|
||||
"""
|
||||
contained = set()
|
||||
for m1, m2 in itertools.combinations(self._max_rects, 2):
|
||||
if m1.contains(m2):
|
||||
contained.add(m2)
|
||||
elif m2.contains(m1):
|
||||
contained.add(m1)
|
||||
|
||||
# Remove from max_rects
|
||||
self._max_rects = [m for m in self._max_rects if m not in contained]
|
||||
|
||||
def fitness(self, width, height):
|
||||
"""
|
||||
Metric used to rate how much space is wasted if a rectangle is placed.
|
||||
Returns a value greater or equal to zero, the smaller the value the more
|
||||
'fit' is the rectangle. If the rectangle can't be placed, returns None.
|
||||
|
||||
Arguments:
|
||||
width (int, float): Rectangle width
|
||||
height (int, float): Rectangle height
|
||||
|
||||
Returns:
|
||||
int, float: Rectangle fitness
|
||||
None: Rectangle can't be placed
|
||||
"""
|
||||
assert(width > 0 and height > 0)
|
||||
|
||||
rect, max_rect = self._select_position(width, height)
|
||||
if rect is None:
|
||||
return None
|
||||
|
||||
# Return fitness
|
||||
return self._rect_fitness(max_rect, rect.width, rect.height)
|
||||
|
||||
def add_rect(self, width, height, rid=None):
|
||||
"""
|
||||
Add rectangle of widthxheight dimensions.
|
||||
|
||||
Arguments:
|
||||
width (int, float): Rectangle width
|
||||
height (int, float): Rectangle height
|
||||
rid: Optional rectangle user id
|
||||
|
||||
Returns:
|
||||
Rectangle: Rectangle with placemente coordinates
|
||||
None: If the rectangle couldn be placed.
|
||||
"""
|
||||
assert(width > 0 and height >0)
|
||||
|
||||
# Search best position and orientation
|
||||
rect, _ = self._select_position(width, height)
|
||||
if not rect:
|
||||
return None
|
||||
|
||||
# Subdivide all the max rectangles intersecting with the selected
|
||||
# rectangle.
|
||||
self._split(rect)
|
||||
|
||||
# Remove any max_rect contained by another
|
||||
self._remove_duplicates()
|
||||
|
||||
# Store and return rectangle position.
|
||||
rect.rid = rid
|
||||
self.rectangles.append(rect)
|
||||
return rect
|
||||
|
||||
def reset(self):
|
||||
super(MaxRects, self).reset()
|
||||
self._max_rects = [Rectangle(0, 0, self.width, self.height)]
|
||||
|
||||
|
||||
|
||||
|
||||
class MaxRectsBl(MaxRects):
|
||||
|
||||
def _select_position(self, w, h):
|
||||
"""
|
||||
Select the position where the y coordinate of the top of the rectangle
|
||||
is lower, if there are severtal pick the one with the smallest x
|
||||
coordinate
|
||||
"""
|
||||
fitn = ((m.y+h, m.x, w, h, m) for m in self._max_rects
|
||||
if self._rect_fitness(m, w, h) is not None)
|
||||
fitr = ((m.y+w, m.x, h, w, m) for m in self._max_rects
|
||||
if self._rect_fitness(m, h, w) is not None)
|
||||
|
||||
if not self.rot:
|
||||
fitr = []
|
||||
|
||||
fit = itertools.chain(fitn, fitr)
|
||||
|
||||
try:
|
||||
_, _, w, h, m = min(fit, key=first_item)
|
||||
except ValueError:
|
||||
return None, None
|
||||
|
||||
return Rectangle(m.x, m.y, w, h), m
|
||||
|
||||
|
||||
class MaxRectsBssf(MaxRects):
|
||||
"""Best Sort Side Fit minimize short leftover side"""
|
||||
def _rect_fitness(self, max_rect, width, height):
|
||||
if width > max_rect.width or height > max_rect.height:
|
||||
return None
|
||||
|
||||
return min(max_rect.width-width, max_rect.height-height)
|
||||
|
||||
class MaxRectsBaf(MaxRects):
|
||||
"""Best Area Fit pick maximal rectangle with smallest area
|
||||
where the rectangle can be placed"""
|
||||
def _rect_fitness(self, max_rect, width, height):
|
||||
if width > max_rect.width or height > max_rect.height:
|
||||
return None
|
||||
|
||||
return (max_rect.width*max_rect.height)-(width*height)
|
||||
|
||||
|
||||
class MaxRectsBlsf(MaxRects):
|
||||
"""Best Long Side Fit minimize long leftover side"""
|
||||
def _rect_fitness(self, max_rect, width, height):
|
||||
if width > max_rect.width or height > max_rect.height:
|
||||
return None
|
||||
|
||||
return max(max_rect.width-width, max_rect.height-height)
|
140
leenkx/blender/lnx/lightmapper/utility/rectpack/pack_algo.py
Normal file
140
leenkx/blender/lnx/lightmapper/utility/rectpack/pack_algo.py
Normal file
@ -0,0 +1,140 @@
|
||||
from .geometry import Rectangle
|
||||
|
||||
|
||||
class PackingAlgorithm(object):
|
||||
"""PackingAlgorithm base class"""
|
||||
|
||||
def __init__(self, width, height, rot=True, bid=None, *args, **kwargs):
|
||||
"""
|
||||
Initialize packing algorithm
|
||||
|
||||
Arguments:
|
||||
width (int, float): Packing surface width
|
||||
height (int, float): Packing surface height
|
||||
rot (bool): Rectangle rotation enabled or disabled
|
||||
bid (string|int|...): Packing surface identification
|
||||
"""
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.rot = rot
|
||||
self.rectangles = []
|
||||
self.bid = bid
|
||||
self._surface = Rectangle(0, 0, width, height)
|
||||
self.reset()
|
||||
|
||||
def __len__(self):
|
||||
return len(self.rectangles)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.rectangles)
|
||||
|
||||
def _fits_surface(self, width, height):
|
||||
"""
|
||||
Test surface is big enough to place a rectangle
|
||||
|
||||
Arguments:
|
||||
width (int, float): Rectangle width
|
||||
height (int, float): Rectangle height
|
||||
|
||||
Returns:
|
||||
boolean: True if it could be placed, False otherwise
|
||||
"""
|
||||
assert(width > 0 and height > 0)
|
||||
if self.rot and (width > self.width or height > self.height):
|
||||
width, height = height, width
|
||||
|
||||
if width > self.width or height > self.height:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""
|
||||
Return rectangle in selected position.
|
||||
"""
|
||||
return self.rectangles[key]
|
||||
|
||||
def used_area(self):
|
||||
"""
|
||||
Total area of rectangles placed
|
||||
|
||||
Returns:
|
||||
int, float: Area
|
||||
"""
|
||||
return sum(r.area() for r in self)
|
||||
|
||||
def fitness(self, width, height, rot = False):
|
||||
"""
|
||||
Metric used to rate how much space is wasted if a rectangle is placed.
|
||||
Returns a value greater or equal to zero, the smaller the value the more
|
||||
'fit' is the rectangle. If the rectangle can't be placed, returns None.
|
||||
|
||||
Arguments:
|
||||
width (int, float): Rectangle width
|
||||
height (int, float): Rectangle height
|
||||
rot (bool): Enable rectangle rotation
|
||||
|
||||
Returns:
|
||||
int, float: Rectangle fitness
|
||||
None: Rectangle can't be placed
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def add_rect(self, width, height, rid=None):
|
||||
"""
|
||||
Add rectangle of widthxheight dimensions.
|
||||
|
||||
Arguments:
|
||||
width (int, float): Rectangle width
|
||||
height (int, float): Rectangle height
|
||||
rid: Optional rectangle user id
|
||||
|
||||
Returns:
|
||||
Rectangle: Rectangle with placemente coordinates
|
||||
None: If the rectangle couldn be placed.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def rect_list(self):
|
||||
"""
|
||||
Returns a list with all rectangles placed into the surface.
|
||||
|
||||
Returns:
|
||||
List: Format [(x, y, width, height, rid), ...]
|
||||
"""
|
||||
rectangle_list = []
|
||||
for r in self:
|
||||
rectangle_list.append((r.x, r.y, r.width, r.height, r.rid))
|
||||
|
||||
return rectangle_list
|
||||
|
||||
def validate_packing(self):
|
||||
"""
|
||||
Check for collisions between rectangles, also check all are placed
|
||||
inside surface.
|
||||
"""
|
||||
surface = Rectangle(0, 0, self.width, self.height)
|
||||
|
||||
for r in self:
|
||||
if not surface.contains(r):
|
||||
raise Exception("Rectangle placed outside surface")
|
||||
|
||||
|
||||
rectangles = [r for r in self]
|
||||
if len(rectangles) <= 1:
|
||||
return
|
||||
|
||||
for r1 in range(0, len(rectangles)-2):
|
||||
for r2 in range(r1+1, len(rectangles)-1):
|
||||
if rectangles[r1].intersects(rectangles[r2]):
|
||||
raise Exception("Rectangle collision detected")
|
||||
|
||||
def is_empty(self):
|
||||
# Returns true if there is no rectangles placed.
|
||||
return not bool(len(self))
|
||||
|
||||
def reset(self):
|
||||
self.rectangles = [] # List of placed Rectangles.
|
||||
|
||||
|
||||
|
580
leenkx/blender/lnx/lightmapper/utility/rectpack/packer.py
Normal file
580
leenkx/blender/lnx/lightmapper/utility/rectpack/packer.py
Normal file
@ -0,0 +1,580 @@
|
||||
from .maxrects import MaxRectsBssf
|
||||
|
||||
import operator
|
||||
import itertools
|
||||
import collections
|
||||
|
||||
import decimal
|
||||
|
||||
# Float to Decimal helper
|
||||
def float2dec(ft, decimal_digits):
|
||||
"""
|
||||
Convert float (or int) to Decimal (rounding up) with the
|
||||
requested number of decimal digits.
|
||||
|
||||
Arguments:
|
||||
ft (float, int): Number to convert
|
||||
decimal (int): Number of digits after decimal point
|
||||
|
||||
Return:
|
||||
Decimal: Number converted to decima
|
||||
"""
|
||||
with decimal.localcontext() as ctx:
|
||||
ctx.rounding = decimal.ROUND_UP
|
||||
places = decimal.Decimal(10)**(-decimal_digits)
|
||||
return decimal.Decimal.from_float(float(ft)).quantize(places)
|
||||
|
||||
|
||||
# Sorting algos for rectangle lists
|
||||
SORT_AREA = lambda rectlist: sorted(rectlist, reverse=True,
|
||||
key=lambda r: r[0]*r[1]) # Sort by area
|
||||
|
||||
SORT_PERI = lambda rectlist: sorted(rectlist, reverse=True,
|
||||
key=lambda r: r[0]+r[1]) # Sort by perimeter
|
||||
|
||||
SORT_DIFF = lambda rectlist: sorted(rectlist, reverse=True,
|
||||
key=lambda r: abs(r[0]-r[1])) # Sort by Diff
|
||||
|
||||
SORT_SSIDE = lambda rectlist: sorted(rectlist, reverse=True,
|
||||
key=lambda r: (min(r[0], r[1]), max(r[0], r[1]))) # Sort by short side
|
||||
|
||||
SORT_LSIDE = lambda rectlist: sorted(rectlist, reverse=True,
|
||||
key=lambda r: (max(r[0], r[1]), min(r[0], r[1]))) # Sort by long side
|
||||
|
||||
SORT_RATIO = lambda rectlist: sorted(rectlist, reverse=True,
|
||||
key=lambda r: r[0]/r[1]) # Sort by side ratio
|
||||
|
||||
SORT_NONE = lambda rectlist: list(rectlist) # Unsorted
|
||||
|
||||
|
||||
|
||||
class BinFactory(object):
|
||||
|
||||
def __init__(self, width, height, count, pack_algo, *args, **kwargs):
|
||||
self._width = width
|
||||
self._height = height
|
||||
self._count = count
|
||||
|
||||
self._pack_algo = pack_algo
|
||||
self._algo_kwargs = kwargs
|
||||
self._algo_args = args
|
||||
self._ref_bin = None # Reference bin used to calculate fitness
|
||||
|
||||
self._bid = kwargs.get("bid", None)
|
||||
|
||||
def _create_bin(self):
|
||||
return self._pack_algo(self._width, self._height, *self._algo_args, **self._algo_kwargs)
|
||||
|
||||
def is_empty(self):
|
||||
return self._count<1
|
||||
|
||||
def fitness(self, width, height):
|
||||
if not self._ref_bin:
|
||||
self._ref_bin = self._create_bin()
|
||||
|
||||
return self._ref_bin.fitness(width, height)
|
||||
|
||||
def fits_inside(self, width, height):
|
||||
# Determine if rectangle widthxheight will fit into empty bin
|
||||
if not self._ref_bin:
|
||||
self._ref_bin = self._create_bin()
|
||||
|
||||
return self._ref_bin._fits_surface(width, height)
|
||||
|
||||
def new_bin(self):
|
||||
if self._count > 0:
|
||||
self._count -= 1
|
||||
return self._create_bin()
|
||||
else:
|
||||
return None
|
||||
|
||||
def __eq__(self, other):
|
||||
return self._width*self._height == other._width*other._height
|
||||
|
||||
def __lt__(self, other):
|
||||
return self._width*self._height < other._width*other._height
|
||||
|
||||
def __str__(self):
|
||||
return "Bin: {} {} {}".format(self._width, self._height, self._count)
|
||||
|
||||
|
||||
|
||||
class PackerBNFMixin(object):
|
||||
"""
|
||||
BNF (Bin Next Fit): Only one open bin at a time. If the rectangle
|
||||
doesn't fit, close the current bin and go to the next.
|
||||
"""
|
||||
|
||||
def add_rect(self, width, height, rid=None):
|
||||
while True:
|
||||
# if there are no open bins, try to open a new one
|
||||
if len(self._open_bins)==0:
|
||||
# can we find an unopened bin that will hold this rect?
|
||||
new_bin = self._new_open_bin(width, height, rid=rid)
|
||||
if new_bin is None:
|
||||
return None
|
||||
|
||||
# we have at least one open bin, so check if it can hold this rect
|
||||
rect = self._open_bins[0].add_rect(width, height, rid=rid)
|
||||
if rect is not None:
|
||||
return rect
|
||||
|
||||
# since the rect doesn't fit, close this bin and try again
|
||||
closed_bin = self._open_bins.popleft()
|
||||
self._closed_bins.append(closed_bin)
|
||||
|
||||
|
||||
class PackerBFFMixin(object):
|
||||
"""
|
||||
BFF (Bin First Fit): Pack rectangle in first bin it fits
|
||||
"""
|
||||
|
||||
def add_rect(self, width, height, rid=None):
|
||||
# see if this rect will fit in any of the open bins
|
||||
for b in self._open_bins:
|
||||
rect = b.add_rect(width, height, rid=rid)
|
||||
if rect is not None:
|
||||
return rect
|
||||
|
||||
while True:
|
||||
# can we find an unopened bin that will hold this rect?
|
||||
new_bin = self._new_open_bin(width, height, rid=rid)
|
||||
if new_bin is None:
|
||||
return None
|
||||
|
||||
# _new_open_bin may return a bin that's too small,
|
||||
# so we have to double-check
|
||||
rect = new_bin.add_rect(width, height, rid=rid)
|
||||
if rect is not None:
|
||||
return rect
|
||||
|
||||
|
||||
class PackerBBFMixin(object):
|
||||
"""
|
||||
BBF (Bin Best Fit): Pack rectangle in bin that gives best fitness
|
||||
"""
|
||||
|
||||
# only create this getter once
|
||||
first_item = operator.itemgetter(0)
|
||||
|
||||
def add_rect(self, width, height, rid=None):
|
||||
|
||||
# Try packing into open bins
|
||||
fit = ((b.fitness(width, height), b) for b in self._open_bins)
|
||||
fit = (b for b in fit if b[0] is not None)
|
||||
try:
|
||||
_, best_bin = min(fit, key=self.first_item)
|
||||
best_bin.add_rect(width, height, rid)
|
||||
return True
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Try packing into one of the empty bins
|
||||
while True:
|
||||
# can we find an unopened bin that will hold this rect?
|
||||
new_bin = self._new_open_bin(width, height, rid=rid)
|
||||
if new_bin is None:
|
||||
return False
|
||||
|
||||
# _new_open_bin may return a bin that's too small,
|
||||
# so we have to double-check
|
||||
if new_bin.add_rect(width, height, rid):
|
||||
return True
|
||||
|
||||
|
||||
|
||||
class PackerOnline(object):
|
||||
"""
|
||||
Rectangles are packed as soon are they are added
|
||||
"""
|
||||
|
||||
def __init__(self, pack_algo=MaxRectsBssf, rotation=True):
|
||||
"""
|
||||
Arguments:
|
||||
pack_algo (PackingAlgorithm): What packing algo to use
|
||||
rotation (bool): Enable/Disable rectangle rotation
|
||||
"""
|
||||
self._rotation = rotation
|
||||
self._pack_algo = pack_algo
|
||||
self.reset()
|
||||
|
||||
def __iter__(self):
|
||||
return itertools.chain(self._closed_bins, self._open_bins)
|
||||
|
||||
def __len__(self):
|
||||
return len(self._closed_bins)+len(self._open_bins)
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""
|
||||
Return bin in selected position. (excluding empty bins)
|
||||
"""
|
||||
if not isinstance(key, int):
|
||||
raise TypeError("Indices must be integers")
|
||||
|
||||
size = len(self) # avoid recalulations
|
||||
|
||||
if key < 0:
|
||||
key += size
|
||||
|
||||
if not 0 <= key < size:
|
||||
raise IndexError("Index out of range")
|
||||
|
||||
if key < len(self._closed_bins):
|
||||
return self._closed_bins[key]
|
||||
else:
|
||||
return self._open_bins[key-len(self._closed_bins)]
|
||||
|
||||
def _new_open_bin(self, width=None, height=None, rid=None):
|
||||
"""
|
||||
Extract the next empty bin and append it to open bins
|
||||
|
||||
Returns:
|
||||
PackingAlgorithm: Initialized empty packing bin.
|
||||
None: No bin big enough for the rectangle was found
|
||||
"""
|
||||
factories_to_delete = set() #
|
||||
new_bin = None
|
||||
|
||||
for key, binfac in self._empty_bins.items():
|
||||
|
||||
# Only return the new bin if the rect fits.
|
||||
# (If width or height is None, caller doesn't know the size.)
|
||||
if not binfac.fits_inside(width, height):
|
||||
continue
|
||||
|
||||
# Create bin and add to open_bins
|
||||
new_bin = binfac.new_bin()
|
||||
if new_bin is None:
|
||||
continue
|
||||
self._open_bins.append(new_bin)
|
||||
|
||||
# If the factory was depleted mark for deletion
|
||||
if binfac.is_empty():
|
||||
factories_to_delete.add(key)
|
||||
|
||||
break
|
||||
|
||||
# Delete marked factories
|
||||
for f in factories_to_delete:
|
||||
del self._empty_bins[f]
|
||||
|
||||
return new_bin
|
||||
|
||||
def add_bin(self, width, height, count=1, **kwargs):
|
||||
# accept the same parameters as PackingAlgorithm objects
|
||||
kwargs['rot'] = self._rotation
|
||||
bin_factory = BinFactory(width, height, count, self._pack_algo, **kwargs)
|
||||
self._empty_bins[next(self._bin_count)] = bin_factory
|
||||
|
||||
def rect_list(self):
|
||||
rectangles = []
|
||||
bin_count = 0
|
||||
|
||||
for abin in self:
|
||||
for rect in abin:
|
||||
rectangles.append((bin_count, rect.x, rect.y, rect.width, rect.height, rect.rid))
|
||||
bin_count += 1
|
||||
|
||||
return rectangles
|
||||
|
||||
def bin_list(self):
|
||||
"""
|
||||
Return a list of the dimmensions of the bins in use, that is closed
|
||||
or open containing at least one rectangle
|
||||
"""
|
||||
return [(b.width, b.height) for b in self]
|
||||
|
||||
def validate_packing(self):
|
||||
for b in self:
|
||||
b.validate_packing()
|
||||
|
||||
def reset(self):
|
||||
# Bins fully packed and closed.
|
||||
self._closed_bins = collections.deque()
|
||||
|
||||
# Bins ready to pack rectangles
|
||||
self._open_bins = collections.deque()
|
||||
|
||||
# User provided bins not in current use
|
||||
self._empty_bins = collections.OrderedDict() # O(1) deletion of arbitrary elem
|
||||
self._bin_count = itertools.count()
|
||||
|
||||
|
||||
class Packer(PackerOnline):
|
||||
"""
|
||||
Rectangles aren't packed untils pack() is called
|
||||
"""
|
||||
|
||||
def __init__(self, pack_algo=MaxRectsBssf, sort_algo=SORT_NONE,
|
||||
rotation=True):
|
||||
"""
|
||||
"""
|
||||
super(Packer, self).__init__(pack_algo=pack_algo, rotation=rotation)
|
||||
|
||||
self._sort_algo = sort_algo
|
||||
|
||||
# User provided bins and Rectangles
|
||||
self._avail_bins = collections.deque()
|
||||
self._avail_rect = collections.deque()
|
||||
|
||||
# Aux vars used during packing
|
||||
self._sorted_rect = []
|
||||
|
||||
def add_bin(self, width, height, count=1, **kwargs):
|
||||
self._avail_bins.append((width, height, count, kwargs))
|
||||
|
||||
def add_rect(self, width, height, rid=None):
|
||||
self._avail_rect.append((width, height, rid))
|
||||
|
||||
def _is_everything_ready(self):
|
||||
return self._avail_rect and self._avail_bins
|
||||
|
||||
def pack(self):
|
||||
|
||||
self.reset()
|
||||
|
||||
if not self._is_everything_ready():
|
||||
# maybe we should throw an error here?
|
||||
return
|
||||
|
||||
# Add available bins to packer
|
||||
for b in self._avail_bins:
|
||||
width, height, count, extra_kwargs = b
|
||||
super(Packer, self).add_bin(width, height, count, **extra_kwargs)
|
||||
|
||||
# If enabled sort rectangles
|
||||
self._sorted_rect = self._sort_algo(self._avail_rect)
|
||||
|
||||
# Start packing
|
||||
for r in self._sorted_rect:
|
||||
super(Packer, self).add_rect(*r)
|
||||
|
||||
|
||||
|
||||
class PackerBNF(Packer, PackerBNFMixin):
|
||||
"""
|
||||
BNF (Bin Next Fit): Only one open bin, if rectangle doesn't fit
|
||||
go to next bin and close current one.
|
||||
"""
|
||||
pass
|
||||
|
||||
class PackerBFF(Packer, PackerBFFMixin):
|
||||
"""
|
||||
BFF (Bin First Fit): Pack rectangle in first bin it fits
|
||||
"""
|
||||
pass
|
||||
|
||||
class PackerBBF(Packer, PackerBBFMixin):
|
||||
"""
|
||||
BBF (Bin Best Fit): Pack rectangle in bin that gives best fitness
|
||||
"""
|
||||
pass
|
||||
|
||||
class PackerOnlineBNF(PackerOnline, PackerBNFMixin):
|
||||
"""
|
||||
BNF Bin Next Fit Online variant
|
||||
"""
|
||||
pass
|
||||
|
||||
class PackerOnlineBFF(PackerOnline, PackerBFFMixin):
|
||||
"""
|
||||
BFF Bin First Fit Online variant
|
||||
"""
|
||||
pass
|
||||
|
||||
class PackerOnlineBBF(PackerOnline, PackerBBFMixin):
|
||||
"""
|
||||
BBF Bin Best Fit Online variant
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class PackerGlobal(Packer, PackerBNFMixin):
|
||||
"""
|
||||
GLOBAL: For each bin pack the rectangle with the best fitness.
|
||||
"""
|
||||
first_item = operator.itemgetter(0)
|
||||
|
||||
def __init__(self, pack_algo=MaxRectsBssf, rotation=True):
|
||||
"""
|
||||
"""
|
||||
super(PackerGlobal, self).__init__(pack_algo=pack_algo,
|
||||
sort_algo=SORT_NONE, rotation=rotation)
|
||||
|
||||
def _find_best_fit(self, pbin):
|
||||
"""
|
||||
Return best fitness rectangle from rectangles packing _sorted_rect list
|
||||
|
||||
Arguments:
|
||||
pbin (PackingAlgorithm): Packing bin
|
||||
|
||||
Returns:
|
||||
key of the rectangle with best fitness
|
||||
"""
|
||||
fit = ((pbin.fitness(r[0], r[1]), k) for k, r in self._sorted_rect.items())
|
||||
fit = (f for f in fit if f[0] is not None)
|
||||
try:
|
||||
_, rect = min(fit, key=self.first_item)
|
||||
return rect
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def _new_open_bin(self, remaining_rect):
|
||||
"""
|
||||
Extract the next bin where at least one of the rectangles in
|
||||
rem
|
||||
|
||||
Arguments:
|
||||
remaining_rect (dict): rectangles not placed yet
|
||||
|
||||
Returns:
|
||||
PackingAlgorithm: Initialized empty packing bin.
|
||||
None: No bin big enough for the rectangle was found
|
||||
"""
|
||||
factories_to_delete = set() #
|
||||
new_bin = None
|
||||
|
||||
for key, binfac in self._empty_bins.items():
|
||||
|
||||
# Only return the new bin if at least one of the remaining
|
||||
# rectangles fit inside.
|
||||
a_rectangle_fits = False
|
||||
for _, rect in remaining_rect.items():
|
||||
if binfac.fits_inside(rect[0], rect[1]):
|
||||
a_rectangle_fits = True
|
||||
break
|
||||
|
||||
if not a_rectangle_fits:
|
||||
factories_to_delete.add(key)
|
||||
continue
|
||||
|
||||
# Create bin and add to open_bins
|
||||
new_bin = binfac.new_bin()
|
||||
if new_bin is None:
|
||||
continue
|
||||
self._open_bins.append(new_bin)
|
||||
|
||||
# If the factory was depleted mark for deletion
|
||||
if binfac.is_empty():
|
||||
factories_to_delete.add(key)
|
||||
|
||||
break
|
||||
|
||||
# Delete marked factories
|
||||
for f in factories_to_delete:
|
||||
del self._empty_bins[f]
|
||||
|
||||
return new_bin
|
||||
|
||||
def pack(self):
|
||||
|
||||
self.reset()
|
||||
|
||||
if not self._is_everything_ready():
|
||||
return
|
||||
|
||||
# Add available bins to packer
|
||||
for b in self._avail_bins:
|
||||
width, height, count, extra_kwargs = b
|
||||
super(Packer, self).add_bin(width, height, count, **extra_kwargs)
|
||||
|
||||
# Store rectangles into dict for fast deletion
|
||||
self._sorted_rect = collections.OrderedDict(
|
||||
enumerate(self._sort_algo(self._avail_rect)))
|
||||
|
||||
# For each bin pack the rectangles with lowest fitness until it is filled or
|
||||
# the rectangles exhausted, then open the next bin where at least one rectangle
|
||||
# will fit and repeat the process until there aren't more rectangles or bins
|
||||
# available.
|
||||
while len(self._sorted_rect) > 0:
|
||||
|
||||
# Find one bin where at least one of the remaining rectangles fit
|
||||
pbin = self._new_open_bin(self._sorted_rect)
|
||||
if pbin is None:
|
||||
break
|
||||
|
||||
# Pack as many rectangles as possible into the open bin
|
||||
while True:
|
||||
|
||||
# Find 'fittest' rectangle
|
||||
best_rect_key = self._find_best_fit(pbin)
|
||||
if best_rect_key is None:
|
||||
closed_bin = self._open_bins.popleft()
|
||||
self._closed_bins.append(closed_bin)
|
||||
break # None of the remaining rectangles can be packed in this bin
|
||||
|
||||
best_rect = self._sorted_rect[best_rect_key]
|
||||
del self._sorted_rect[best_rect_key]
|
||||
|
||||
PackerBNFMixin.add_rect(self, *best_rect)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Packer factory
|
||||
class Enum(tuple):
|
||||
__getattr__ = tuple.index
|
||||
|
||||
PackingMode = Enum(["Online", "Offline"])
|
||||
PackingBin = Enum(["BNF", "BFF", "BBF", "Global"])
|
||||
|
||||
|
||||
def newPacker(mode=PackingMode.Offline,
|
||||
bin_algo=PackingBin.BBF,
|
||||
pack_algo=MaxRectsBssf,
|
||||
sort_algo=SORT_AREA,
|
||||
rotation=True):
|
||||
"""
|
||||
Packer factory helper function
|
||||
|
||||
Arguments:
|
||||
mode (PackingMode): Packing mode
|
||||
Online: Rectangles are packed as soon are they are added
|
||||
Offline: Rectangles aren't packed untils pack() is called
|
||||
bin_algo (PackingBin): Bin selection heuristic
|
||||
pack_algo (PackingAlgorithm): Algorithm used
|
||||
rotation (boolean): Enable or disable rectangle rotation.
|
||||
|
||||
Returns:
|
||||
Packer: Initialized packer instance.
|
||||
"""
|
||||
packer_class = None
|
||||
|
||||
# Online Mode
|
||||
if mode == PackingMode.Online:
|
||||
sort_algo=None
|
||||
if bin_algo == PackingBin.BNF:
|
||||
packer_class = PackerOnlineBNF
|
||||
elif bin_algo == PackingBin.BFF:
|
||||
packer_class = PackerOnlineBFF
|
||||
elif bin_algo == PackingBin.BBF:
|
||||
packer_class = PackerOnlineBBF
|
||||
else:
|
||||
raise AttributeError("Unsupported bin selection heuristic")
|
||||
|
||||
# Offline Mode
|
||||
elif mode == PackingMode.Offline:
|
||||
if bin_algo == PackingBin.BNF:
|
||||
packer_class = PackerBNF
|
||||
elif bin_algo == PackingBin.BFF:
|
||||
packer_class = PackerBFF
|
||||
elif bin_algo == PackingBin.BBF:
|
||||
packer_class = PackerBBF
|
||||
elif bin_algo == PackingBin.Global:
|
||||
packer_class = PackerGlobal
|
||||
sort_algo=None
|
||||
else:
|
||||
raise AttributeError("Unsupported bin selection heuristic")
|
||||
|
||||
else:
|
||||
raise AttributeError("Unknown packing mode.")
|
||||
|
||||
if sort_algo:
|
||||
return packer_class(pack_algo=pack_algo, sort_algo=sort_algo,
|
||||
rotation=rotation)
|
||||
else:
|
||||
return packer_class(pack_algo=pack_algo, rotation=rotation)
|
||||
|
||||
|
303
leenkx/blender/lnx/lightmapper/utility/rectpack/skyline.py
Normal file
303
leenkx/blender/lnx/lightmapper/utility/rectpack/skyline.py
Normal file
@ -0,0 +1,303 @@
|
||||
import collections
|
||||
import itertools
|
||||
import operator
|
||||
import heapq
|
||||
import copy
|
||||
from .pack_algo import PackingAlgorithm
|
||||
from .geometry import Point as P
|
||||
from .geometry import HSegment, Rectangle
|
||||
from .waste import WasteManager
|
||||
|
||||
|
||||
class Skyline(PackingAlgorithm):
|
||||
""" Class implementing Skyline algorithm as described by
|
||||
Jukka Jylanki - A Thousand Ways to Pack the Bin (February 27, 2010)
|
||||
|
||||
_skyline: stores all the segments at the top of the skyline.
|
||||
_waste: Handles all wasted sections.
|
||||
"""
|
||||
|
||||
def __init__(self, width, height, rot=True, *args, **kwargs):
|
||||
"""
|
||||
_skyline is the list used to store all the skyline segments, each
|
||||
one is a list with the format [x, y, width] where x is the x
|
||||
coordinate of the left most point of the segment, y the y coordinate
|
||||
of the segment, and width the length of the segment. The initial
|
||||
segment is allways [0, 0, surface_width]
|
||||
|
||||
Arguments:
|
||||
width (int, float):
|
||||
height (int, float):
|
||||
rot (bool): Enable or disable rectangle rotation
|
||||
"""
|
||||
self._waste_management = False
|
||||
self._waste = WasteManager(rot=rot)
|
||||
super(Skyline, self).__init__(width, height, rot, merge=False, *args, **kwargs)
|
||||
|
||||
def _placement_points_generator(self, skyline, width):
|
||||
"""Returns a generator for the x coordinates of all the placement
|
||||
points on the skyline for a given rectangle.
|
||||
|
||||
WARNING: In some cases could be duplicated points, but it is faster
|
||||
to compute them twice than to remove them.
|
||||
|
||||
Arguments:
|
||||
skyline (list): Skyline HSegment list
|
||||
width (int, float): Rectangle width
|
||||
|
||||
Returns:
|
||||
generator
|
||||
"""
|
||||
skyline_r = skyline[-1].right
|
||||
skyline_l = skyline[0].left
|
||||
|
||||
# Placements using skyline segment left point
|
||||
ppointsl = (s.left for s in skyline if s.left+width <= skyline_r)
|
||||
|
||||
# Placements using skyline segment right point
|
||||
ppointsr = (s.right-width for s in skyline if s.right-width >= skyline_l)
|
||||
|
||||
# Merge positions
|
||||
return heapq.merge(ppointsl, ppointsr)
|
||||
|
||||
def _generate_placements(self, width, height):
|
||||
"""
|
||||
Generate a list with
|
||||
|
||||
Arguments:
|
||||
skyline (list): SkylineHSegment list
|
||||
width (number):
|
||||
|
||||
Returns:
|
||||
tuple (Rectangle, fitness):
|
||||
Rectangle: Rectangle in valid position
|
||||
left_skyline: Index for the skyline under the rectangle left edge.
|
||||
right_skyline: Index for the skyline under the rectangle right edte.
|
||||
"""
|
||||
skyline = self._skyline
|
||||
|
||||
points = collections.deque()
|
||||
|
||||
left_index = right_index = 0 # Left and right side skyline index
|
||||
support_height = skyline[0].top
|
||||
support_index = 0
|
||||
|
||||
placements = self._placement_points_generator(skyline, width)
|
||||
for p in placements:
|
||||
|
||||
# If Rectangle's right side changed segment, find new support
|
||||
if p+width > skyline[right_index].right:
|
||||
for right_index in range(right_index+1, len(skyline)):
|
||||
if skyline[right_index].top >= support_height:
|
||||
support_index = right_index
|
||||
support_height = skyline[right_index].top
|
||||
if p+width <= skyline[right_index].right:
|
||||
break
|
||||
|
||||
# If left side changed segment.
|
||||
if p >= skyline[left_index].right:
|
||||
left_index +=1
|
||||
|
||||
# Find new support if the previous one was shifted out.
|
||||
if support_index < left_index:
|
||||
support_index = left_index
|
||||
support_height = skyline[left_index].top
|
||||
for i in range(left_index, right_index+1):
|
||||
if skyline[i].top >= support_height:
|
||||
support_index = i
|
||||
support_height = skyline[i].top
|
||||
|
||||
# Add point if there is enought room at the top
|
||||
if support_height+height <= self.height:
|
||||
points.append((Rectangle(p, support_height, width, height),\
|
||||
left_index, right_index))
|
||||
|
||||
return points
|
||||
|
||||
def _merge_skyline(self, skylineq, segment):
|
||||
"""
|
||||
Arguments:
|
||||
skylineq (collections.deque):
|
||||
segment (HSegment):
|
||||
"""
|
||||
if len(skylineq) == 0:
|
||||
skylineq.append(segment)
|
||||
return
|
||||
|
||||
if skylineq[-1].top == segment.top:
|
||||
s = skylineq[-1]
|
||||
skylineq[-1] = HSegment(s.start, s.length+segment.length)
|
||||
else:
|
||||
skylineq.append(segment)
|
||||
|
||||
def _add_skyline(self, rect):
|
||||
"""
|
||||
Arguments:
|
||||
seg (Rectangle):
|
||||
"""
|
||||
skylineq = collections.deque([]) # Skyline after adding new one
|
||||
|
||||
for sky in self._skyline:
|
||||
if sky.right <= rect.left or sky.left >= rect.right:
|
||||
self._merge_skyline(skylineq, sky)
|
||||
continue
|
||||
|
||||
if sky.left < rect.left and sky.right > rect.left:
|
||||
# Skyline section partially under segment left
|
||||
self._merge_skyline(skylineq,
|
||||
HSegment(sky.start, rect.left-sky.left))
|
||||
sky = HSegment(P(rect.left, sky.top), sky.right-rect.left)
|
||||
|
||||
if sky.left < rect.right:
|
||||
if sky.left == rect.left:
|
||||
self._merge_skyline(skylineq,
|
||||
HSegment(P(rect.left, rect.top), rect.width))
|
||||
# Skyline section partially under segment right
|
||||
if sky.right > rect.right:
|
||||
self._merge_skyline(skylineq,
|
||||
HSegment(P(rect.right, sky.top), sky.right-rect.right))
|
||||
sky = HSegment(sky.start, rect.right-sky.left)
|
||||
|
||||
if sky.left >= rect.left and sky.right <= rect.right:
|
||||
# Skyline section fully under segment, account for wasted space
|
||||
if self._waste_management and sky.top < rect.bottom:
|
||||
self._waste.add_waste(sky.left, sky.top,
|
||||
sky.length, rect.bottom - sky.top)
|
||||
else:
|
||||
# Segment
|
||||
self._merge_skyline(skylineq, sky)
|
||||
|
||||
# Aaaaand ..... Done
|
||||
self._skyline = list(skylineq)
|
||||
|
||||
def _rect_fitness(self, rect, left_index, right_index):
|
||||
return rect.top
|
||||
|
||||
def _select_position(self, width, height):
|
||||
"""
|
||||
Search for the placement with the bes fitness for the rectangle.
|
||||
|
||||
Returns:
|
||||
tuple (Rectangle, fitness) - Rectangle placed in the fittest position
|
||||
None - Rectangle couldn't be placed
|
||||
"""
|
||||
positions = self._generate_placements(width, height)
|
||||
if self.rot and width != height:
|
||||
positions += self._generate_placements(height, width)
|
||||
if not positions:
|
||||
return None, None
|
||||
return min(((p[0], self._rect_fitness(*p))for p in positions),
|
||||
key=operator.itemgetter(1))
|
||||
|
||||
def fitness(self, width, height):
|
||||
"""Search for the best fitness
|
||||
"""
|
||||
assert(width > 0 and height >0)
|
||||
if width > max(self.width, self.height) or\
|
||||
height > max(self.height, self.width):
|
||||
return None
|
||||
|
||||
# If there is room in wasted space, FREE PACKING!!
|
||||
if self._waste_management:
|
||||
if self._waste.fitness(width, height) is not None:
|
||||
return 0
|
||||
|
||||
# Get best fitness segment, for normal rectangle, and for
|
||||
# rotated rectangle if rotation is enabled.
|
||||
rect, fitness = self._select_position(width, height)
|
||||
return fitness
|
||||
|
||||
def add_rect(self, width, height, rid=None):
|
||||
"""
|
||||
Add new rectangle
|
||||
"""
|
||||
assert(width > 0 and height > 0)
|
||||
if width > max(self.width, self.height) or\
|
||||
height > max(self.height, self.width):
|
||||
return None
|
||||
|
||||
rect = None
|
||||
# If Waste managment is enabled, first try to place the rectangle there
|
||||
if self._waste_management:
|
||||
rect = self._waste.add_rect(width, height, rid)
|
||||
|
||||
# Get best possible rectangle position
|
||||
if not rect:
|
||||
rect, _ = self._select_position(width, height)
|
||||
if rect:
|
||||
self._add_skyline(rect)
|
||||
|
||||
if rect is None:
|
||||
return None
|
||||
|
||||
# Store rectangle, and recalculate skyline
|
||||
rect.rid = rid
|
||||
self.rectangles.append(rect)
|
||||
return rect
|
||||
|
||||
def reset(self):
|
||||
super(Skyline, self).reset()
|
||||
self._skyline = [HSegment(P(0, 0), self.width)]
|
||||
self._waste.reset()
|
||||
|
||||
|
||||
|
||||
|
||||
class SkylineWMixin(Skyline):
|
||||
"""Waste managment mixin"""
|
||||
def __init__(self, width, height, *args, **kwargs):
|
||||
super(SkylineWMixin, self).__init__(width, height, *args, **kwargs)
|
||||
self._waste_management = True
|
||||
|
||||
|
||||
class SkylineMwf(Skyline):
|
||||
"""Implements Min Waste fit heuristic, minimizing the area wasted under the
|
||||
rectangle.
|
||||
"""
|
||||
def _rect_fitness(self, rect, left_index, right_index):
|
||||
waste = 0
|
||||
for seg in self._skyline[left_index:right_index+1]:
|
||||
waste +=\
|
||||
(min(rect.right, seg.right)-max(rect.left, seg.left)) *\
|
||||
(rect.bottom-seg.top)
|
||||
|
||||
return waste
|
||||
|
||||
def _rect_fitnes2s(self, rect, left_index, right_index):
|
||||
waste = ((min(rect.right, seg.right)-max(rect.left, seg.left)) for seg in self._skyline[left_index:right_index+1])
|
||||
return sum(waste)
|
||||
|
||||
class SkylineMwfl(Skyline):
|
||||
"""Implements Min Waste fit with low profile heuritic, minimizing the area
|
||||
wasted below the rectangle, at the same time it tries to keep the height
|
||||
minimal.
|
||||
"""
|
||||
def _rect_fitness(self, rect, left_index, right_index):
|
||||
waste = 0
|
||||
for seg in self._skyline[left_index:right_index+1]:
|
||||
waste +=\
|
||||
(min(rect.right, seg.right)-max(rect.left, seg.left)) *\
|
||||
(rect.bottom-seg.top)
|
||||
|
||||
return waste*self.width*self.height+rect.top
|
||||
|
||||
|
||||
class SkylineBl(Skyline):
|
||||
"""Implements Bottom Left heuristic, the best fit option is that which
|
||||
results in which the top side of the rectangle lies at the bottom-most
|
||||
position.
|
||||
"""
|
||||
def _rect_fitness(self, rect, left_index, right_index):
|
||||
return rect.top
|
||||
|
||||
|
||||
|
||||
|
||||
class SkylineBlWm(SkylineBl, SkylineWMixin):
|
||||
pass
|
||||
|
||||
class SkylineMwfWm(SkylineMwf, SkylineWMixin):
|
||||
pass
|
||||
|
||||
class SkylineMwflWm(SkylineMwfl, SkylineWMixin):
|
||||
pass
|
23
leenkx/blender/lnx/lightmapper/utility/rectpack/waste.py
Normal file
23
leenkx/blender/lnx/lightmapper/utility/rectpack/waste.py
Normal file
@ -0,0 +1,23 @@
|
||||
from .guillotine import GuillotineBafMinas
|
||||
from .geometry import Rectangle
|
||||
|
||||
|
||||
|
||||
class WasteManager(GuillotineBafMinas):
|
||||
|
||||
def __init__(self, rot=True, merge=True, *args, **kwargs):
|
||||
super(WasteManager, self).__init__(1, 1, rot=rot, merge=merge, *args, **kwargs)
|
||||
|
||||
def add_waste(self, x, y, width, height):
|
||||
"""Add new waste section"""
|
||||
self._add_section(Rectangle(x, y, width, height))
|
||||
|
||||
def _fits_surface(self, width, height):
|
||||
raise NotImplementedError
|
||||
|
||||
def validate_packing(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def reset(self):
|
||||
super(WasteManager, self).reset()
|
||||
self._sections = []
|
Reference in New Issue
Block a user