///|
fn sort_name(sort : SortOrder) -> String {
match sort {
InputOrder => "input"
AreaDesc => "area-desc"
HeightDesc => "height-desc"
WidthDesc => "width-desc"
PerimeterDesc => "perimeter-desc"
}
}
///|
fn bool_name(value : Bool) -> String {
if value {
"true"
} else {
"false"
}
}
///|
fn options_key(options : PackOptions) -> String {
"\{algorithm_name(options.algorithm)}|\{sort_name(options.sort)}|rotate=\{bool_name(options.allow_rotate)}|outer=\{options.padding.outer}|inner=\{options.padding.inner}"
}
///|
fn same_options(left : PackOptions, right : PackOptions) -> Bool {
left.bin == right.bin &&
left.padding == right.padding &&
left.sort == right.sort &&
left.algorithm == right.algorithm &&
left.allow_rotate == right.allow_rotate
}
///|
fn contains_options(
options_list : ArrayView[PackOptions],
options : PackOptions,
) -> Bool {
for existing in options_list {
if same_options(existing, options) {
return true
}
}
false
}
///|
fn push_options_once(
options_list : Array[PackOptions],
options : PackOptions,
) -> Unit {
if !contains_options(options_list, options) {
options_list.push(options)
}
}
///|
fn candidate_options_from(
base : PackOptions,
algorithm : Algorithm,
sort : SortOrder,
rotate : Bool,
inner : Int,
) -> PackOptions {
PackOptions::{
bin: base.bin,
padding: Padding::{ outer: base.padding.outer, inner },
sort,
algorithm,
allow_rotate: rotate,
}
}
///|
fn available_algorithms() -> Array[Algorithm] {
[ShelfFirstFit, GuillotineBestArea, SkylineBottomLeft]
}
///|
fn available_sorts() -> Array[SortOrder] {
[InputOrder, AreaDesc, HeightDesc, WidthDesc, PerimeterDesc]
}
///|
fn available_rotations(base : PackOptions) -> Array[Bool] {
if base.allow_rotate {
[true, false]
} else {
[false, true]
}
}
///|
fn push_inner_padding_variants(values : Array[Int], base_inner : Int) -> Unit {
if base_inner >= 0 {
values.push(base_inner)
}
if base_inner != 0 {
values.push(0)
}
if base_inner != 1 {
values.push(1)
}
if base_inner != 2 {
values.push(2)
}
}
///|
fn available_inner_paddings(base : PackOptions) -> Array[Int] {
let values : Array[Int] = []
push_inner_padding_variants(values, base.padding.inner)
values
}
///|
fn build_option_space(base : PackOptions) -> Array[PackOptions] {
let options_list : Array[PackOptions] = []
for algorithm in available_algorithms() {
for sort in available_sorts() {
for rotate in available_rotations(base) {
for inner in available_inner_paddings(base) {
push_options_once(
options_list,
candidate_options_from(base, algorithm, sort, rotate, inner),
)
}
}
}
}
push_options_once(options_list, base)
options_list
}
///|
fn pack_candidate_score(layout : Layout) -> Int {
match analyze_layout(layout) {
Err(_) => -100000000
Ok(stats) => {
let all_packed_bonus = if stats.unpacked_count == 0 { 20000 } else { 0 }
let rotation_penalty = stats.rotated_count * 150
layout_quality_score(stats) + all_packed_bonus - rotation_penalty
}
}
}
///|
fn make_candidate(options : PackOptions, layout : Layout) -> PackCandidate {
PackCandidate::{
options,
layout,
score: pack_candidate_score(layout),
rank: 0,
}
}
///|
fn compare_candidates_for_sort(
left : PackCandidate,
right : PackCandidate,
) -> Int {
if left.score > right.score {
-1
} else if left.score < right.score {
1
} else {
let lk = options_key(left.options)
let rk = options_key(right.options)
if lk < rk {
-1
} else if lk > rk {
1
} else {
0
}
}
}
///|
fn rank_candidates(
candidates : ArrayView[PackCandidate],
) -> Array[PackCandidate] {
let sorted = candidates.to_owned()
sorted.sort_by(compare_candidates_for_sort)
let ranked : Array[PackCandidate] = []
for i in 0.. Result[Array[PackCandidate], PackError] {
let candidates : Array[PackCandidate] = []
for options in build_option_space(base) {
match pack(rects, options) {
Err(err) => return Err(err)
Ok(layout) => candidates.push(make_candidate(options, layout))
}
}
Ok(candidates)
}
///|
/// Compare deterministic option combinations for the same input.
pub fn compare_packers(
rects : ArrayView[Rect],
base : PackOptions,
) -> Result[PackComparison, PackError] {
match collect_candidates(rects, base) {
Err(err) => Err(err)
Ok(candidates) => {
let ranked = rank_candidates(candidates)
Ok(PackComparison::{
base_options: base,
candidates: ranked,
best: ranked[0],
})
}
}
}
///|
/// Pack with the highest-scoring option combination.
pub fn recommend_pack(
rects : ArrayView[Rect],
base : PackOptions,
) -> Result[Layout, PackError] {
match compare_packers(rects, base) {
Err(err) => Err(err)
Ok(comparison) => Ok(comparison.best.layout)
}
}
///|
/// Return the top candidate without discarding ranking metadata.
pub fn recommend_candidate(
rects : ArrayView[Rect],
base : PackOptions,
) -> Result[PackCandidate, PackError] {
match compare_packers(rects, base) {
Err(err) => Err(err)
Ok(comparison) => Ok(comparison.best)
}
}
///|
fn write_candidate_line(out : StringBuilder, candidate : PackCandidate) -> Unit {
out.write_string("#")
out.write_string("\{candidate.rank}")
out.write_string(" ")
out.write_string(options_key(candidate.options))
out.write_string(" score=")
out.write_string("\{candidate.score}")
out.write_string(" placed=")
out.write_string("\{candidate.layout.placements.length()}")
out.write_string(" unpacked=")
out.write_string("\{candidate.layout.unpacked.length()}")
out.write_string(" occupancy=")
out.write_string("\{candidate.layout.occupancy_permille}/1000")
out.write_char('\n')
}
///|
/// Human-readable report for automatic option exploration.
pub fn to_comparison_report(comparison : PackComparison) -> String {
let out = StringBuilder()
out.write_string("rectloom option comparison\n")
out.write_string("base=")
out.write_string(options_key(comparison.base_options))
out.write_char('\n')
out.write_string("candidates=")
out.write_string("\{comparison.candidates.length()}")
out.write_char('\n')
out.write_string("best=")
out.write_string(options_key(comparison.best.options))
out.write_char('\n')
for candidate in comparison.candidates {
write_candidate_line(out, candidate)
}
out.to_string()
}
///|
/// CSV output for spreadsheet-based result comparison.
pub fn to_comparison_csv(comparison : PackComparison) -> String {
let out = StringBuilder()
out.write_string(
"rank,algorithm,sort,rotate,outer_padding,inner_padding,score,placed,unpacked,occupancy_permille,waste_area\n",
)
for candidate in comparison.candidates {
out.write_string("\{candidate.rank},")
out.write_string(algorithm_name(candidate.options.algorithm))
out.write_char(',')
out.write_string(sort_name(candidate.options.sort))
out.write_char(',')
out.write_string(bool_name(candidate.options.allow_rotate))
out.write_char(',')
out.write_string("\{candidate.options.padding.outer},")
out.write_string("\{candidate.options.padding.inner},")
out.write_string("\{candidate.score},")
out.write_string("\{candidate.layout.placements.length()},")
out.write_string("\{candidate.layout.unpacked.length()},")
out.write_string("\{candidate.layout.occupancy_permille},")
out.write_string("\{candidate.layout.waste_area}")
out.write_char('\n')
}
out.to_string()
}