use std::collections::HashMap;
use super::{
ARROWHEAD_AREA_RATIO, ARROWHEAD_MAX_SIDE_RATIO, CAPTION_CEILING, CAPTION_REACH, CONCENTRIC_AREA_RATIO,
CONTAINER_MIN_MEMBERS, FRAME_MIN_FLUSH_EDGES, GRID_CLUSTER_RATIO, GRID_MIN_CELLS, GRIDLINE_AXIS_EPSILON,
GRIDLINE_MIN_COUNT, GRIDLINE_SPACING_TOLERANCE_RATIO,
};
use super::{Connector, Label, Outline, Rect};
pub(super) fn median_area(areas: impl Iterator<Item = f32>) -> Option<f32> {
let mut sorted: Vec<f32> = areas.filter(|a| a.is_finite()).collect();
sorted.sort_by(f32::total_cmp);
sorted.get(sorted.len() / 2).copied()
}
pub(super) fn holds_a_grid(bbox: &Rect, texts: &[&Label]) -> bool {
if texts.len() < GRID_MIN_CELLS {
return false;
}
let rows = cluster_count(texts.iter().map(|t| t.y), bbox.height() * GRID_CLUSTER_RATIO);
let columns = cluster_count(texts.iter().map(|t| t.x), bbox.width() * GRID_CLUSTER_RATIO);
rows > 1 && columns > 1
}
pub(super) fn cluster_count(values: impl Iterator<Item = f32>, tolerance: f32) -> usize {
let mut sorted: Vec<f32> = values.filter(|v| v.is_finite()).collect();
sorted.sort_by(f32::total_cmp);
let mut clusters = 0;
let mut current = f32::NEG_INFINITY;
for value in sorted {
if clusters == 0 || value - current > tolerance.max(f32::EPSILON) {
clusters += 1;
}
current = value;
}
clusters
}
pub(super) fn find_containers(
outlines: &[Outline],
arrowheads: &[bool],
owned: &[Vec<&Label>],
landings: &[Vec<(f32, f32)>],
snap: f32,
) -> Vec<bool> {
outlines
.iter()
.enumerate()
.map(|(i, outer)| {
let members = outlines
.iter()
.enumerate()
.filter(|(j, inner)| *j != i && !arrowheads[*j] && outer.bbox.encloses(&inner.bbox));
if owned[i].is_empty() {
return members.count() >= CONTAINER_MIN_MEMBERS;
}
members
.filter(|(j, _)| {
landings
.get(*j)
.is_some_and(|points| points.iter().any(|(x, y)| outer.bbox.depth_of(*x, *y) > snap))
})
.take(CONTAINER_MIN_MEMBERS)
.count()
>= CONTAINER_MIN_MEMBERS
})
.collect()
}
pub(super) fn collapse_concentric(outlines: &mut Vec<Outline>) {
let mut inner = vec![false; outlines.len()];
let mut merged: Vec<(usize, Outline)> = Vec::new();
for (i, a) in outlines.iter().enumerate() {
for (j, b) in outlines.iter().enumerate() {
if i == j || inner[j] {
continue;
}
let area = a.bbox.area();
if area <= 0.0 || area >= b.bbox.area() {
continue;
}
if b.bbox.encloses(&a.bbox) && area >= b.bbox.area() * CONCENTRIC_AREA_RATIO {
inner[i] = true;
let mut outer = b.clone();
outer.fill = outer.fill.or_else(|| a.fill.clone());
outer.stroke = outer.stroke.or_else(|| a.stroke.clone());
outer.stroke_width = outer.stroke_width.or(a.stroke_width);
outer.dashed |= a.dashed;
merged.push((j, outer));
break;
}
}
}
for (index, outline) in merged {
outlines[index] = outline;
}
let mut keep = inner.iter().map(|i| !i);
outlines.retain(|_| keep.next().unwrap_or(true));
}
pub(super) fn find_arrowheads(
outlines: &[Outline],
connectors: &[Connector],
label_owners: &[Option<usize>],
snap: f32,
canvas_max: f32,
) -> Vec<bool> {
let max_side = canvas_max * ARROWHEAD_MAX_SIDE_RATIO;
let mut marked = vec![false; outlines.len()];
let Some(largest) = outlines
.iter()
.map(|o| o.bbox.area())
.max_by(|a, b| a.total_cmp(b))
.filter(|a| *a > 0.0)
else {
return marked;
};
let mut labelled = vec![false; outlines.len()];
for owner in label_owners.iter().flatten() {
labelled[*owner] = true;
}
for (index, outline) in outlines.iter().enumerate() {
if outline.bbox.area() > largest * ARROWHEAD_AREA_RATIO {
continue;
}
if outline.bbox.width() > max_side || outline.bbox.height() > max_side {
continue;
}
if labelled[index] {
continue;
}
let touches = connectors.iter().any(|c| {
outline.bbox.distance_to(c.start.0, c.start.1) <= snap || outline.bbox.distance_to(c.end.0, c.end.1) <= snap
});
marked[index] = touches;
}
marked
}
pub(super) fn find_gridlines(connectors: &[Connector], snap: f32) -> Vec<bool> {
let mut marked = vec![false; connectors.len()];
for horizontal in [true, false] {
let mut groups: HashMap<(String, bool), Vec<usize>> = HashMap::new();
for (index, connector) in connectors.iter().enumerate() {
let axis_aligned = if horizontal {
(connector.start.1 - connector.end.1).abs() <= GRIDLINE_AXIS_EPSILON
} else {
(connector.start.0 - connector.end.0).abs() <= GRIDLINE_AXIS_EPSILON
};
if !axis_aligned {
continue;
}
let key = (connector.stroke.clone().unwrap_or_default(), connector.dashed);
groups.entry(key).or_default().push(index);
}
for indices in groups.values() {
if indices.len() >= GRIDLINE_MIN_COUNT && is_gridline_family(connectors, indices, horizontal, snap) {
for &index in indices {
marked[index] = true;
}
}
}
}
marked
}
pub(super) fn is_gridline_family(connectors: &[Connector], indices: &[usize], horizontal: bool, snap: f32) -> bool {
let mut axis_positions = Vec::with_capacity(indices.len());
let mut run_starts = Vec::with_capacity(indices.len());
let mut run_ends = Vec::with_capacity(indices.len());
for &index in indices {
let connector = &connectors[index];
let (axis, run_a, run_b) = if horizontal {
(
(connector.start.1 + connector.end.1) / 2.0,
connector.start.0,
connector.end.0,
)
} else {
(
(connector.start.0 + connector.end.0) / 2.0,
connector.start.1,
connector.end.1,
)
};
axis_positions.push(axis);
run_starts.push(run_a.min(run_b));
run_ends.push(run_a.max(run_b));
}
if spread(run_starts.iter().copied()) > snap || spread(run_ends.iter().copied()) > snap {
return false;
}
let mut sorted = axis_positions;
sorted.sort_by(f32::total_cmp);
let gaps: Vec<f32> = sorted.windows(2).map(|pair| pair[1] - pair[0]).collect();
if gaps.iter().any(|gap| *gap <= snap) {
return false;
}
let average = gaps.iter().sum::<f32>() / gaps.len() as f32;
gaps.iter()
.all(|gap| (gap - average).abs() <= average * GRIDLINE_SPACING_TOLERANCE_RATIO)
}
pub(super) fn find_frame_lines(outlines: &[Outline], connectors: &[Connector], snap: f32) -> Vec<bool> {
connectors
.iter()
.map(|connector| is_frame_line(outlines, connector, snap))
.collect()
}
pub(super) fn is_frame_line(outlines: &[Outline], connector: &Connector, snap: f32) -> bool {
let horizontal = (connector.start.1 - connector.end.1).abs() <= GRIDLINE_AXIS_EPSILON;
let vertical = (connector.start.0 - connector.end.0).abs() <= GRIDLINE_AXIS_EPSILON;
if !horizontal && !vertical {
return false;
}
let (axis, run_min, run_max) = if horizontal {
(
(connector.start.1 + connector.end.1) / 2.0,
connector.start.0.min(connector.end.0),
connector.start.0.max(connector.end.0),
)
} else {
(
(connector.start.0 + connector.end.0) / 2.0,
connector.start.1.min(connector.end.1),
connector.start.1.max(connector.end.1),
)
};
let flush_edges = outlines
.iter()
.filter(|outline| {
let (near, far, span_min, span_max) = if horizontal {
(outline.bbox.y0, outline.bbox.y1, outline.bbox.x0, outline.bbox.x1)
} else {
(outline.bbox.x0, outline.bbox.x1, outline.bbox.y0, outline.bbox.y1)
};
let on_boundary = (near - axis).abs() <= snap || (far - axis).abs() <= snap;
let overlap = (span_max.min(run_max) - span_min.max(run_min)).max(0.0);
on_boundary && overlap > snap
})
.count();
flush_edges >= FRAME_MIN_FLUSH_EDGES
}
pub(super) fn spread(values: impl Iterator<Item = f32>) -> f32 {
let (mut min, mut max) = (f32::INFINITY, f32::NEG_INFINITY);
for value in values {
min = min.min(value);
max = max.max(value);
}
if min.is_finite() && max.is_finite() {
max - min
} else {
0.0
}
}
pub(super) fn touching_arrowhead(arrowheads: &[Rect], point: (f32, f32), snap: f32) -> Option<f32> {
arrowheads
.iter()
.map(|bbox| (bbox.distance_to(point.0, point.1), bbox))
.filter(|(distance, _)| *distance <= snap)
.min_by(|a, b| a.0.total_cmp(&b.0))
.map(|(_, bbox)| bbox.width().hypot(bbox.height()))
}
pub(super) fn snap_to_outline(outlines: &[&Outline], point: (f32, f32), tolerance: f32) -> Option<usize> {
let (x, y) = point;
if !x.is_finite() || !y.is_finite() {
return None;
}
outlines
.iter()
.enumerate()
.map(|(i, o)| (i, o.bbox.distance_to(x, y), o.bbox.area()))
.filter(|(_, distance, _)| *distance <= tolerance)
.min_by(|a, b| a.1.total_cmp(&b.1).then(a.2.total_cmp(&b.2)))
.map(|(i, _, _)| i)
}
pub(super) fn adopt_captions(
outlines: &[Outline],
labels: &[Label],
arrowheads: &[bool],
connectors: &[Connector],
owners: &mut [Option<usize>],
snap: f32,
) {
let mut labelled = vec![false; outlines.len()];
for owner in owners.iter().flatten() {
labelled[*owner] = true;
}
let reach = (snap * CAPTION_REACH).min(CAPTION_CEILING);
for (label, owner) in labels.iter().zip(owners.iter_mut()) {
if owner.is_some() {
continue;
}
let Some((index, gap)) = outlines
.iter()
.enumerate()
.filter(|(i, outline)| {
!labelled[*i]
&& !arrowheads[*i]
&& label.x >= outline.bbox.x0
&& label.x <= outline.bbox.x1
&& label.y > outline.bbox.y1
&& label.y - outline.bbox.y1 <= reach
})
.min_by(|(_, a), (_, b)| {
(label.y - a.bbox.y1).total_cmp(&(label.y - b.bbox.y1)).then(
(label.x - a.bbox.centre_x())
.abs()
.total_cmp(&(label.x - b.bbox.centre_x()).abs()),
)
})
.map(|(i, outline)| (i, label.y - outline.bbox.y1))
else {
continue;
};
if connectors.iter().any(|connector| {
let dx = label.x - connector.midpoint.0;
let dy = label.y - connector.midpoint.1;
dx.hypot(dy) < gap
}) {
continue;
}
*owner = Some(index);
}
}
pub(super) fn nearest_free_label(labels: &[&Label], midpoint: (f32, f32), tolerance: f32) -> Option<String> {
labels
.iter()
.map(|l| {
let dx = l.x - midpoint.0;
let dy = l.y - midpoint.1;
(l, (dx * dx + dy * dy).sqrt())
})
.filter(|(_, distance)| *distance <= tolerance)
.min_by(|a, b| a.1.total_cmp(&b.1))
.map(|(l, _)| l.text.clone())
}