Update Files
This commit is contained in:
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)
|
||||
|
||||
|
Reference in New Issue
Block a user