///|
fn scale_permille(numerator : Int, denominator : Int) -> Int {
if denominator <= 0 {
0
} else {
numerator * 1000 / denominator
}
}
///|
fn non_negative(value : Int) -> Int {
if value < 0 {
0
} else {
value
}
}
///|
fn padded_capacity_for_options(options : PackOptions) -> Int {
let width = non_negative(options.bin.width - options.padding.outer * 2)
let height = non_negative(options.bin.height - options.padding.outer * 2)
width * height
}
///|
fn rect_fits_bin(rect : Rect, bin : Bin) -> Bool {
if bin.width <= 0 || bin.height <= 0 {
true
} else {
rect.width <= bin.width && rect.height <= bin.height
}
}
///|
fn empty_rect_stats() -> RectSetStats {
RectSetStats::{
count: 0,
total_area: 0,
min_width: 0,
max_width: 0,
min_height: 0,
max_height: 0,
min_area: 0,
max_area: 0,
average_width_permille: 0,
average_height_permille: 0,
average_area_permille: 0,
square_count: 0,
wide_count: 0,
tall_count: 0,
oversized_count: 0,
}
}
///|
fn summarize_rects_inner(rects : ArrayView[Rect], bin : Bin) -> RectSetStats {
if rects.is_empty() {
return empty_rect_stats()
}
let first = rects[0]
let mut total_area = 0
let mut total_width = 0
let mut total_height = 0
let mut min_width = first.width
let mut max_width = first.width
let mut min_height = first.height
let mut max_height = first.height
let mut min_area = rect_area(first)
let mut max_area = rect_area(first)
let mut square_count = 0
let mut wide_count = 0
let mut tall_count = 0
let mut oversized_count = 0
for rect in rects {
let area = rect_area(rect)
total_area = total_area + area
total_width = total_width + rect.width
total_height = total_height + rect.height
if rect.width < min_width {
min_width = rect.width
}
if rect.width > max_width {
max_width = rect.width
}
if rect.height < min_height {
min_height = rect.height
}
if rect.height > max_height {
max_height = rect.height
}
if area < min_area {
min_area = area
}
if area > max_area {
max_area = area
}
if rect.width == rect.height {
square_count = square_count + 1
} else if rect.width > rect.height {
wide_count = wide_count + 1
} else {
tall_count = tall_count + 1
}
if !rect_fits_bin(rect, bin) {
oversized_count = oversized_count + 1
}
}
let count = rects.length()
RectSetStats::{
count,
total_area,
min_width,
max_width,
min_height,
max_height,
min_area,
max_area,
average_width_permille: scale_permille(total_width, count),
average_height_permille: scale_permille(total_height, count),
average_area_permille: scale_permille(total_area, count),
square_count,
wide_count,
tall_count,
oversized_count,
}
}
///|
/// Summarize a rectangle collection without considering a target bin.
pub fn summarize_rects(rects : ArrayView[Rect]) -> RectSetStats {
summarize_rects_inner(rects, Bin::{ width: 0, height: 0 })
}
///|
/// Summarize a rectangle collection and count items larger than `bin`.
pub fn summarize_rects_for_bin(
rects : ArrayView[Rect],
bin : Bin,
) -> RectSetStats {
summarize_rects_inner(rects, bin)
}
///|
fn min_placed_x(placements : ArrayView[Placed]) -> Int {
if placements.is_empty() {
return 0
}
let mut value = placements[0].x
for placed in placements {
if placed.x < value {
value = placed.x
}
}
value
}
///|
fn min_placed_y(placements : ArrayView[Placed]) -> Int {
if placements.is_empty() {
return 0
}
let mut value = placements[0].y
for placed in placements {
if placed.y < value {
value = placed.y
}
}
value
}
///|
fn max_placed_right(placements : ArrayView[Placed]) -> Int {
if placements.is_empty() {
return 0
}
let mut value = placements[0].x + placements[0].width
for placed in placements {
let right = placed.x + placed.width
if right > value {
value = right
}
}
value
}
///|
fn max_placed_bottom(placements : ArrayView[Placed]) -> Int {
if placements.is_empty() {
return 0
}
let mut value = placements[0].y + placements[0].height
for placed in placements {
let bottom = placed.y + placed.height
if bottom > value {
value = bottom
}
}
value
}
///|
fn rotated_count(placements : ArrayView[Placed]) -> Int {
let mut count = 0
for placed in placements {
if placed.rotated {
count = count + 1
}
}
count
}
///|
fn row_seen_before(placements : ArrayView[Placed], index : Int) -> Bool {
let y = placements[index].y
for i in 0.. Bool {
let x = placements[index].x
for i in 0.. Int {
let mut count = 0
for i in 0.. Int {
let mut count = 0
for i in 0.. Int {
if placements.is_empty() {
return 0
}
let rows = unique_row_count(placements)
scale_permille(placements.length(), rows.max(1))
}
///|
fn column_density_score(placements : ArrayView[Placed]) -> Int {
if placements.is_empty() {
return 0
}
let cols = unique_column_count(placements)
scale_permille(placements.length(), cols.max(1))
}
///|
fn fragmentation_from_rows_cols(rows : Int, cols : Int, placed : Int) -> Int {
if rows <= 0 || cols <= 0 || placed <= 0 {
return 0
}
let slots = rows * cols
if slots <= placed {
0
} else {
scale_permille(slots - placed, slots)
}
}
///|
fn compactness_from_stats(
used_area : Int,
bounding_area : Int,
occupancy : Int,
) -> Int {
if bounding_area <= 0 {
occupancy
} else {
scale_permille(used_area, bounding_area)
}
}
///|
fn empty_layout_stats(layout : Layout, padded_capacity : Int) -> LayoutStats {
LayoutStats::{
placed_count: 0,
unpacked_count: layout.unpacked.length(),
rotated_count: 0,
used_area: 0,
container_area: bin_area(layout.bin),
padded_capacity,
occupancy_permille: 0,
padded_occupancy_permille: 0,
waste_area: bin_area(layout.bin),
bounding_x: 0,
bounding_y: 0,
bounding_width: 0,
bounding_height: 0,
bounding_area: 0,
bounding_occupancy_permille: 0,
row_count: 0,
column_count: 0,
horizontal_span: 0,
vertical_span: 0,
fragmentation_score: 0,
compactness_score: 0,
}
}
///|
fn analyze_layout_inner(
layout : Layout,
padded_capacity : Int,
) -> Result[LayoutStats, PackError] {
match validate_layout(layout) {
Err(err) => return Err(err)
Ok(_) => ()
}
if layout.placements.is_empty() {
return Ok(empty_layout_stats(layout, padded_capacity))
}
let min_x = min_placed_x(layout.placements)
let min_y = min_placed_y(layout.placements)
let max_x = max_placed_right(layout.placements)
let max_y = max_placed_bottom(layout.placements)
let bounding_width = max_x - min_x
let bounding_height = max_y - min_y
let bounding_area = bounding_width * bounding_height
let used_area = total_placed_area(layout.placements)
let container_area = bin_area(layout.bin)
let rows = unique_row_count(layout.placements)
let cols = unique_column_count(layout.placements)
let occupancy = scale_permille(used_area, container_area)
let padded_occupancy = scale_permille(used_area, padded_capacity)
let bounding_occupancy = scale_permille(used_area, bounding_area)
let fragmentation = fragmentation_from_rows_cols(
rows,
cols,
layout.placements.length(),
)
LayoutStats::{
placed_count: layout.placements.length(),
unpacked_count: layout.unpacked.length(),
rotated_count: rotated_count(layout.placements),
used_area,
container_area,
padded_capacity,
occupancy_permille: occupancy,
padded_occupancy_permille: padded_occupancy,
waste_area: container_area - used_area,
bounding_x: min_x,
bounding_y: min_y,
bounding_width,
bounding_height,
bounding_area,
bounding_occupancy_permille: bounding_occupancy,
row_count: rows,
column_count: cols,
horizontal_span: bounding_width,
vertical_span: bounding_height,
fragmentation_score: fragmentation,
compactness_score: compactness_from_stats(
used_area, bounding_area, occupancy,
),
}
|> Ok
}
///|
/// Analyze a layout using the full container area as capacity.
pub fn analyze_layout(layout : Layout) -> Result[LayoutStats, PackError] {
analyze_layout_inner(layout, bin_area(layout.bin))
}
///|
/// Analyze a layout with the padded capacity from the original options.
pub fn analyze_layout_with_options(
layout : Layout,
options : PackOptions,
) -> Result[LayoutStats, PackError] {
analyze_layout_inner(layout, padded_capacity_for_options(options))
}
///|
pub fn layout_quality_score(stats : LayoutStats) -> Int {
let pack_bonus = stats.placed_count * 10000
let unpack_penalty = stats.unpacked_count * 12000
let occupancy_bonus = stats.occupancy_permille * 8
let compact_bonus = stats.compactness_score * 2
let fragmentation_penalty = stats.fragmentation_score * 2
pack_bonus +
occupancy_bonus +
compact_bonus -
unpack_penalty -
fragmentation_penalty
}
///|
pub fn utilization_label(permille : Int) -> String {
if permille >= 900 {
"excellent"
} else if permille >= 750 {
"strong"
} else if permille >= 550 {
"usable"
} else if permille >= 350 {
"sparse"
} else {
"low"
}
}
///|
fn write_stat_line(out : StringBuilder, label : String, value : Int) -> Unit {
out.write_string(label)
out.write_string(": ")
out.write_string("\{value}")
out.write_char('\n')
}
///|
fn write_permille_line(
out : StringBuilder,
label : String,
value : Int,
) -> Unit {
out.write_string(label)
out.write_string(": ")
out.write_string("\{value}/1000 ")
out.write_string(utilization_label(value))
out.write_char('\n')
}
///|
pub fn to_rect_stats_report(stats : RectSetStats) -> String {
let out = StringBuilder()
out.write_string("rectloom rect-set statistics\n")
write_stat_line(out, "count", stats.count)
write_stat_line(out, "total_area", stats.total_area)
write_stat_line(out, "min_width", stats.min_width)
write_stat_line(out, "max_width", stats.max_width)
write_stat_line(out, "min_height", stats.min_height)
write_stat_line(out, "max_height", stats.max_height)
write_stat_line(out, "min_area", stats.min_area)
write_stat_line(out, "max_area", stats.max_area)
write_stat_line(out, "average_width_x1000", stats.average_width_permille)
write_stat_line(out, "average_height_x1000", stats.average_height_permille)
write_stat_line(out, "average_area_x1000", stats.average_area_permille)
write_stat_line(out, "square_count", stats.square_count)
write_stat_line(out, "wide_count", stats.wide_count)
write_stat_line(out, "tall_count", stats.tall_count)
write_stat_line(out, "oversized_count", stats.oversized_count)
out.to_string()
}
///|
pub fn to_layout_stats_report(stats : LayoutStats) -> String {
let out = StringBuilder()
out.write_string("rectloom layout statistics\n")
write_stat_line(out, "placed_count", stats.placed_count)
write_stat_line(out, "unpacked_count", stats.unpacked_count)
write_stat_line(out, "rotated_count", stats.rotated_count)
write_stat_line(out, "used_area", stats.used_area)
write_stat_line(out, "container_area", stats.container_area)
write_stat_line(out, "padded_capacity", stats.padded_capacity)
write_permille_line(out, "occupancy", stats.occupancy_permille)
write_permille_line(out, "padded_occupancy", stats.padded_occupancy_permille)
write_stat_line(out, "waste_area", stats.waste_area)
write_stat_line(out, "bounding_x", stats.bounding_x)
write_stat_line(out, "bounding_y", stats.bounding_y)
write_stat_line(out, "bounding_width", stats.bounding_width)
write_stat_line(out, "bounding_height", stats.bounding_height)
write_stat_line(out, "bounding_area", stats.bounding_area)
write_permille_line(
out,
"bounding_occupancy",
stats.bounding_occupancy_permille,
)
write_stat_line(out, "row_count", stats.row_count)
write_stat_line(out, "column_count", stats.column_count)
write_stat_line(out, "horizontal_span", stats.horizontal_span)
write_stat_line(out, "vertical_span", stats.vertical_span)
write_stat_line(out, "fragmentation_score", stats.fragmentation_score)
write_stat_line(out, "compactness_score", stats.compactness_score)
write_stat_line(out, "quality_score", layout_quality_score(stats))
out.to_string()
}
///|
pub fn to_analysis_report(layout : Layout) -> Result[String, PackError] {
match analyze_layout(layout) {
Err(err) => Err(err)
Ok(stats) => Ok(to_layout_stats_report(stats))
}
}
///|
pub fn row_density_hint(layout : Layout) -> Result[Int, PackError] {
match validate_layout(layout) {
Err(err) => Err(err)
Ok(_) => Ok(row_density_score(layout.placements))
}
}
///|
pub fn column_density_hint(layout : Layout) -> Result[Int, PackError] {
match validate_layout(layout) {
Err(err) => Err(err)
Ok(_) => Ok(column_density_score(layout.placements))
}
}