///|
fn scaled_value(value : Int, permille : Int) -> Int {
let scaled = value * permille / 1000
if scaled <= 0 {
1
} else {
scaled
}
}
///|
fn rect_with_size(rect : Rect, width : Int, height : Int) -> Rect {
Rect::{ id: rect.id, width, height }
}
///|
fn rect_with_id(rect : Rect, id : String) -> Rect {
Rect::{ id, width: rect.width, height: rect.height }
}
///|
/// Scale all rectangles by an integer permille factor.
pub fn scale_rects(rects : ArrayView[Rect], permille : Int) -> Array[Rect] {
let out : Array[Rect] = []
for rect in rects {
out.push(
rect_with_size(
rect,
scaled_value(rect.width, permille),
scaled_value(rect.height, permille),
),
)
}
out
}
///|
/// Add pixels around every rectangle.
pub fn expand_rects(rects : ArrayView[Rect], amount : Int) -> Array[Rect] {
let out : Array[Rect] = []
let delta = amount.max(0) * 2
for rect in rects {
out.push(rect_with_size(rect, rect.width + delta, rect.height + delta))
}
out
}
///|
/// Remove pixels around every rectangle while keeping each side positive.
pub fn shrink_rects(rects : ArrayView[Rect], amount : Int) -> Array[Rect] {
let out : Array[Rect] = []
let delta = amount.max(0) * 2
for rect in rects {
out.push(
rect_with_size(
rect,
(rect.width - delta).max(1),
(rect.height - delta).max(1),
),
)
}
out
}
///|
/// Prefix all rectangle ids.
pub fn prefix_rect_ids(rects : ArrayView[Rect], prefix : String) -> Array[Rect] {
let out : Array[Rect] = []
for rect in rects {
out.push(rect_with_id(rect, "\{prefix}\{rect.id}"))
}
out
}
///|
/// Suffix all rectangle ids.
pub fn suffix_rect_ids(rects : ArrayView[Rect], suffix : String) -> Array[Rect] {
let out : Array[Rect] = []
for rect in rects {
out.push(rect_with_id(rect, "\{rect.id}\{suffix}"))
}
out
}
///|
fn rect_can_fit_direct(rect : Rect, bin : Bin) -> Bool {
rect.width <= bin.width && rect.height <= bin.height
}
///|
fn rect_can_fit_rotated(rect : Rect, bin : Bin) -> Bool {
rect.height <= bin.width && rect.width <= bin.height
}
///|
/// Keep rectangles that can fit the bin, optionally considering rotation.
pub fn filter_fit_rects(
rects : ArrayView[Rect],
bin : Bin,
allow_rotate : Bool,
) -> Array[Rect] {
let out : Array[Rect] = []
for rect in rects {
if rect_can_fit_direct(rect, bin) ||
(allow_rotate && rect_can_fit_rotated(rect, bin)) {
out.push(rect)
}
}
out
}
///|
/// Keep rectangles that cannot fit the bin.
pub fn filter_oversized_rects(
rects : ArrayView[Rect],
bin : Bin,
allow_rotate : Bool,
) -> Array[Rect] {
let out : Array[Rect] = []
for rect in rects {
if !rect_can_fit_direct(rect, bin) &&
!(allow_rotate && rect_can_fit_rotated(rect, bin)) {
out.push(rect)
}
}
out
}
///|
fn clamp_dimension(value : Int, limit : Int) -> Int {
if limit <= 0 {
value.max(1)
} else {
value.min(limit).max(1)
}
}
///|
/// Clamp rectangle dimensions to a bin-sized maximum.
pub fn clamp_rects_to_bin(rects : ArrayView[Rect], bin : Bin) -> Array[Rect] {
let out : Array[Rect] = []
for rect in rects {
out.push(
rect_with_size(
rect,
clamp_dimension(rect.width, bin.width),
clamp_dimension(rect.height, bin.height),
),
)
}
out
}
///|
fn split_count_for(value : Int, limit : Int) -> Int {
if limit <= 0 || value <= limit {
1
} else {
ceil_div(value, limit)
}
}
///|
fn split_width_once(rect : Rect, max_width : Int) -> Array[Rect] {
let out : Array[Rect] = []
let parts = split_count_for(rect.width, max_width)
if parts <= 1 {
out.push(rect)
return out
}
let mut remaining = rect.width
for i in 0.. max_width { max_width } else { remaining }
out.push(Rect::make("\{rect.id}-w\{i}", width, rect.height))
remaining = remaining - width
}
out
}
///|
/// Split rectangles wider than `max_width` into deterministic pieces.
pub fn split_wide_rects(
rects : ArrayView[Rect],
max_width : Int,
) -> Array[Rect] {
let out : Array[Rect] = []
for rect in rects {
for part in split_width_once(rect, max_width) {
out.push(part)
}
}
out
}
///|
fn split_height_once(rect : Rect, max_height : Int) -> Array[Rect] {
let out : Array[Rect] = []
let parts = split_count_for(rect.height, max_height)
if parts <= 1 {
out.push(rect)
return out
}
let mut remaining = rect.height
for i in 0.. max_height { max_height } else { remaining }
out.push(Rect::make("\{rect.id}-h\{i}", rect.width, height))
remaining = remaining - height
}
out
}
///|
/// Split rectangles taller than `max_height` into deterministic pieces.
pub fn split_tall_rects(
rects : ArrayView[Rect],
max_height : Int,
) -> Array[Rect] {
let out : Array[Rect] = []
for rect in rects {
for part in split_height_once(rect, max_height) {
out.push(part)
}
}
out
}
///|
/// Convert placements back to rectangle dimensions for follow-up packing.
pub fn rects_from_placements(placements : ArrayView[Placed]) -> Array[Rect] {
let out : Array[Rect] = []
for placed in placements {
out.push(Rect::make(placed.id, placed.width, placed.height))
}
out
}
///|
/// Convert a layout back to the dimensions that were actually packed.
pub fn rects_from_layout(layout : Layout) -> Array[Rect] {
rects_from_placements(layout.placements)
}
///|
/// Return all original dimensions from a layout, including unpacked items.
pub fn original_rects_from_layout(layout : Layout) -> Array[Rect] {
let out : Array[Rect] = []
for placed in layout.placements {
out.push(
Rect::make(placed.id, placed.original_width, placed.original_height),
)
}
for rect in layout.unpacked {
out.push(rect)
}
out
}
///|
fn id_used(rects : ArrayView[Rect], id : String) -> Bool {
for rect in rects {
if rect.id == id {
return true
}
}
false
}
///|
fn next_deduped_id(used : ArrayView[Rect], base : String) -> String {
let mut suffix = 2
let mut candidate = "\{base}-dup\{suffix}"
while id_used(used, candidate) {
suffix = suffix + 1
candidate = "\{base}-dup\{suffix}"
}
candidate
}
///|
/// Make all ids unique with stable numeric suffixes.
pub fn dedupe_rect_ids(rects : ArrayView[Rect]) -> Array[Rect] {
let out : Array[Rect] = []
for rect in rects {
if id_used(out, rect.id) {
out.push(rect_with_id(rect, next_deduped_id(out, rect.id)))
} else {
out.push(rect)
}
}
out
}
///|
/// Normalize inputs by deduping ids and splitting pieces larger than limits.
pub fn normalize_rects(
rects : ArrayView[Rect],
max_width : Int,
max_height : Int,
) -> Array[Rect] {
let deduped = dedupe_rect_ids(rects)
let width_split = split_wide_rects(deduped, max_width)
split_tall_rects(width_split, max_height)
}
///|
/// Normalize inputs for a target bin.
pub fn normalize_rects_for_bin(
rects : ArrayView[Rect],
bin : Bin,
) -> Array[Rect] {
normalize_rects(rects, bin.width.max(1), bin.height.max(1))
}
///|
fn write_rect_csv_line(out : StringBuilder, rect : Rect) -> Unit {
out.write_string(csv_escape(rect.id))
out.write_char(',')
out.write_string("\{rect.width}")
out.write_char(',')
out.write_string("\{rect.height}")
out.write_char(',')
out.write_string("\{rect_area(rect)}")
out.write_char('\n')
}
///|
/// Export rectangle dimensions as CSV.
pub fn rects_to_csv(rects : ArrayView[Rect]) -> String {
let out = StringBuilder()
out.write_string("id,width,height,area\n")
for rect in rects {
write_rect_csv_line(out, rect)
}
out.to_string()
}
///|
fn write_rect_markdown_row(out : StringBuilder, rect : Rect) -> Unit {
out.write_string("| ")
out.write_string(rect.id)
out.write_string(" | ")
out.write_string("\{rect.width}")
out.write_string(" | ")
out.write_string("\{rect.height}")
out.write_string(" | ")
out.write_string("\{rect_area(rect)}")
out.write_string(" |\n")
}
///|
/// Export rectangle dimensions as a Markdown table.
pub fn rects_to_markdown_table(rects : ArrayView[Rect]) -> String {
let out = StringBuilder()
out.write_string("| id | width | height | area |\n")
out.write_string("| --- | ---: | ---: | ---: |\n")
for rect in rects {
write_rect_markdown_row(out, rect)
}
out.to_string()
}