use crate::text::Ruling;
const SNAP: f64 = 1.5;
const STACK_GAP: f64 = 6.0;
#[derive(Debug, Clone)]
pub struct Lattice {
pub xs: Vec<f64>,
pub ys: Vec<f64>,
horizontal: Vec<Ruling>,
vertical: Vec<Ruling>,
}
impl Lattice {
pub fn bbox(&self) -> [f64; 4] {
[
self.xs[0],
*self.ys.last().expect("two rows of lines"),
*self.xs.last().expect("two columns of lines"),
self.ys[0],
]
}
fn has_vertical(&self, x: f64, y: f64) -> bool {
self.vertical
.iter()
.any(|r| (r.x0 - x).abs() <= SNAP && r.y0 - SNAP <= y && r.y1 + SNAP >= y)
}
fn has_horizontal(&self, y: f64, x: f64) -> bool {
self.horizontal
.iter()
.any(|r| (r.y0 - y).abs() <= SNAP && r.x0 - SNAP <= x && r.x1 + SNAP >= x)
}
pub fn cells(&self) -> Vec<(std::ops::Range<usize>, std::ops::Range<usize>)> {
let rows = self.ys.len() - 1;
let columns = self.xs.len() - 1;
let at = |r: usize, c: usize| r * columns + c;
let mut parent: Vec<usize> = (0..rows * columns).collect();
fn find(parent: &mut [usize], i: usize) -> usize {
let mut i = i;
while parent[i] != i {
parent[i] = parent[parent[i]];
i = parent[i];
}
i
}
for r in 0..rows {
let middle_y = (self.ys[r] + self.ys[r + 1]) * 0.5;
for c in 0..columns {
let middle_x = (self.xs[c] + self.xs[c + 1]) * 0.5;
if c + 1 < columns && !self.has_vertical(self.xs[c + 1], middle_y) {
let (a, b) = (find(&mut parent, at(r, c)), find(&mut parent, at(r, c + 1)));
parent[b] = a;
}
if r + 1 < rows && !self.has_horizontal(self.ys[r + 1], middle_x) {
let (a, b) = (find(&mut parent, at(r, c)), find(&mut parent, at(r + 1, c)));
parent[b] = a;
}
}
}
let mut groups: std::collections::BTreeMap<usize, (usize, usize, usize, usize)> =
Default::default();
for r in 0..rows {
for c in 0..columns {
let root = find(&mut parent, at(r, c));
let e = groups.entry(root).or_insert((r, r, c, c));
e.0 = e.0.min(r);
e.1 = e.1.max(r);
e.2 = e.2.min(c);
e.3 = e.3.max(c);
}
}
let mut out: Vec<(std::ops::Range<usize>, std::ops::Range<usize>)> = groups
.into_values()
.map(|(r0, r1, c0, c1)| (r0..r1 + 1, c0..c1 + 1))
.collect();
out.sort_by_key(|(r, c)| (r.start, c.start));
out
}
pub fn cell_at(
&self,
cells: &[(std::ops::Range<usize>, std::ops::Range<usize>)],
x: f64,
y: f64,
) -> Option<usize> {
let column = (0..self.xs.len() - 1).find(|&c| x >= self.xs[c] && x <= self.xs[c + 1])?;
let row = (0..self.ys.len() - 1).find(|&r| y <= self.ys[r] && y >= self.ys[r + 1])?;
cells
.iter()
.position(|(rows, columns)| rows.contains(&row) && columns.contains(&column))
}
}
fn merged(mut segments: Vec<Ruling>, horizontal: bool) -> Vec<Ruling> {
type Of = fn(&Ruling) -> f64;
let (pos, lo, hi): (Of, Of, Of) = if horizontal {
(|r| r.y0, |r| r.x0, |r| r.x1)
} else {
(|r| r.x0, |r| r.y0, |r| r.y1)
};
segments.sort_by(|a, b| {
pos(a)
.partial_cmp(&pos(b))
.unwrap_or(std::cmp::Ordering::Equal)
.then(
lo(a)
.partial_cmp(&lo(b))
.unwrap_or(std::cmp::Ordering::Equal),
)
});
let mut out: Vec<Ruling> = Vec::new();
for s in segments {
if let Some(last) = out.last_mut() {
if (pos(last) - pos(&s)).abs() <= SNAP && lo(&s) <= hi(last) + SNAP {
if horizontal {
last.x1 = last.x1.max(s.x1);
} else {
last.y1 = last.y1.max(s.y1);
}
continue;
}
}
out.push(s);
}
out
}
fn distinct(mut values: Vec<f64>) -> Vec<f64> {
values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let mut out: Vec<f64> = Vec::new();
for v in values {
if out.last().is_none_or(|last| v - last > SNAP) {
out.push(v);
}
}
out
}
pub fn lattices(rulings: &[Ruling]) -> Vec<Lattice> {
let horizontal = merged(
rulings.iter().copied().filter(|r| r.y0 == r.y1).collect(),
true,
);
let vertical = merged(
rulings
.iter()
.copied()
.filter(|r| r.x0 == r.x1 && r.y0 != r.y1)
.collect(),
false,
);
let n = horizontal.len() + vertical.len();
if horizontal.len() < 3 || vertical.len() < 3 {
return Vec::new();
}
let mut parent: Vec<usize> = (0..n).collect();
fn find(parent: &mut [usize], i: usize) -> usize {
let mut i = i;
while parent[i] != i {
parent[i] = parent[parent[i]];
i = parent[i];
}
i
}
for (i, h) in horizontal.iter().enumerate() {
for (j, v) in vertical.iter().enumerate() {
let crosses = v.x0 >= h.x0 - SNAP
&& v.x0 <= h.x1 + SNAP
&& h.y0 >= v.y0 - SNAP
&& h.y0 <= v.y1 + SNAP;
if crosses {
let (a, b) = (
find(&mut parent, i),
find(&mut parent, horizontal.len() + j),
);
parent[b] = a;
}
}
}
let mut groups: std::collections::BTreeMap<usize, (Vec<Ruling>, Vec<Ruling>)> =
Default::default();
for i in 0..n {
let root = find(&mut parent, i);
let e = groups.entry(root).or_default();
if i < horizontal.len() {
e.0.push(horizontal[i]);
} else {
e.1.push(vertical[i - horizontal.len()]);
}
}
let mut groups: Vec<(Vec<Ruling>, Vec<Ruling>)> = groups.into_values().collect();
let extent = |g: &(Vec<Ruling>, Vec<Ruling>)| {
let xs: Vec<f64> = g.1.iter().map(|r| r.x0).collect();
let bottom =
g.0.iter()
.map(|r| r.y0)
.chain(g.1.iter().map(|r| r.y0))
.fold(f64::INFINITY, f64::min);
let top =
g.0.iter()
.map(|r| r.y0)
.chain(g.1.iter().map(|r| r.y1))
.fold(f64::NEG_INFINITY, f64::max);
(distinct(xs), bottom, top)
};
let mut joined = true;
while joined {
joined = false;
'pairs: for a in 0..groups.len() {
for b in 0..groups.len() {
if a == b {
continue;
}
let (xa, bottom_a, _) = extent(&groups[a]);
let (xb, _, top_b) = extent(&groups[b]);
let shared = xa
.iter()
.filter(|x| xb.iter().any(|y| (*x - y).abs() <= SNAP))
.count();
let touching = (bottom_a - top_b).abs() <= STACK_GAP;
if touching && shared >= 2 && shared * 2 >= xa.len().min(xb.len()) {
let other = groups.remove(b);
let a = if b < a { a - 1 } else { a };
groups[a].0.extend(other.0);
groups[a].1.extend(other.1);
joined = true;
break 'pairs;
}
}
}
}
let mut out = Vec::new();
for (h, v) in groups {
let xs = distinct(
v.iter()
.map(|r| r.x0)
.chain(h.iter().map(|r| r.x0))
.chain(h.iter().map(|r| r.x1))
.filter(|&x| {
v.iter().any(|r| (r.x0 - x).abs() <= SNAP)
|| h.iter()
.filter(|r| (r.x0 - x).abs() <= SNAP || (r.x1 - x).abs() <= SNAP)
.count()
>= 2
})
.collect(),
);
let mut ys = distinct(
h.iter()
.map(|r| r.y0)
.chain(v.iter().map(|r| r.y0))
.chain(v.iter().map(|r| r.y1))
.filter(|&y| {
h.iter().any(|r| (r.y0 - y).abs() <= SNAP)
|| v.iter()
.filter(|r| (r.y0 - y).abs() <= SNAP || (r.y1 - y).abs() <= SNAP)
.count()
>= 2
})
.collect(),
);
ys.reverse();
if xs.len() >= 3 && ys.len() >= 3 {
out.push(Lattice {
xs,
ys,
horizontal: h,
vertical: v,
});
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn h(y: f64, x0: f64, x1: f64) -> Ruling {
Ruling {
x0,
y0: y,
x1,
y1: y,
}
}
fn v(x: f64, y0: f64, y1: f64) -> Ruling {
Ruling {
x0: x,
y0,
x1: x,
y1,
}
}
#[test]
fn a_drawn_grid_is_its_cells_and_a_missing_line_is_a_merged_cell() {
let mut lines = vec![
h(100.0, 0.0, 90.0),
h(80.0, 0.0, 90.0),
h(60.0, 0.0, 90.0),
h(40.0, 0.0, 90.0),
v(0.0, 40.0, 100.0),
v(90.0, 40.0, 100.0),
];
lines.push(v(30.0, 40.0, 80.0));
lines.push(v(60.0, 40.0, 80.0));
let found = lattices(&lines);
assert_eq!(found.len(), 1);
let grid = &found[0];
assert_eq!(grid.xs, vec![0.0, 30.0, 60.0, 90.0]);
assert_eq!(grid.ys, vec![100.0, 80.0, 60.0, 40.0]);
let cells = grid.cells();
assert_eq!(cells[0], (0..1, 0..3), "the head is one cell across");
assert_eq!(cells.len(), 1 + 2 * 3);
assert_eq!(grid.cell_at(&cells, 45.0, 70.0), Some(2));
}
#[test]
fn an_open_side_is_still_a_side() {
let lines = vec![
h(100.0, 0.0, 90.0),
h(80.0, 0.0, 90.0),
h(60.0, 0.0, 90.0),
v(30.0, 60.0, 100.0),
v(60.0, 60.0, 100.0),
v(45.0, 60.0, 100.0),
];
let found = lattices(&lines);
assert_eq!(found.len(), 1);
assert_eq!(found[0].xs, vec![0.0, 30.0, 45.0, 60.0, 90.0]);
}
#[test]
fn a_frame_round_the_page_is_not_a_grid() {
let lines = vec![
h(800.0, 20.0, 590.0),
h(20.0, 20.0, 590.0),
v(20.0, 20.0, 800.0),
v(590.0, 20.0, 800.0),
h(400.0, 20.0, 590.0),
];
assert!(lattices(&lines).is_empty());
}
}