///|
pub fn placement_change_name(kind : PlacementChangeKind) -> String {
match kind {
Added => "added"
Removed => "removed"
Moved => "moved"
Resized => "resized"
RotationChanged => "rotation-changed"
Unchanged => "unchanged"
}
}
///|
fn find_placement_by_id(placements : ArrayView[Placed], id : String) -> Placed? {
for placed in placements {
if placed.id == id {
return Some(placed)
}
}
None
}
///|
fn placement_area_delta(old : Placed, fresh : Placed) -> Int {
placed_area(fresh) - placed_area(old)
}
///|
fn placement_moved(old : Placed, fresh : Placed) -> Bool {
old.x != fresh.x || old.y != fresh.y
}
///|
fn placement_resized(old : Placed, fresh : Placed) -> Bool {
old.width != fresh.width || old.height != fresh.height
}
///|
fn placement_rotation_changed(old : Placed, fresh : Placed) -> Bool {
old.rotated != fresh.rotated
}
///|
fn compare_existing_placement(old : Placed, fresh : Placed) -> PlacementChange {
let kind = if placement_resized(old, fresh) {
Resized
} else if placement_rotation_changed(old, fresh) {
RotationChanged
} else if placement_moved(old, fresh) {
Moved
} else {
Unchanged
}
PlacementChange::{
id: old.id,
kind,
old_x: old.x,
old_y: old.y,
old_width: old.width,
old_height: old.height,
new_x: fresh.x,
new_y: fresh.y,
new_width: fresh.width,
new_height: fresh.height,
area_delta: placement_area_delta(old, fresh),
}
}
///|
fn removed_change(old : Placed) -> PlacementChange {
PlacementChange::{
id: old.id,
kind: Removed,
old_x: old.x,
old_y: old.y,
old_width: old.width,
old_height: old.height,
new_x: 0,
new_y: 0,
new_width: 0,
new_height: 0,
area_delta: 0 - placed_area(old),
}
}
///|
fn added_change(fresh : Placed) -> PlacementChange {
PlacementChange::{
id: fresh.id,
kind: Added,
old_x: 0,
old_y: 0,
old_width: 0,
old_height: 0,
new_x: fresh.x,
new_y: fresh.y,
new_width: fresh.width,
new_height: fresh.height,
area_delta: placed_area(fresh),
}
}
///|
fn collect_left_changes(
changes : Array[PlacementChange],
left : Layout,
right : Layout,
) -> Unit {
for old in left.placements {
match find_placement_by_id(right.placements, old.id) {
Some(fresh) => changes.push(compare_existing_placement(old, fresh))
None => changes.push(removed_change(old))
}
}
}
///|
fn collect_right_additions(
changes : Array[PlacementChange],
left : Layout,
right : Layout,
) -> Unit {
for fresh in right.placements {
match find_placement_by_id(left.placements, fresh.id) {
Some(_) => ()
None => changes.push(added_change(fresh))
}
}
}
///|
fn count_change_kind(
changes : ArrayView[PlacementChange],
kind : PlacementChangeKind,
) -> Int {
let mut count = 0
for change in changes {
if change.kind == kind {
count = count + 1
}
}
count
}
///|
/// Compare two validated layouts by placement id.
pub fn diff_layouts(
left : Layout,
right : Layout,
) -> Result[LayoutDiff, PackError] {
match validate_layout(left) {
Err(err) => return Err(err)
Ok(_) => ()
}
match validate_layout(right) {
Err(err) => return Err(err)
Ok(_) => ()
}
let changes : Array[PlacementChange] = []
collect_left_changes(changes, left, right)
collect_right_additions(changes, left, right)
Ok(LayoutDiff::{
left_algorithm: left.algorithm,
right_algorithm: right.algorithm,
left_occupancy_permille: left.occupancy_permille,
right_occupancy_permille: right.occupancy_permille,
occupancy_delta: right.occupancy_permille - left.occupancy_permille,
added_count: count_change_kind(changes, Added),
removed_count: count_change_kind(changes, Removed),
moved_count: count_change_kind(changes, Moved),
resized_count: count_change_kind(changes, Resized),
rotation_changed_count: count_change_kind(changes, RotationChanged),
unchanged_count: count_change_kind(changes, Unchanged),
changes,
})
}
///|
fn write_change(out : StringBuilder, change : PlacementChange) -> Unit {
out.write_string("- ")
out.write_string(change.id)
out.write_string(" ")
out.write_string(placement_change_name(change.kind))
out.write_string(" old=")
out.write_string(
"\{change.old_x},\{change.old_y},\{change.old_width},\{change.old_height}",
)
out.write_string(" new=")
out.write_string(
"\{change.new_x},\{change.new_y},\{change.new_width},\{change.new_height}",
)
out.write_string(" area_delta=")
out.write_string("\{change.area_delta}")
out.write_char('\n')
}
///|
/// Human-readable layout diff.
pub fn to_layout_diff_report(diff : LayoutDiff) -> String {
let out = StringBuilder()
out.write_string("rectloom layout diff\n")
out.write_string("left=")
out.write_string(algorithm_name(diff.left_algorithm))
out.write_string(" right=")
out.write_string(algorithm_name(diff.right_algorithm))
out.write_string(" occupancy_delta=")
out.write_string("\{diff.occupancy_delta}")
out.write_char('\n')
out.write_string("added=")
out.write_string("\{diff.added_count}")
out.write_string(" removed=")
out.write_string("\{diff.removed_count}")
out.write_string(" moved=")
out.write_string("\{diff.moved_count}")
out.write_string(" resized=")
out.write_string("\{diff.resized_count}")
out.write_string(" rotation_changed=")
out.write_string("\{diff.rotation_changed_count}")
out.write_string(" unchanged=")
out.write_string("\{diff.unchanged_count}")
out.write_char('\n')
for change in diff.changes {
write_change(out, change)
}
out.to_string()
}
///|
/// CSV layout diff for snapshots and spreadsheets.
pub fn to_layout_diff_csv(diff : LayoutDiff) -> String {
let out = StringBuilder()
out.write_string(
"id,kind,old_x,old_y,old_width,old_height,new_x,new_y,new_width,new_height,area_delta\n",
)
for change in diff.changes {
out.write_string(csv_escape(change.id))
out.write_char(',')
out.write_string(placement_change_name(change.kind))
out.write_char(',')
out.write_string("\{change.old_x},")
out.write_string("\{change.old_y},")
out.write_string("\{change.old_width},")
out.write_string("\{change.old_height},")
out.write_string("\{change.new_x},")
out.write_string("\{change.new_y},")
out.write_string("\{change.new_width},")
out.write_string("\{change.new_height},")
out.write_string("\{change.area_delta}")
out.write_char('\n')
}
out.to_string()
}
///|
/// Convenience wrapper to compare two algorithms using the same input.
pub fn diff_algorithms(
rects : ArrayView[Rect],
base : PackOptions,
left_algorithm : Algorithm,
right_algorithm : Algorithm,
) -> Result[LayoutDiff, PackError] {
let left_options = PackOptions::{
bin: base.bin,
padding: base.padding,
sort: base.sort,
algorithm: left_algorithm,
allow_rotate: base.allow_rotate,
}
let right_options = PackOptions::{
bin: base.bin,
padding: base.padding,
sort: base.sort,
algorithm: right_algorithm,
allow_rotate: base.allow_rotate,
}
let left = match pack(rects, left_options) {
Err(err) => return Err(err)
Ok(layout) => layout
}
let right = match pack(rects, right_options) {
Err(err) => return Err(err)
Ok(layout) => layout
}
diff_layouts(left, right)
}