///|
pub fn LayoutConstraints::default() -> LayoutConstraints {
LayoutConstraints::{
min_occupancy_permille: 0,
max_waste_permille: 1000,
min_gap: 0,
max_row_count: 0,
max_column_count: 0,
max_rotated_permille: 1000,
require_all_packed: false,
}
}
///|
pub fn LayoutConstraints::strict() -> LayoutConstraints {
LayoutConstraints::{
min_occupancy_permille: 500,
max_waste_permille: 500,
min_gap: 0,
max_row_count: 0,
max_column_count: 0,
max_rotated_permille: 700,
require_all_packed: true,
}
}
///|
fn make_violation(
name : String,
expected : Int,
actual : Int,
severity : IssueSeverity,
) -> ConstraintViolation {
ConstraintViolation::{ name, expected, actual, severity }
}
///|
fn push_violation(
violations : Array[ConstraintViolation],
name : String,
expected : Int,
actual : Int,
severity : IssueSeverity,
) -> Unit {
violations.push(make_violation(name, expected, actual, severity))
}
///|
fn horizontal_gap(a : Placed, b : Placed) -> Int {
if a.x + a.width <= b.x {
b.x - (a.x + a.width)
} else if b.x + b.width <= a.x {
a.x - (b.x + b.width)
} else {
0
}
}
///|
fn vertical_gap(a : Placed, b : Placed) -> Int {
if a.y + a.height <= b.y {
b.y - (a.y + a.height)
} else if b.y + b.height <= a.y {
a.y - (b.y + b.height)
} else {
0
}
}
///|
fn placement_gap(a : Placed, b : Placed) -> Int {
let x_gap = horizontal_gap(a, b)
let y_gap = vertical_gap(a, b)
if x_gap == 0 {
y_gap
} else if y_gap == 0 {
x_gap
} else {
grid_min(x_gap, y_gap)
}
}
///|
fn min_gap_in_layout(placements : ArrayView[Placed]) -> Int {
if placements.length() < 2 {
return 0
}
let mut best = placement_gap(placements[0], placements[1])
for i in 0.. Unit {
if constraints.require_all_packed && !layout.unpacked.is_empty() {
push_violation(
violations,
"require_all_packed",
0,
layout.unpacked.length(),
Critical,
)
}
}
///|
fn check_occupancy_constraint(
violations : Array[ConstraintViolation],
stats : LayoutStats,
constraints : LayoutConstraints,
) -> Unit {
if constraints.min_occupancy_permille > 0 &&
stats.occupancy_permille < constraints.min_occupancy_permille {
push_violation(
violations,
"min_occupancy_permille",
constraints.min_occupancy_permille,
stats.occupancy_permille,
Warning,
)
}
}
///|
fn check_waste_constraint(
violations : Array[ConstraintViolation],
stats : LayoutStats,
constraints : LayoutConstraints,
) -> Unit {
let waste_permille = scale_permille(stats.waste_area, stats.container_area)
if constraints.max_waste_permille < 1000 &&
waste_permille > constraints.max_waste_permille {
push_violation(
violations,
"max_waste_permille",
constraints.max_waste_permille,
waste_permille,
Warning,
)
}
}
///|
fn check_gap_constraint(
violations : Array[ConstraintViolation],
layout : Layout,
constraints : LayoutConstraints,
) -> Unit {
if constraints.min_gap <= 0 || layout.placements.length() < 2 {
return
}
let actual = min_gap_in_layout(layout.placements)
if actual < constraints.min_gap {
push_violation(violations, "min_gap", constraints.min_gap, actual, Warning)
}
}
///|
fn check_row_column_constraints(
violations : Array[ConstraintViolation],
stats : LayoutStats,
constraints : LayoutConstraints,
) -> Unit {
if constraints.max_row_count > 0 &&
stats.row_count > constraints.max_row_count {
push_violation(
violations,
"max_row_count",
constraints.max_row_count,
stats.row_count,
Warning,
)
}
if constraints.max_column_count > 0 &&
stats.column_count > constraints.max_column_count {
push_violation(
violations,
"max_column_count",
constraints.max_column_count,
stats.column_count,
Warning,
)
}
}
///|
fn check_rotation_constraint(
violations : Array[ConstraintViolation],
stats : LayoutStats,
constraints : LayoutConstraints,
) -> Unit {
if constraints.max_rotated_permille >= 1000 || stats.placed_count == 0 {
return
}
let actual = scale_permille(stats.rotated_count, stats.placed_count)
if actual > constraints.max_rotated_permille {
push_violation(
violations,
"max_rotated_permille",
constraints.max_rotated_permille,
actual,
Warning,
)
}
}
///|
/// Check a layout against caller-supplied production constraints.
pub fn check_constraints(
layout : Layout,
constraints : LayoutConstraints,
) -> Result[Array[ConstraintViolation], PackError] {
match analyze_layout(layout) {
Err(err) => Err(err)
Ok(stats) => {
let violations : Array[ConstraintViolation] = []
check_all_packed(violations, layout, constraints)
check_occupancy_constraint(violations, stats, constraints)
check_waste_constraint(violations, stats, constraints)
check_gap_constraint(violations, layout, constraints)
check_row_column_constraints(violations, stats, constraints)
check_rotation_constraint(violations, stats, constraints)
Ok(violations)
}
}
}
///|
fn constraint_status(violations : ArrayView[ConstraintViolation]) -> String {
if violations.is_empty() {
"pass"
} else {
"fail"
}
}
///|
fn write_violation(
out : StringBuilder,
violation : ConstraintViolation,
) -> Unit {
out.write_string("- [")
out.write_string(severity_name(violation.severity))
out.write_string("] ")
out.write_string(violation.name)
out.write_string(" expected=")
out.write_string("\{violation.expected}")
out.write_string(" actual=")
out.write_string("\{violation.actual}")
out.write_char('\n')
}
///|
/// Human-readable constraint check output.
pub fn to_constraint_report(
violations : ArrayView[ConstraintViolation],
) -> String {
let out = StringBuilder()
out.write_string("rectloom constraint report\n")
out.write_string("status=")
out.write_string(constraint_status(violations))
out.write_string(" violations=")
out.write_string("\{violations.length()}")
out.write_char('\n')
for violation in violations {
write_violation(out, violation)
}
out.to_string()
}
///|
/// Convenience wrapper for strict production-oriented checks.
pub fn strict_constraint_report(layout : Layout) -> Result[String, PackError] {
match check_constraints(layout, LayoutConstraints::strict()) {
Err(err) => Err(err)
Ok(violations) => Ok(to_constraint_report(violations))
}
}