use egui::{Pos2, Rect, Vec2};
pub const DEFAULT_GAP: f32 = 8.0;
pub fn overlap_area(a: Rect, b: Rect) -> f32 {
let x_overlap = (a.max.x.min(b.max.x) - a.min.x.max(b.min.x)).max(0.0);
let y_overlap = (a.max.y.min(b.max.y) - a.min.y.max(b.min.y)).max(0.0);
x_overlap * y_overlap
}
pub fn rects_overlap(a: Rect, b: Rect) -> bool {
overlap_area(a, b) > 0.0
}
pub fn rects_overlap_with_gap(a: Rect, b: Rect, min_gap: f32) -> bool {
let a_expanded = a.expand(min_gap / 2.0);
let b_expanded = b.expand(min_gap / 2.0);
rects_overlap(a_expanded, b_expanded)
}
pub fn find_overlapping_indices(rect: Rect, others: &[Rect]) -> Vec<usize> {
others
.iter()
.enumerate()
.filter(|(_, other)| rects_overlap(rect, **other))
.map(|(i, _)| i)
.collect()
}
pub fn find_overlapping<'a>(rect: Rect, others: &'a [Rect]) -> Vec<&'a Rect> {
others
.iter()
.filter(|other| rects_overlap(rect, **other))
.collect()
}
pub fn has_any_overlap(rect: Rect, others: &[Rect]) -> bool {
others.iter().any(|other| rects_overlap(rect, *other))
}
pub fn total_overlap_area(rect: Rect, others: &[Rect]) -> f32 {
others.iter().map(|other| overlap_area(rect, *other)).sum()
}
pub fn find_nearest_empty_slot(
size: Vec2,
occupied: &[Rect],
bounds: Option<Rect>,
preferred_pos: Option<Pos2>,
gap: f32,
) -> Pos2 {
let start = preferred_pos.unwrap_or(Pos2::ZERO);
let candidate = Rect::from_min_size(start, size);
if !has_any_overlap_with_gap(candidate, occupied, gap) && fits_in_bounds(candidate, bounds) {
return start;
}
let step = gap + 20.0; let max_distance = 2000.0;
let mut distance = step;
while distance < max_distance {
for angle_step in 0..((distance * 2.0 * std::f32::consts::PI / step) as i32).max(8) {
let angle = (angle_step as f32) * step / distance;
let offset = Vec2::new(angle.cos() * distance, angle.sin() * distance);
let pos = start + offset;
if bounds.is_none() && (pos.x < 0.0 || pos.y < 0.0) {
continue;
}
let candidate = Rect::from_min_size(pos, size);
if !has_any_overlap_with_gap(candidate, occupied, gap)
&& fits_in_bounds(candidate, bounds)
{
return pos;
}
}
distance += step;
}
let fallback_pos = if let Some(bounds) = bounds {
Pos2::new(bounds.min.x + gap, bounds.min.y + gap)
} else {
Pos2::new(gap, gap)
};
fallback_pos
}
pub fn find_empty_slot_grid(
size: Vec2,
occupied: &[Rect],
bounds: Option<Rect>,
preferred_pos: Option<Pos2>,
gap: f32,
grid_size: f32,
) -> Pos2 {
let start =
preferred_pos.unwrap_or_else(|| bounds.map_or(Pos2::ZERO, |b| b.min + Vec2::splat(gap)));
let snapped_start = snap_to_grid(start, grid_size);
let candidate = Rect::from_min_size(snapped_start, size);
if !has_any_overlap_with_gap(candidate, occupied, gap) && fits_in_bounds(candidate, bounds) {
return snapped_start;
}
let search_bounds = bounds.unwrap_or(Rect::from_min_size(Pos2::ZERO, Vec2::splat(2000.0)));
let mut positions: Vec<Pos2> = Vec::new();
let mut y = search_bounds.min.y + gap;
while y + size.y <= search_bounds.max.y - gap {
let mut x = search_bounds.min.x + gap;
while x + size.x <= search_bounds.max.x - gap {
positions.push(snap_to_grid(Pos2::new(x, y), grid_size));
x += grid_size;
}
y += grid_size;
}
positions.sort_by(|a, b| {
let dist_a = (*a - start).length_sq();
let dist_b = (*b - start).length_sq();
dist_a
.partial_cmp(&dist_b)
.unwrap_or(std::cmp::Ordering::Equal)
});
for pos in positions {
let candidate = Rect::from_min_size(pos, size);
if !has_any_overlap_with_gap(candidate, occupied, gap) {
return pos;
}
}
Pos2::new(search_bounds.min.x + gap, search_bounds.min.y + gap)
}
pub fn snap_to_grid(pos: Pos2, grid_size: f32) -> Pos2 {
Pos2::new(
(pos.x / grid_size).round() * grid_size,
(pos.y / grid_size).round() * grid_size,
)
}
fn fits_in_bounds(rect: Rect, bounds: Option<Rect>) -> bool {
match bounds {
Some(b) => b.contains_rect(rect),
None => true,
}
}
fn has_any_overlap_with_gap(rect: Rect, others: &[Rect], gap: f32) -> bool {
let rect_with_gap = rect.expand(gap / 2.0);
others.iter().any(|other| {
let other_with_gap = other.expand(gap / 2.0);
rects_overlap(rect_with_gap, other_with_gap)
})
}
pub fn bounding_box(rects: &[Rect]) -> Option<Rect> {
if rects.is_empty() {
return None;
}
let mut min_x = f32::MAX;
let mut min_y = f32::MAX;
let mut max_x = f32::MIN;
let mut max_y = f32::MIN;
for rect in rects {
min_x = min_x.min(rect.min.x);
min_y = min_y.min(rect.min.y);
max_x = max_x.max(rect.max.x);
max_y = max_y.max(rect.max.y);
}
Some(Rect::from_min_max(
Pos2::new(min_x, min_y),
Pos2::new(max_x, max_y),
))
}
pub fn center_of_mass(rects: &[Rect]) -> Option<Pos2> {
if rects.is_empty() {
return None;
}
let mut total_area = 0.0;
let mut weighted_x = 0.0;
let mut weighted_y = 0.0;
for rect in rects {
let area = rect.width() * rect.height();
weighted_x += rect.center().x * area;
weighted_y += rect.center().y * area;
total_area += area;
}
if total_area > 0.0 {
Some(Pos2::new(weighted_x / total_area, weighted_y / total_area))
} else {
None
}
}
#[derive(Clone, Debug)]
pub struct ResolveResult {
pub positions: Vec<Pos2>,
pub changed: bool,
pub iterations: usize,
}
pub fn resolve_overlaps(rects: &[Rect], gap: f32, max_iterations: usize) -> ResolveResult {
if rects.len() <= 1 {
return ResolveResult {
positions: rects.iter().map(|r| r.min).collect(),
changed: false,
iterations: 0,
};
}
let mut positions: Vec<Pos2> = rects.iter().map(|r| r.min).collect();
let sizes: Vec<Vec2> = rects.iter().map(|r| r.size()).collect();
let mut changed = false;
let mut iterations = 0;
for iter in 0..max_iterations {
iterations = iter + 1;
let mut any_moved = false;
for i in 0..positions.len() {
for j in (i + 1)..positions.len() {
let rect_i = Rect::from_min_size(positions[i], sizes[i]);
let rect_j = Rect::from_min_size(positions[j], sizes[j]);
if rects_overlap_with_gap(rect_i, rect_j, gap) {
let push = calculate_push_vector(rect_i, rect_j, gap);
if push.length_sq() > 0.01 {
positions[i] -= push * 0.5;
positions[j] += push * 0.5;
any_moved = true;
changed = true;
}
}
}
}
if !any_moved {
break;
}
}
ResolveResult {
positions,
changed,
iterations,
}
}
fn calculate_push_vector(a: Rect, b: Rect, gap: f32) -> Vec2 {
let a_center = a.center();
let b_center = b.center();
let direction = b_center - a_center;
let combined_half_width = (a.width() + b.width()) / 2.0 + gap;
let combined_half_height = (a.height() + b.height()) / 2.0 + gap;
let overlap_x = combined_half_width - direction.x.abs();
let overlap_y = combined_half_height - direction.y.abs();
if overlap_x <= 0.0 || overlap_y <= 0.0 {
return Vec2::ZERO;
}
if overlap_x < overlap_y {
let sign = if direction.x >= 0.0 { 1.0 } else { -1.0 };
Vec2::new(overlap_x * sign, 0.0)
} else {
let sign = if direction.y >= 0.0 { 1.0 } else { -1.0 };
Vec2::new(0.0, overlap_y * sign)
}
}
pub fn resolve_overlaps_with_anchors(
rects: &[Rect],
gap: f32,
anchor_strength: f32,
max_iterations: usize,
) -> ResolveResult {
if rects.len() <= 1 {
return ResolveResult {
positions: rects.iter().map(|r| r.min).collect(),
changed: false,
iterations: 0,
};
}
let original_positions: Vec<Pos2> = rects.iter().map(|r| r.min).collect();
let mut positions = original_positions.clone();
let sizes: Vec<Vec2> = rects.iter().map(|r| r.size()).collect();
let mut changed = false;
let mut iterations = 0;
for iter in 0..max_iterations {
iterations = iter + 1;
let mut any_moved = false;
for i in 0..positions.len() {
for j in (i + 1)..positions.len() {
let rect_i = Rect::from_min_size(positions[i], sizes[i]);
let rect_j = Rect::from_min_size(positions[j], sizes[j]);
if rects_overlap_with_gap(rect_i, rect_j, gap) {
let push = calculate_push_vector(rect_i, rect_j, gap);
if push.length_sq() > 0.01 {
positions[i] -= push * 0.5;
positions[j] += push * 0.5;
any_moved = true;
changed = true;
}
}
}
}
for i in 0..positions.len() {
let anchor_force = (original_positions[i] - positions[i]) * anchor_strength;
if anchor_force.length_sq() > 0.01 {
let test_pos = positions[i] + anchor_force;
let test_rect = Rect::from_min_size(test_pos, sizes[i]);
let would_overlap = (0..positions.len()).any(|j| {
if i == j {
return false;
}
let other = Rect::from_min_size(positions[j], sizes[j]);
rects_overlap_with_gap(test_rect, other, gap)
});
if !would_overlap {
positions[i] = test_pos;
}
}
}
if !any_moved {
break;
}
}
ResolveResult {
positions,
changed,
iterations,
}
}
pub fn has_overlaps(rects: &[Rect], gap: f32) -> bool {
for i in 0..rects.len() {
for j in (i + 1)..rects.len() {
if rects_overlap_with_gap(rects[i], rects[j], gap) {
return true;
}
}
}
false
}
pub fn count_overlaps(rects: &[Rect], gap: f32) -> usize {
let mut count = 0;
for i in 0..rects.len() {
for j in (i + 1)..rects.len() {
if rects_overlap_with_gap(rects[i], rects[j], gap) {
count += 1;
}
}
}
count
}
pub fn sort_indices_by_x(rects: &[Rect]) -> Vec<usize> {
let mut indices: Vec<usize> = (0..rects.len()).collect();
indices.sort_by(|&a, &b| {
rects[a]
.center()
.x
.partial_cmp(&rects[b].center().x)
.unwrap_or(std::cmp::Ordering::Equal)
});
indices
}
pub fn sort_indices_by_y(rects: &[Rect]) -> Vec<usize> {
let mut indices: Vec<usize> = (0..rects.len()).collect();
indices.sort_by(|&a, &b| {
rects[a]
.center()
.y
.partial_cmp(&rects[b].center().y)
.unwrap_or(std::cmp::Ordering::Equal)
});
indices
}
pub fn sort_indices_raster(rects: &[Rect], row_threshold: f32) -> Vec<usize> {
let mut indices: Vec<usize> = (0..rects.len()).collect();
indices.sort_by(|&a, &b| {
let ya = rects[a].center().y;
let yb = rects[b].center().y;
let xa = rects[a].center().x;
let xb = rects[b].center().x;
if (ya - yb).abs() > row_threshold {
ya.partial_cmp(&yb).unwrap_or(std::cmp::Ordering::Equal)
} else {
xa.partial_cmp(&xb).unwrap_or(std::cmp::Ordering::Equal)
}
});
indices
}
pub fn sort_indices_diagonal(rects: &[Rect]) -> Vec<usize> {
let mut indices: Vec<usize> = (0..rects.len()).collect();
indices.sort_by(|&a, &b| {
let diag_a = rects[a].center().x + rects[a].center().y;
let diag_b = rects[b].center().x + rects[b].center().y;
diag_a
.partial_cmp(&diag_b)
.unwrap_or(std::cmp::Ordering::Equal)
});
indices
}
#[derive(Clone, Debug)]
pub struct ArrangeResult {
pub positions: Vec<Pos2>,
pub changed: bool,
}
impl From<ResolveResult> for ArrangeResult {
fn from(result: ResolveResult) -> Self {
ArrangeResult {
positions: result.positions,
changed: result.changed,
}
}
}
#[derive(Clone, Debug)]
pub struct TileResult {
pub positions: Vec<Pos2>,
pub sizes: Vec<Vec2>,
pub changed: bool,
}
pub fn arrange_grid(
rects: &[Rect],
columns: Option<usize>,
origin: Pos2,
gap: f32,
) -> ArrangeResult {
if rects.is_empty() {
return ArrangeResult {
positions: Vec::new(),
changed: false,
};
}
let count = rects.len();
let cols = columns.unwrap_or_else(|| match count {
1 => 1,
2 => 2,
3..=4 => 2,
5..=6 => 3,
_ => ((count as f32).sqrt().ceil() as usize).max(2),
});
let max_width = rects.iter().map(|r| r.width()).fold(0.0f32, f32::max);
let max_height = rects.iter().map(|r| r.height()).fold(0.0f32, f32::max);
let cell_width = max_width + gap;
let cell_height = max_height + gap;
let mut positions = Vec::with_capacity(count);
let mut changed = false;
for (i, rect) in rects.iter().enumerate() {
let col = i % cols;
let row = i / cols;
let cell_x = origin.x + (col as f32) * cell_width;
let cell_y = origin.y + (row as f32) * cell_height;
let offset_x = (max_width - rect.width()) / 2.0;
let offset_y = (max_height - rect.height()) / 2.0;
let new_pos = Pos2::new(cell_x + offset_x, cell_y + offset_y);
if new_pos != rect.min {
changed = true;
}
positions.push(new_pos);
}
ArrangeResult { positions, changed }
}
pub fn arrange_grid_proportional(
rects: &[Rect],
columns: Option<usize>,
origin: Pos2,
gap: f32,
) -> ArrangeResult {
if rects.is_empty() {
return ArrangeResult {
positions: Vec::new(),
changed: false,
};
}
let count = rects.len();
let cols = columns.unwrap_or_else(|| match count {
1 => 1,
2 => 2,
3..=4 => 2,
5..=6 => 3,
_ => ((count as f32).sqrt().ceil() as usize).max(2),
});
let avg_height = rects.iter().map(|r| r.height()).sum::<f32>() / count as f32;
let row_threshold = avg_height * 0.5;
let order = sort_indices_raster(rects, row_threshold);
let rows = (count + cols - 1) / cols;
let mut col_widths = vec![0.0f32; cols];
let mut row_heights = vec![0.0f32; rows];
for (grid_pos, &orig_idx) in order.iter().enumerate() {
let col = grid_pos % cols;
let row = grid_pos / cols;
col_widths[col] = col_widths[col].max(rects[orig_idx].width());
row_heights[row] = row_heights[row].max(rects[orig_idx].height());
}
let mut col_starts = vec![origin.x];
for (i, &width) in col_widths.iter().enumerate() {
col_starts.push(col_starts[i] + width + gap);
}
let mut row_starts = vec![origin.y];
for (i, &height) in row_heights.iter().enumerate() {
row_starts.push(row_starts[i] + height + gap);
}
let mut positions = vec![Pos2::ZERO; count];
let mut changed = false;
for (grid_pos, &orig_idx) in order.iter().enumerate() {
let rect = &rects[orig_idx];
let col = grid_pos % cols;
let row = grid_pos / cols;
let cell_width = col_widths[col];
let cell_height = row_heights[row];
let offset_x = (cell_width - rect.width()) / 2.0;
let offset_y = (cell_height - rect.height()) / 2.0;
let new_pos = Pos2::new(col_starts[col] + offset_x, row_starts[row] + offset_y);
if new_pos != rect.min {
changed = true;
}
positions[orig_idx] = new_pos;
}
ArrangeResult { positions, changed }
}
pub fn arrange_cascade(
rects: &[Rect],
origin: Pos2,
offset: Vec2,
custom_order: Option<&[usize]>,
) -> ArrangeResult {
if rects.is_empty() {
return ArrangeResult {
positions: Vec::new(),
changed: false,
};
}
let default_order;
let order: &[usize] = match custom_order {
Some(o) => o,
None => {
default_order = sort_indices_diagonal(rects);
&default_order
}
};
let mut positions = vec![Pos2::ZERO; rects.len()];
let mut changed = false;
for (cascade_pos, &orig_idx) in order.iter().enumerate() {
let rect = &rects[orig_idx];
let new_pos = origin + offset * (cascade_pos as f32);
if new_pos != rect.min {
changed = true;
}
positions[orig_idx] = new_pos;
}
ArrangeResult { positions, changed }
}
pub fn arrange_horizontal(
rects: &[Rect],
origin: Pos2,
gap: f32,
align_top: bool,
) -> ArrangeResult {
if rects.is_empty() {
return ArrangeResult {
positions: Vec::new(),
changed: false,
};
}
let order = sort_indices_by_x(rects);
let max_height = if align_top {
0.0
} else {
rects.iter().map(|r| r.height()).fold(0.0f32, f32::max)
};
let mut positions = vec![Pos2::ZERO; rects.len()];
let mut changed = false;
let mut x = origin.x;
for &i in &order {
let rect = &rects[i];
let y = if align_top {
origin.y
} else {
origin.y + (max_height - rect.height()) / 2.0
};
let new_pos = Pos2::new(x, y);
if new_pos != rect.min {
changed = true;
}
positions[i] = new_pos;
x += rect.width() + gap;
}
ArrangeResult { positions, changed }
}
pub fn arrange_vertical(rects: &[Rect], origin: Pos2, gap: f32, align_left: bool) -> ArrangeResult {
if rects.is_empty() {
return ArrangeResult {
positions: Vec::new(),
changed: false,
};
}
let order = sort_indices_by_y(rects);
let max_width = if align_left {
0.0
} else {
rects.iter().map(|r| r.width()).fold(0.0f32, f32::max)
};
let mut positions = vec![Pos2::ZERO; rects.len()];
let mut changed = false;
let mut y = origin.y;
for &i in &order {
let rect = &rects[i];
let x = if align_left {
origin.x
} else {
origin.x + (max_width - rect.width()) / 2.0
};
let new_pos = Pos2::new(x, y);
if new_pos != rect.min {
changed = true;
}
positions[i] = new_pos;
y += rect.height() + gap;
}
ArrangeResult { positions, changed }
}
pub fn arrange_fit_bounds(rects: &[Rect], bounds: Rect, gap: f32) -> ArrangeResult {
if rects.is_empty() {
return ArrangeResult {
positions: Vec::new(),
changed: false,
};
}
let count = rects.len();
let avg_width: f32 = rects.iter().map(|r| r.width()).sum::<f32>() / count as f32;
let available_width = bounds.width() - gap;
let cols = ((available_width + gap) / (avg_width + gap)).floor() as usize;
let cols = cols.max(1).min(count);
arrange_grid_proportional(rects, Some(cols), bounds.min + Vec2::splat(gap / 2.0), gap)
}
pub fn arrange_tile(
rects: &[Rect],
bounds: Rect,
gap: f32,
min_sizes: Option<&[Vec2]>,
) -> TileResult {
let count = rects.len();
if count == 0 {
return TileResult {
positions: Vec::new(),
sizes: Vec::new(),
changed: false,
};
}
let avg_height = rects.iter().map(|r| r.height()).sum::<f32>() / count as f32;
let order = sort_indices_raster(rects, avg_height * 0.5);
let mut positions = vec![Pos2::ZERO; count];
let mut sizes = vec![Vec2::ZERO; count];
let mut changed = false;
let available_width = bounds.width() - gap;
let available_height = bounds.height() - gap;
match count {
1 => {
let orig_idx = order[0];
let new_pos = bounds.min + Vec2::splat(gap / 2.0);
let new_size = Vec2::new(available_width, available_height);
let new_size = apply_min_size(new_size, min_sizes, orig_idx);
positions[orig_idx] = new_pos;
sizes[orig_idx] = new_size;
changed = check_changed(&rects[orig_idx], new_pos, new_size);
}
2 => {
let half_width = (available_width - gap) / 2.0;
for (tile_idx, &orig_idx) in order.iter().enumerate() {
let x = bounds.min.x + gap / 2.0 + (tile_idx as f32) * (half_width + gap);
let new_pos = Pos2::new(x, bounds.min.y + gap / 2.0);
let new_size = Vec2::new(half_width, available_height);
let new_size = apply_min_size(new_size, min_sizes, orig_idx);
positions[orig_idx] = new_pos;
sizes[orig_idx] = new_size;
if check_changed(&rects[orig_idx], new_pos, new_size) {
changed = true;
}
}
}
3 => {
let left_width = (available_width - gap) / 2.0;
let right_width = left_width;
let right_height = (available_height - gap) / 2.0;
let orig_idx = order[0];
let new_pos = Pos2::new(bounds.min.x + gap / 2.0, bounds.min.y + gap / 2.0);
let new_size = Vec2::new(left_width, available_height);
let new_size = apply_min_size(new_size, min_sizes, orig_idx);
positions[orig_idx] = new_pos;
sizes[orig_idx] = new_size;
if check_changed(&rects[orig_idx], new_pos, new_size) {
changed = true;
}
let orig_idx = order[1];
let new_pos = Pos2::new(
bounds.min.x + gap / 2.0 + left_width + gap,
bounds.min.y + gap / 2.0,
);
let new_size = Vec2::new(right_width, right_height);
let new_size = apply_min_size(new_size, min_sizes, orig_idx);
positions[orig_idx] = new_pos;
sizes[orig_idx] = new_size;
if check_changed(&rects[orig_idx], new_pos, new_size) {
changed = true;
}
let orig_idx = order[2];
let new_pos = Pos2::new(
bounds.min.x + gap / 2.0 + left_width + gap,
bounds.min.y + gap / 2.0 + right_height + gap,
);
let new_size = Vec2::new(right_width, right_height);
let new_size = apply_min_size(new_size, min_sizes, orig_idx);
positions[orig_idx] = new_pos;
sizes[orig_idx] = new_size;
if check_changed(&rects[orig_idx], new_pos, new_size) {
changed = true;
}
}
4 => {
let cell_width = (available_width - gap) / 2.0;
let cell_height = (available_height - gap) / 2.0;
for (tile_idx, &orig_idx) in order.iter().enumerate() {
let col = tile_idx % 2;
let row = tile_idx / 2;
let x = bounds.min.x + gap / 2.0 + (col as f32) * (cell_width + gap);
let y = bounds.min.y + gap / 2.0 + (row as f32) * (cell_height + gap);
let new_pos = Pos2::new(x, y);
let new_size = Vec2::new(cell_width, cell_height);
let new_size = apply_min_size(new_size, min_sizes, orig_idx);
positions[orig_idx] = new_pos;
sizes[orig_idx] = new_size;
if check_changed(&rects[orig_idx], new_pos, new_size) {
changed = true;
}
}
}
_ => {
let (cols, rows) = calculate_grid_dimensions(count);
let cell_width = (available_width - gap * (cols as f32 - 1.0)) / cols as f32;
let cell_height = (available_height - gap * (rows as f32 - 1.0)) / rows as f32;
for (tile_idx, &orig_idx) in order.iter().enumerate() {
let col = tile_idx % cols;
let row = tile_idx / cols;
let x = bounds.min.x + gap / 2.0 + (col as f32) * (cell_width + gap);
let y = bounds.min.y + gap / 2.0 + (row as f32) * (cell_height + gap);
let new_pos = Pos2::new(x, y);
let new_size = Vec2::new(cell_width, cell_height);
let new_size = apply_min_size(new_size, min_sizes, orig_idx);
positions[orig_idx] = new_pos;
sizes[orig_idx] = new_size;
if check_changed(&rects[orig_idx], new_pos, new_size) {
changed = true;
}
}
}
}
TileResult {
positions,
sizes,
changed,
}
}
fn calculate_grid_dimensions(count: usize) -> (usize, usize) {
match count {
0 => (0, 0),
1 => (1, 1),
2 => (2, 1),
3 => (3, 1),
4 => (2, 2),
5 => (3, 2),
6 => (3, 2),
7..=9 => (3, 3),
10..=12 => (4, 3),
_ => {
let cols = (count as f32).sqrt().ceil() as usize;
let rows = (count + cols - 1) / cols;
(cols, rows)
}
}
}
fn apply_min_size(size: Vec2, min_sizes: Option<&[Vec2]>, idx: usize) -> Vec2 {
match min_sizes {
Some(mins) if idx < mins.len() => {
Vec2::new(size.x.max(mins[idx].x), size.y.max(mins[idx].y))
}
_ => size,
}
}
fn check_changed(rect: &Rect, new_pos: Pos2, new_size: Vec2) -> bool {
rect.min != new_pos || rect.size() != new_size
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_overlap_area() {
let a = Rect::from_min_size(Pos2::new(0.0, 0.0), Vec2::new(10.0, 10.0));
let b = Rect::from_min_size(Pos2::new(20.0, 0.0), Vec2::new(10.0, 10.0));
assert_eq!(overlap_area(a, b), 0.0);
assert_eq!(overlap_area(a, a), 100.0);
let c = Rect::from_min_size(Pos2::new(5.0, 5.0), Vec2::new(10.0, 10.0));
assert_eq!(overlap_area(a, c), 25.0); }
#[test]
fn test_rects_overlap() {
let a = Rect::from_min_size(Pos2::new(0.0, 0.0), Vec2::new(10.0, 10.0));
let b = Rect::from_min_size(Pos2::new(20.0, 0.0), Vec2::new(10.0, 10.0));
let c = Rect::from_min_size(Pos2::new(5.0, 5.0), Vec2::new(10.0, 10.0));
assert!(!rects_overlap(a, b));
assert!(rects_overlap(a, c));
}
#[test]
fn test_find_overlapping() {
let target = Rect::from_min_size(Pos2::new(0.0, 0.0), Vec2::new(10.0, 10.0));
let others = vec![
Rect::from_min_size(Pos2::new(5.0, 5.0), Vec2::new(10.0, 10.0)), Rect::from_min_size(Pos2::new(20.0, 0.0), Vec2::new(10.0, 10.0)), Rect::from_min_size(Pos2::new(0.0, 5.0), Vec2::new(5.0, 10.0)), ];
let overlapping = find_overlapping(target, &others);
assert_eq!(overlapping.len(), 2);
}
#[test]
fn test_find_nearest_empty_slot() {
let size = Vec2::new(100.0, 100.0);
let occupied = vec![Rect::from_min_size(
Pos2::new(0.0, 0.0),
Vec2::new(100.0, 100.0),
)];
let slot = find_nearest_empty_slot(size, &occupied, None, Some(Pos2::ZERO), DEFAULT_GAP);
let new_rect = Rect::from_min_size(slot, size);
assert!(!has_any_overlap(new_rect, &occupied));
}
#[test]
fn test_bounding_box() {
let rects = vec![
Rect::from_min_size(Pos2::new(0.0, 0.0), Vec2::new(10.0, 10.0)),
Rect::from_min_size(Pos2::new(20.0, 20.0), Vec2::new(10.0, 10.0)),
];
let bbox = bounding_box(&rects).unwrap();
assert_eq!(bbox.min, Pos2::new(0.0, 0.0));
assert_eq!(bbox.max, Pos2::new(30.0, 30.0));
}
#[test]
fn test_has_overlaps() {
let rects = vec![
Rect::from_min_size(Pos2::new(0.0, 0.0), Vec2::new(10.0, 10.0)),
Rect::from_min_size(Pos2::new(20.0, 0.0), Vec2::new(10.0, 10.0)),
];
assert!(!has_overlaps(&rects, 0.0));
let rects_overlapping = vec![
Rect::from_min_size(Pos2::new(0.0, 0.0), Vec2::new(10.0, 10.0)),
Rect::from_min_size(Pos2::new(5.0, 5.0), Vec2::new(10.0, 10.0)),
];
assert!(has_overlaps(&rects_overlapping, 0.0));
}
#[test]
fn test_resolve_overlaps_no_overlap() {
let rects = vec![
Rect::from_min_size(Pos2::new(0.0, 0.0), Vec2::new(10.0, 10.0)),
Rect::from_min_size(Pos2::new(20.0, 0.0), Vec2::new(10.0, 10.0)),
];
let result = resolve_overlaps(&rects, DEFAULT_GAP, 100);
assert!(!result.changed);
}
#[test]
fn test_resolve_overlaps_with_overlap() {
let rects = vec![
Rect::from_min_size(Pos2::new(0.0, 0.0), Vec2::new(100.0, 100.0)),
Rect::from_min_size(Pos2::new(50.0, 50.0), Vec2::new(100.0, 100.0)),
];
let result = resolve_overlaps(&rects, DEFAULT_GAP, 100);
assert!(result.changed);
let resolved_rects: Vec<Rect> = result
.positions
.iter()
.zip(rects.iter())
.map(|(pos, r)| Rect::from_min_size(*pos, r.size()))
.collect();
assert!(!has_overlaps(&resolved_rects, DEFAULT_GAP));
}
#[test]
fn test_resolve_overlaps_multiple() {
let rects = vec![
Rect::from_min_size(Pos2::new(0.0, 0.0), Vec2::new(100.0, 100.0)),
Rect::from_min_size(Pos2::new(50.0, 0.0), Vec2::new(100.0, 100.0)),
Rect::from_min_size(Pos2::new(25.0, 50.0), Vec2::new(100.0, 100.0)),
];
let result = resolve_overlaps(&rects, DEFAULT_GAP, 100);
assert!(result.changed);
let resolved_rects: Vec<Rect> = result
.positions
.iter()
.zip(rects.iter())
.map(|(pos, r)| Rect::from_min_size(*pos, r.size()))
.collect();
assert!(!has_overlaps(&resolved_rects, DEFAULT_GAP));
}
#[test]
fn test_count_overlaps() {
let rects = vec![
Rect::from_min_size(Pos2::new(0.0, 0.0), Vec2::new(100.0, 100.0)),
Rect::from_min_size(Pos2::new(50.0, 0.0), Vec2::new(100.0, 100.0)),
Rect::from_min_size(Pos2::new(200.0, 0.0), Vec2::new(100.0, 100.0)), ];
assert_eq!(count_overlaps(&rects, 0.0), 1);
}
#[test]
fn test_arrange_grid() {
let rects = vec![
Rect::from_min_size(Pos2::new(0.0, 0.0), Vec2::new(100.0, 100.0)),
Rect::from_min_size(Pos2::new(0.0, 0.0), Vec2::new(100.0, 100.0)),
Rect::from_min_size(Pos2::new(0.0, 0.0), Vec2::new(100.0, 100.0)),
Rect::from_min_size(Pos2::new(0.0, 0.0), Vec2::new(100.0, 100.0)),
];
let result = arrange_grid(&rects, Some(2), Pos2::ZERO, DEFAULT_GAP);
assert!(result.changed);
assert_eq!(result.positions.len(), 4);
assert_eq!(result.positions[0].x, 0.0);
assert_eq!(result.positions[1].x, 100.0 + DEFAULT_GAP);
assert_eq!(result.positions[2].y, 100.0 + DEFAULT_GAP);
assert_eq!(result.positions[3].y, 100.0 + DEFAULT_GAP);
let arranged: Vec<Rect> = result
.positions
.iter()
.zip(rects.iter())
.map(|(pos, r)| Rect::from_min_size(*pos, r.size()))
.collect();
assert!(!has_overlaps(&arranged, DEFAULT_GAP));
}
#[test]
fn test_arrange_grid_proportional() {
let rects = vec![
Rect::from_min_size(Pos2::new(0.0, 0.0), Vec2::new(100.0, 50.0)),
Rect::from_min_size(Pos2::new(0.0, 0.0), Vec2::new(80.0, 100.0)),
Rect::from_min_size(Pos2::new(0.0, 0.0), Vec2::new(120.0, 80.0)),
];
let result = arrange_grid_proportional(&rects, Some(2), Pos2::ZERO, DEFAULT_GAP);
assert!(result.changed);
let arranged: Vec<Rect> = result
.positions
.iter()
.zip(rects.iter())
.map(|(pos, r)| Rect::from_min_size(*pos, r.size()))
.collect();
assert!(!has_overlaps(&arranged, DEFAULT_GAP));
}
#[test]
fn test_arrange_horizontal() {
let rects = vec![
Rect::from_min_size(Pos2::new(50.0, 0.0), Vec2::new(100.0, 50.0)), Rect::from_min_size(Pos2::new(200.0, 0.0), Vec2::new(80.0, 100.0)), Rect::from_min_size(Pos2::new(100.0, 0.0), Vec2::new(60.0, 70.0)), ];
let result = arrange_horizontal(&rects, Pos2::new(10.0, 10.0), DEFAULT_GAP, false);
assert!(result.changed);
assert_eq!(result.positions[0].x, 10.0);
assert_eq!(result.positions[2].x, 10.0 + 100.0 + DEFAULT_GAP);
assert_eq!(
result.positions[1].x,
10.0 + 100.0 + DEFAULT_GAP + 60.0 + DEFAULT_GAP
);
let arranged: Vec<Rect> = result
.positions
.iter()
.zip(rects.iter())
.map(|(pos, r)| Rect::from_min_size(*pos, r.size()))
.collect();
assert!(!has_overlaps(&arranged, DEFAULT_GAP));
}
#[test]
fn test_arrange_vertical() {
let rects = vec![
Rect::from_min_size(Pos2::new(0.0, 20.0), Vec2::new(100.0, 50.0)), Rect::from_min_size(Pos2::new(0.0, 200.0), Vec2::new(80.0, 100.0)), Rect::from_min_size(Pos2::new(0.0, 80.0), Vec2::new(60.0, 70.0)), ];
let result = arrange_vertical(&rects, Pos2::new(10.0, 10.0), DEFAULT_GAP, false);
assert!(result.changed);
assert_eq!(result.positions[0].y, 10.0);
assert_eq!(result.positions[2].y, 10.0 + 50.0 + DEFAULT_GAP);
assert_eq!(
result.positions[1].y,
10.0 + 50.0 + DEFAULT_GAP + 70.0 + DEFAULT_GAP
);
let arranged: Vec<Rect> = result
.positions
.iter()
.zip(rects.iter())
.map(|(pos, r)| Rect::from_min_size(*pos, r.size()))
.collect();
assert!(!has_overlaps(&arranged, DEFAULT_GAP));
}
#[test]
fn test_arrange_cascade() {
let rects = vec![
Rect::from_min_size(Pos2::new(0.0, 0.0), Vec2::new(200.0, 150.0)),
Rect::from_min_size(Pos2::new(0.0, 0.0), Vec2::new(200.0, 150.0)),
Rect::from_min_size(Pos2::new(0.0, 0.0), Vec2::new(200.0, 150.0)),
];
let offset = Vec2::new(30.0, 30.0);
let order = vec![0, 1, 2];
let result = arrange_cascade(&rects, Pos2::new(10.0, 10.0), offset, Some(&order));
assert!(result.changed);
assert_eq!(result.positions[0], Pos2::new(10.0, 10.0));
assert_eq!(result.positions[1], Pos2::new(40.0, 40.0));
assert_eq!(result.positions[2], Pos2::new(70.0, 70.0));
}
#[test]
fn test_arrange_cascade_custom_order() {
let rects = vec![
Rect::from_min_size(Pos2::new(100.0, 100.0), Vec2::new(200.0, 150.0)),
Rect::from_min_size(Pos2::new(50.0, 50.0), Vec2::new(200.0, 150.0)),
Rect::from_min_size(Pos2::new(150.0, 150.0), Vec2::new(200.0, 150.0)),
];
let offset = Vec2::new(30.0, 30.0);
let order = vec![2, 0, 1];
let result = arrange_cascade(&rects, Pos2::new(10.0, 10.0), offset, Some(&order));
assert!(result.changed);
assert_eq!(result.positions[2], Pos2::new(10.0, 10.0));
assert_eq!(result.positions[0], Pos2::new(40.0, 40.0));
assert_eq!(result.positions[1], Pos2::new(70.0, 70.0));
}
}