use super::model::{Placement2D, Rect, Sheet2D};
pub(crate) fn usable_drop_metrics(
sheet: &Sheet2D,
placements: &[Placement2D],
min_usable_side: u32,
) -> (u64, u128) {
let largest = largest_usable_free_rectangle_area(sheet, placements, min_usable_side);
let partition = disjoint_partition(sheet, placements);
let sum_sq = partition
.iter()
.filter(|r| r.width >= min_usable_side && r.height >= min_usable_side)
.map(|r| {
let area = u128::from(r.width) * u128::from(r.height);
area * area
})
.fold(0_u128, u128::saturating_add);
(largest, sum_sq)
}
fn disjoint_partition(sheet: &Sheet2D, placements: &[Placement2D]) -> Vec<Rect> {
let mut ys: Vec<u32> = Vec::with_capacity(placements.len() * 2 + 2);
ys.push(0);
ys.push(sheet.height);
for p in placements {
ys.push(p.y.min(sheet.height));
ys.push(p.y.saturating_add(p.height).min(sheet.height));
}
ys.sort_unstable();
ys.dedup();
let mut rects = Vec::new();
for window in ys.windows(2) {
let y_lo = window[0];
let y_hi = window[1];
if y_hi == y_lo {
continue;
}
let band_height = y_hi - y_lo;
let mut occupied: Vec<(u32, u32)> = placements
.iter()
.filter(|p| {
let p_bottom = p.y.saturating_add(p.height);
p.y <= y_lo && p_bottom >= y_hi
})
.map(|p| (p.x, p.x.saturating_add(p.width).min(sheet.width)))
.collect();
occupied.sort_unstable();
let mut cursor = 0_u32;
for (start, end) in occupied {
if start > cursor {
rects.push(Rect { x: cursor, y: y_lo, width: start - cursor, height: band_height });
}
cursor = cursor.max(end);
}
if cursor < sheet.width {
rects.push(Rect {
x: cursor,
y: y_lo,
width: sheet.width - cursor,
height: band_height,
});
}
}
rects
}
fn largest_usable_free_rectangle_area(
sheet: &Sheet2D,
placements: &[Placement2D],
min_usable_side: u32,
) -> u64 {
let mut ys: Vec<u32> = Vec::with_capacity(placements.len() * 2 + 2);
ys.push(0);
ys.push(sheet.height);
for p in placements {
ys.push(p.y.min(sheet.height));
ys.push(p.y.saturating_add(p.height).min(sheet.height));
}
ys.sort_unstable();
ys.dedup();
let mut best: u64 = 0;
for i in 0..ys.len() {
for j in (i + 1)..ys.len() {
let y_lo = ys[i];
let y_hi = ys[j];
let slab_height = y_hi - y_lo;
if slab_height < min_usable_side {
continue;
}
let mut occupied: Vec<(u32, u32)> = placements
.iter()
.filter(|p| p.y < y_hi && p.y.saturating_add(p.height) > y_lo)
.map(|p| (p.x, p.x.saturating_add(p.width).min(sheet.width)))
.collect();
occupied.sort_unstable();
let mut cursor = 0_u32;
for (start, end) in &occupied {
if *start > cursor {
let free_width = start - cursor;
if free_width >= min_usable_side {
let candidate = u64::from(free_width) * u64::from(slab_height);
if candidate > best {
best = candidate;
}
}
}
cursor = cursor.max(*end);
}
if sheet.width > cursor {
let free_width = sheet.width - cursor;
if free_width >= min_usable_side {
let candidate = u64::from(free_width) * u64::from(slab_height);
if candidate > best {
best = candidate;
}
}
}
}
}
best
}
#[cfg(test)]
mod tests {
use super::*;
fn sheet(width: u32, height: u32) -> Sheet2D {
Sheet2D {
name: "s".to_string(),
width,
height,
cost: 1.0,
quantity: None,
kerf: 0,
edge_kerf_relief: false,
}
}
fn place(x: u32, y: u32, width: u32, height: u32) -> Placement2D {
Placement2D { name: "p".to_string(), x, y, width, height, rotated: false }
}
#[test]
fn empty_placements_make_the_whole_sheet_one_drop() {
let s = sheet(10, 10);
let (largest, sum_sq) = usable_drop_metrics(&s, &[], 0);
assert_eq!(largest, 100);
assert_eq!(sum_sq, 10_000);
}
#[test]
fn full_sheet_placement_yields_zero_metrics() {
let s = sheet(10, 10);
let (largest, sum_sq) = usable_drop_metrics(&s, &[place(0, 0, 10, 10)], 0);
assert_eq!(largest, 0);
assert_eq!(sum_sq, 0);
}
#[test]
fn single_corner_placement_yields_l_shape_largest_and_partition_sum() {
let s = sheet(10, 10);
let (largest, sum_sq) = usable_drop_metrics(&s, &[place(0, 0, 4, 4)], 0);
assert_eq!(largest, 60);
assert_eq!(sum_sq, 4_176);
}
#[test]
fn two_placements_produce_multiple_drops() {
let s = sheet(10, 10);
let placements = vec![place(0, 0, 3, 3), place(7, 7, 3, 3)];
let (largest, sum_sq) = usable_drop_metrics(&s, &placements, 0);
assert_eq!(largest, 49);
assert_eq!(sum_sq, 2_482);
}
#[test]
fn threshold_filters_strips_below_minimum_side() {
let s = sheet(20, 10);
let placements = vec![place(0, 0, 9, 10), place(11, 0, 9, 10)];
let (largest_unfiltered, sum_sq_unfiltered) = usable_drop_metrics(&s, &placements, 0);
assert_eq!(largest_unfiltered, 20); assert_eq!(sum_sq_unfiltered, 400);
let (largest_filtered, sum_sq_filtered) = usable_drop_metrics(&s, &placements, 3);
assert_eq!(largest_filtered, 0);
assert_eq!(sum_sq_filtered, 0);
}
#[test]
fn sum_sq_strictly_increases_when_drops_merge() {
let s = sheet(6, 2);
let (_, sum_sq_fragmented) = usable_drop_metrics(&s, &[place(2, 0, 2, 2)], 0);
let (_, sum_sq_merged) = usable_drop_metrics(&s, &[place(0, 0, 2, 2)], 0);
assert!(
sum_sq_merged > sum_sq_fragmented,
"merged layout should have higher sum_sq: merged={sum_sq_merged} fragmented={sum_sq_fragmented}"
);
}
#[test]
fn largest_drop_bounded_by_waste_area() {
let s = sheet(10, 10);
let placements = vec![place(0, 0, 3, 3), place(7, 7, 3, 3)];
let (largest, _) = usable_drop_metrics(&s, &placements, 0);
let used_area =
placements.iter().map(|p| u64::from(p.width) * u64::from(p.height)).sum::<u64>();
let sheet_area = u64::from(s.width) * u64::from(s.height);
assert!(largest <= sheet_area - used_area);
}
#[test]
fn sum_sq_bounded_by_waste_area_squared() {
let s = sheet(10, 10);
let placements = vec![place(2, 2, 6, 6)];
let (_, sum_sq) = usable_drop_metrics(&s, &placements, 0);
let used_area =
placements.iter().map(|p| u128::from(p.width) * u128::from(p.height)).sum::<u128>();
let sheet_area = u128::from(s.width) * u128::from(s.height);
let waste_area = sheet_area - used_area;
assert!(sum_sq <= waste_area * waste_area);
}
}