Files
2025-04-26 10:41:46 -05:00

139 lines
5.7 KiB
GDScript

@tool
# This code is taken from: https://github.com/semibran/pack/blob/master/lib/pack.js
# Copyright (c) 2018 Brandon Semilla (MIT License) - original author
# Copyright (c) 2023 Nikolay Lebedev (MIT License) - porting to gdscript, refactoring and optimization
const _Result = preload("result.gd").Class
const _Common = preload("common.gd")
const __WHITESPACE_WEIGHT: float = 1
const __SIDE_LENGTH_WEIGHT: float = 10
class RectPackingResult:
extends _Result
# Total size of the entire layout of rectangles.
var bounds: Vector2i
# Computed positions of the input rectangles
# in the same order as their sizes were passed in.
var rects_positions: Array[Vector2i]
func _get_result_type_description() -> String:
return "Rect packing"
func success(bounds: Vector2i, rects_positions: Array[Vector2i]) -> void:
_success()
self.bounds = bounds
self.rects_positions = rects_positions
static func __add_rect_to_cache(rect: Rect2i, cache: Dictionary, cache_grid_size: Vector2i) -> void:
var left_top_cell: Vector2i = rect.position / cache_grid_size
var right_bottom_cell: Vector2i = rect.end / cache_grid_size + (rect.end % cache_grid_size).sign()
for y in range(left_top_cell.y, right_bottom_cell.y):
for x in range(left_top_cell.x, right_bottom_cell.x):
var cell: Vector2i = Vector2i(x, y)
if cache.has(cell):
cache[cell].push_back(rect)
else:
cache[cell] = [rect] as Array[Rect2i]
const __empty_rect_array: Array[Rect2i] = []
static func __has_intersection(rect: Rect2i, cache: Dictionary, cache_grid_size: Vector2i) -> bool:
var left_top_cell: Vector2i = rect.position / cache_grid_size
var right_bottom_cell: Vector2i = rect.end / cache_grid_size + (rect.end % cache_grid_size).sign()
for y in range(left_top_cell.y, right_bottom_cell.y):
for x in range(left_top_cell.x, right_bottom_cell.x):
for cached_rect in cache.get(Vector2i(x, y), __empty_rect_array):
if cached_rect.intersects(rect):
return true
return false
# The function takes an array of rectangle sizes as input and compactly packs them.
static func pack(rects_sizes: Array[Vector2i]) -> RectPackingResult:
var result: RectPackingResult = RectPackingResult.new()
var rects_count: int = rects_sizes.size()
if rects_count == 0:
result.success(Vector2i.ZERO, [])
return result
var rects_positions: Array[Vector2i]
rects_positions.resize(rects_count)
var min_area: int
var rect_sizes_sum: Vector2i
for size in rects_sizes:
if size.x < 0 or size.y < 0:
result.fail(ERR_INVALID_DATA, "Negative rect size found")
return result
min_area += size.x * size.y
rect_sizes_sum += size
if min_area == 0:
result.success(Vector2i.ZERO, rects_positions)
return result
var average_rect_size: Vector2 = Vector2(rect_sizes_sum) / rects_count
var rect_cache_grid_size: Vector2i = average_rect_size.ceil() * 2
var average_squared_rect_side_length: float = sqrt(min_area / float(rects_count))
var rect_cache: Dictionary
var possible_bounds_side_length: int = ceili(sqrt(rects_count))
nearest_po2(possible_bounds_side_length)
var rects_order_arr: Array = PackedInt32Array(range(0, rects_count))
rects_order_arr.sort_custom(func(a: int, b: int) -> bool:
return rects_sizes[a].x * rects_sizes[a].y > rects_sizes[b].x * rects_sizes[b].y)
var rects_order: PackedInt32Array = PackedInt32Array(rects_order_arr)
var bounds: Vector2i = rects_sizes[rects_order[0]]
var utilized_area: int = bounds.x * bounds.y
var splits_by_axis: Array[PackedInt32Array] = [[0, bounds.x], [0, bounds.y]]
__add_rect_to_cache(Rect2i(Vector2i.ZERO, rects_sizes[rects_order[0]]), rect_cache, rect_cache_grid_size)
for rect_index in range(1, rects_count): # skip first rect at (0, 0)
var ordered_rect_index: int = rects_order[rect_index]
var rect: Rect2i = Rect2i(Vector2i.ZERO, rects_sizes[ordered_rect_index])
var rect_area: int = rect.get_area()
if rect_area == 0:
continue
utilized_area += rect_area
var best_score: float = INF
var best_new_bounds: Vector2i = bounds
for landing_rect_index in rect_index:
var ordered_landing_rect_index: int = rects_order[landing_rect_index]
var landing_rect: Rect2i = Rect2i(
rects_positions[ordered_landing_rect_index],
rects_sizes[ordered_landing_rect_index])
for split_axis_index in 2:
var orthogonal_asis_index: int = (split_axis_index + 1) % 2
var splits: PackedInt32Array = splits_by_axis[split_axis_index]
rect.position[orthogonal_asis_index] = landing_rect.end[orthogonal_asis_index]
for split_index in range(
splits.bsearch(landing_rect.position[split_axis_index]),
splits.bsearch(landing_rect.end[split_axis_index])):
rect.position[split_axis_index] = splits[split_index]
if __has_intersection(rect, rect_cache, rect_cache_grid_size):
continue
var new_bounds: Vector2i = Vector2i(
maxi(bounds.x, rect.end.x),
maxi(bounds.y, rect.end.y))
var score: float = \
__WHITESPACE_WEIGHT * (new_bounds.x * new_bounds.y - utilized_area) + \
__SIDE_LENGTH_WEIGHT * average_squared_rect_side_length * maxf(new_bounds.x, new_bounds.y)
if score < best_score:
best_score = score
rects_positions[ordered_rect_index] = rect.position
best_new_bounds = new_bounds
bounds = best_new_bounds
rect.position = rects_positions[ordered_rect_index]
__add_rect_to_cache(rect, rect_cache, rect_cache_grid_size)
# Add new splits at rect.end if they dot't already exist
for split_axis_index in 2:
var splits: PackedInt32Array = splits_by_axis[split_axis_index]
var position: int = rect.end[split_axis_index]
var split_index: int = splits.bsearch(position)
if split_index == splits.size():
splits.append(position)
elif splits[split_index] != position:
splits.insert(split_index, position)
result.success(bounds, rects_positions)
return result