use crate::geometry::primitives::Point;
use crate::geometry::shapes::Rectangle;
use crate::plotting::regions::RegionPiece;
mod boxes;
mod discs;
mod relax;
mod scan;
pub use boxes::{GlyphBoxOptions, GlyphBoxPlacements, place_glyph_boxes};
pub use discs::{GlyphOptions, GlyphPlacements, place_glyphs};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum GlyphArrangement {
#[default]
Uniform,
Random,
}
pub(super) const OBSTACLE_SHRINK_FLOOR: f64 = 0.5;
pub(super) const PROBE: PackMode = PackMode {
spread: false,
strict_obstacles: true,
};
pub(super) const PACK: PackMode = PackMode {
spread: true,
strict_obstacles: false,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct PackMode {
pub(super) spread: bool,
pub(super) strict_obstacles: bool,
}
pub(super) fn apportion(n: usize, weights: &[f64]) -> Vec<usize> {
let total: f64 = weights.iter().sum();
if weights.is_empty() {
return Vec::new();
}
if total.is_nan() || total <= 0.0 {
let mut out = vec![0; weights.len()];
out[0] = n;
return out;
}
let mut base = Vec::with_capacity(weights.len());
let mut rems = Vec::with_capacity(weights.len());
for w in weights {
let share = n as f64 * (w / total).max(0.0);
let floor = share.floor();
base.push(floor as usize);
rems.push(share - floor);
}
let assigned: usize = base.iter().sum();
let mut order: Vec<usize> = (0..weights.len()).collect();
order.sort_by(|&a, &b| rems[b].total_cmp(&rems[a]).then(a.cmp(&b)));
for &i in order.iter().take(n.saturating_sub(assigned)) {
base[i] += 1;
}
base
}
pub(super) fn fnv1a(bytes: &[u8]) -> u64 {
let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
for &b in bytes {
hash ^= u64::from(b);
hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
}
hash
}
pub(super) fn dist_to_rect(px: f64, py: f64, rect: &Rectangle) -> f64 {
let dx = (px - rect.center().x()).abs() - 0.5 * rect.width();
let dy = (py - rect.center().y()).abs() - 0.5 * rect.height();
dx.max(0.0).hypot(dy.max(0.0))
}
pub(super) fn clear_of_obstacles(
px: f64,
py: f64,
obstacles: &[Rectangle],
clearance: f64,
) -> bool {
obstacles
.iter()
.all(|rect| dist_to_rect(px, py, rect) >= clearance)
}
pub(super) fn box_rect_separation(cx: f64, cy: f64, w: f64, h: f64, rect: &Rectangle) -> f64 {
let dx = (cx - rect.center().x()).abs() - 0.5 * w - 0.5 * rect.width();
let dy = (cy - rect.center().y()).abs() - 0.5 * h - 0.5 * rect.height();
dx.max(0.0).hypot(dy.max(0.0))
}
pub(super) fn box_clear_of_obstacles(
cx: f64,
cy: f64,
w: f64,
h: f64,
obstacles: &[Rectangle],
clearance: f64,
) -> bool {
obstacles
.iter()
.all(|rect| box_rect_separation(cx, cy, w, h, rect) >= clearance)
}
pub(super) fn sanitize_obstacles(obstacles: &[Rectangle]) -> Vec<Rectangle> {
obstacles
.iter()
.filter(|rect| {
rect.center().x().is_finite()
&& rect.center().y().is_finite()
&& rect.width() > 0.0
&& rect.height() > 0.0
&& rect.width().is_finite()
&& rect.height().is_finite()
})
.copied()
.collect()
}
pub(super) fn obstacles_near(
piece: &RegionPiece,
obstacles: &[Rectangle],
clearance: f64,
) -> Vec<Rectangle> {
if obstacles.is_empty() {
return Vec::new();
}
let Some((min_x, max_x, min_y, max_y)) = ring_bounds(piece.outer.vertices()) else {
return Vec::new();
};
obstacles
.iter()
.filter(|rect| {
let half_w = 0.5 * rect.width() + clearance;
let half_h = 0.5 * rect.height() + clearance;
let (cx, cy) = (rect.center().x(), rect.center().y());
cx + half_w >= min_x
&& cx - half_w <= max_x
&& cy + half_h >= min_y
&& cy - half_h <= max_y
})
.copied()
.collect()
}
pub(super) fn ring_bounds(ring: &[Point]) -> Option<(f64, f64, f64, f64)> {
if ring.len() < 3 {
return None;
}
let mut min_x = f64::INFINITY;
let mut max_x = f64::NEG_INFINITY;
let mut min_y = f64::INFINITY;
let mut max_y = f64::NEG_INFINITY;
for p in ring {
min_x = min_x.min(p.x());
max_x = max_x.max(p.x());
min_y = min_y.min(p.y());
max_y = max_y.max(p.y());
}
(max_x > min_x && max_y > min_y).then_some((min_x, max_x, min_y, max_y))
}
#[cfg(test)]
mod test_utils {
use std::collections::HashMap;
use crate::geometry::primitives::Point;
use crate::geometry::shapes::{Circle, Polygon};
use crate::plotting::regions::signed_clearance;
use crate::plotting::regions::{RegionPiece, RegionPolygons, classify_into_pieces};
use crate::spec::Combination;
use crate::{DiagramSpecBuilder, Fitter, InputType};
pub(super) fn rect_ring(x0: f64, y0: f64, x1: f64, y1: f64) -> Polygon {
Polygon::new(vec![
Point::new(x0, y0),
Point::new(x1, y0),
Point::new(x1, y1),
Point::new(x0, y1),
])
}
pub(super) fn region_clearance(p: &Point, pieces: &[RegionPiece]) -> f64 {
pieces
.iter()
.map(|piece| signed_clearance(p.x(), p.y(), piece))
.fold(f64::NEG_INFINITY, f64::max)
}
pub(super) fn two_set_regions() -> RegionPolygons {
let spec = DiagramSpecBuilder::new()
.set("A", 5.0)
.set("B", 3.0)
.intersection(&["A", "B"], 1.0)
.input_type(InputType::Exclusive)
.build()
.unwrap();
let layout = Fitter::<Circle>::new(&spec).seed(42).fit().unwrap();
layout.region_polygons(&spec, 64)
}
pub(super) fn boxed_region(x0: f64, y0: f64, x1: f64, y1: f64) -> RegionPolygons {
let mut map = HashMap::new();
map.insert(
Combination::new(&["A"]),
classify_into_pieces(vec![rect_ring(x0, y0, x1, y1)]),
);
RegionPolygons::from_map(map)
}
}