You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
89 lines
3.2 KiB
Python
89 lines
3.2 KiB
Python
"""
|
|
2D bin-packing
|
|
"""
|
|
from typing import Tuple
|
|
|
|
import numpy
|
|
from numpy.typing import NDArray, ArrayLike
|
|
|
|
from ..error import MasqueError
|
|
|
|
|
|
def maxrects_bssf(
|
|
rects: ArrayLike,
|
|
containers: ArrayLike,
|
|
presort: bool = True,
|
|
allow_rejects: bool = True,
|
|
) -> Tuple[NDArray[numpy.float64], NDArray[numpy.float64]]:
|
|
"""
|
|
sizes should be Nx2
|
|
regions should be Mx4 (xmin, ymin, xmax, ymax)
|
|
"""
|
|
regions = numpy.array(containers, copy=False, dtype=float)
|
|
rect_sizes = numpy.array(rects, copy=False, dtype=float)
|
|
rect_locs = numpy.zeros_like(rect_sizes)
|
|
rejected_rects = []
|
|
|
|
if presort:
|
|
rotated_sizes = numpy.sort(rect_sizes, axis=0) # shortest side first
|
|
rect_order = numpy.lexsort(rotated_sizes.T)[::-1] # Descending shortest side
|
|
rect_sizes = rect_sizes[rect_order]
|
|
|
|
for rect_ind, rect_size in enumerate(rect_sizes):
|
|
''' Remove degenerate regions '''
|
|
# First remove duplicate regions (but keep one; code below would drop both)
|
|
regions = numpy.unique(regions, axis=0)
|
|
|
|
# Now remove regions enclosed in another
|
|
min_more = (regions[None, :, :2] >= regions[:, None, :2]).all(axis=2) # first axis > second axis
|
|
max_less = (regions[None, :, 2:] <= regions[:, None, 2:]).all(axis=2) # first axis < second axis
|
|
max_less &= ~numpy.eye(regions.shape[0], dtype=bool) # exclude self
|
|
degenerate = (min_more & max_less).any(axis=0)
|
|
regions = regions[~degenerate]
|
|
|
|
''' Place the rect '''
|
|
# Best short-side fit (bssf) to pick a region
|
|
bssf_scores = ((regions[:, 2:] - regions[:, :2]) - rect_size).min(axis=1).astype(float)
|
|
bssf_scores[bssf_scores < 0] = numpy.inf # doesn't fit!
|
|
rr = bssf_scores.argmin()
|
|
if numpy.isinf(bssf_scores[rr]):
|
|
if allow_rejects:
|
|
rejected_rects.append(rect_ind)
|
|
continue
|
|
else:
|
|
raise MasqueError(f'Failed to find a suitable location for rectangle {rect_ind}')
|
|
|
|
# Read out location
|
|
loc = regions[rr, :2]
|
|
rect_locs[rect_ind] = loc
|
|
|
|
''' Shatter regions '''
|
|
# Which regions does this rectangle intersect?
|
|
min_over = regions[:, :2] >= loc + rect_size
|
|
max_undr = regions[:, 2:] <= loc
|
|
intersects = ~(min_over | max_undr).any(axis=1)
|
|
|
|
# Which sides is there excess on?
|
|
region_past_botleft = intersects[:, None] & (regions[:, :2] < loc)
|
|
region_past_topright = intersects[:, None] & (regions[:, 2:] > loc + rect_size)
|
|
|
|
# Create new regions
|
|
r_lft = regions[region_past_botleft[:, 0]].copy()
|
|
r_bot = regions[region_past_botleft[:, 1]].copy()
|
|
r_rgt = regions[region_past_topright[:, 0]].copy()
|
|
r_top = regions[region_past_topright[:, 1]].copy()
|
|
|
|
r_lft[:, 2] = loc[0]
|
|
r_bot[:, 3] = loc[1]
|
|
r_rgt[:, 0] = loc[0] + rect_size[0]
|
|
r_top[:, 1] = loc[1] + rect_size[1]
|
|
|
|
regions = numpy.vstack((regions[~intersects], r_lft, r_bot, r_rgt, r_top))
|
|
|
|
if rejected_rects:
|
|
rejected_rects_arr = numpy.vstack(rejected_rects)
|
|
else:
|
|
rejected_rects_arr = numpy.empty((0, 2))
|
|
|
|
return rect_locs, rejected_rects_arr
|