use super::Table;
pub const MAX_GRID_POSITIONS: u64 = 1_048_576;
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
serde::Serialize,
serde::Deserialize,
schemars::JsonSchema,
)]
pub struct GridCoord {
pub row: u32,
pub col: u32,
}
impl GridCoord {
#[must_use]
pub const fn new(row: u32, col: u32) -> Self {
Self { row, col }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct GridCell {
pub anchor: GridCoord,
pub row_idx: usize,
pub cell_idx: usize,
pub row_span: u16,
pub col_span: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum GridError {
TooLarge {
rows: u64,
cols: u64,
},
Overlap {
at: GridCoord,
},
OverhangsBottom {
at: GridCoord,
},
NotTiled {
at: GridCoord,
},
}
impl core::fmt::Display for GridError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::TooLarge { rows, cols } => write!(
f,
"table grid {rows}x{cols} exceeds the {MAX_GRID_POSITIONS}-position limit"
),
Self::Overlap { at } => {
write!(f, "cell spans overlap at logical position ({}, {})", at.row, at.col)
}
Self::OverhangsBottom { at } => write!(
f,
"cell row span extends past the last table row at ({}, {})",
at.row, at.col
),
Self::NotTiled { at } => {
write!(f, "no cell covers logical position ({}, {})", at.row, at.col)
}
}
}
}
impl std::error::Error for GridError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PlacedCell {
pub at: GridCoord,
pub row_idx: usize,
pub cell_idx: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GridPlacements {
pub cells: Vec<PlacedCell>,
pub cols: u32,
}
#[must_use]
pub fn grid_placements(table: &Table) -> GridPlacements {
let mut occupied = std::collections::HashSet::<(u32, u32)>::new();
let mut cells = Vec::new();
let mut cols: u32 = 0;
for (row_idx, row) in table.rows.iter().enumerate() {
let mut cursor: u32 = 0;
for (cell_idx, cell) in row.cells.iter().enumerate() {
while occupied.contains(&(row_idx as u32, cursor)) {
cursor += 1;
}
cells.push(PlacedCell {
at: GridCoord::new(row_idx as u32, cursor),
row_idx,
cell_idx,
});
let col_span = u32::from(cell.col_span).max(1);
let row_span = u32::from(cell.row_span).max(1);
for dr in 0..row_span {
for dc in 0..col_span {
occupied.insert((row_idx as u32 + dr, cursor + dc));
}
}
cursor += col_span;
}
cols = cols.max(cursor);
}
GridPlacements { cells, cols }
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct RowInterval {
start: u32,
end: u32,
idx: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TableGrid {
rows: u32,
cols: u32,
anchors: Vec<GridCell>,
coverage: Vec<Vec<RowInterval>>,
}
impl TableGrid {
pub fn from_table(table: &Table) -> Result<Self, GridError> {
let rows = table.rows.len() as u32;
let mut covered_area: u64 = 0;
for row in &table.rows {
for cell in &row.cells {
let area = u64::from(cell.col_span.max(1)) * u64::from(cell.row_span.max(1));
covered_area = covered_area.saturating_add(area);
}
}
if covered_area > MAX_GRID_POSITIONS {
return Err(GridError::TooLarge { rows: u64::from(rows), cols: covered_area });
}
let mut anchors: Vec<GridCell> = Vec::new();
let mut coverage: Vec<Vec<RowInterval>> = vec![Vec::new(); table.rows.len()];
let mut cols: u32 = 0;
for (row_idx, row) in table.rows.iter().enumerate() {
let mut cursor: u32 = 0;
for (cell_idx, cell) in row.cells.iter().enumerate() {
while covered(&coverage[row_idx], cursor) {
cursor += 1;
}
let col_span = cell.col_span.max(1);
let row_span = cell.row_span.max(1);
let idx = anchors.len();
let anchor = GridCoord::new(row_idx as u32, cursor);
let end_row = row_idx as u64 + u64::from(row_span);
if end_row > u64::from(rows) {
return Err(GridError::OverhangsBottom {
at: GridCoord::new(rows, anchor.col),
});
}
let end_col = u64::from(cursor) + u64::from(col_span);
if u64::from(rows) * end_col > MAX_GRID_POSITIONS {
return Err(GridError::TooLarge { rows: u64::from(rows), cols: end_col });
}
for dr in 0..u32::from(row_span) {
let target = &mut coverage[row_idx + dr as usize];
let start = cursor;
let end = cursor + u32::from(col_span);
if let Some(col) = first_covered_in(target, start, end) {
return Err(GridError::Overlap {
at: GridCoord::new(row_idx as u32 + dr, col),
});
}
let pos = target.partition_point(|iv| iv.start < start);
target.insert(pos, RowInterval { start, end, idx });
}
anchors.push(GridCell { anchor, row_idx, cell_idx, row_span, col_span });
cols = cols.max(cursor + u32::from(col_span));
cursor += u32::from(col_span);
}
}
for (row_idx, intervals) in coverage.iter().enumerate() {
let mut expected: u32 = 0;
for iv in intervals {
if iv.start != expected {
return Err(GridError::NotTiled {
at: GridCoord::new(row_idx as u32, expected),
});
}
expected = iv.end;
}
if expected != cols {
return Err(GridError::NotTiled { at: GridCoord::new(row_idx as u32, expected) });
}
}
Ok(Self { rows, cols, anchors, coverage })
}
#[must_use]
pub fn dimensions(&self) -> (u32, u32) {
(self.rows, self.cols)
}
#[must_use]
pub fn resolve(&self, at: GridCoord) -> Option<&GridCell> {
let intervals = self.coverage.get(at.row as usize)?;
let idx = interval_at(intervals, at.col)?;
Some(&self.anchors[idx])
}
pub fn iter_anchors(&self) -> impl Iterator<Item = &GridCell> {
self.anchors.iter()
}
}
fn covered(intervals: &[RowInterval], col: u32) -> bool {
interval_at(intervals, col).is_some()
}
fn interval_at(intervals: &[RowInterval], col: u32) -> Option<usize> {
let pos = intervals.partition_point(|iv| iv.end <= col);
let iv = intervals.get(pos)?;
(iv.start <= col).then_some(iv.idx)
}
fn first_covered_in(intervals: &[RowInterval], start: u32, end: u32) -> Option<u32> {
let pos = intervals.partition_point(|iv| iv.end <= start);
let iv = intervals.get(pos)?;
(iv.start < end).then_some(iv.start.max(start))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::paragraph::Paragraph;
use crate::table::{TableCell, TableRow};
use hwpforge_foundation::{HwpUnit, ParaShapeIndex};
fn cell(row_span: u16, col_span: u16) -> TableCell {
TableCell::with_span(
vec![Paragraph::new(ParaShapeIndex::new(0))],
HwpUnit::from_mm(10.0).unwrap(),
col_span,
row_span,
)
}
fn table(rows: Vec<Vec<TableCell>>) -> Table {
Table::new(rows.into_iter().map(TableRow::new).collect())
}
#[test]
fn empty_table_yields_zero_dimensions() {
let grid = TableGrid::from_table(&table(vec![])).unwrap();
assert_eq!(grid.dimensions(), (0, 0));
assert_eq!(grid.iter_anchors().count(), 0);
assert_eq!(grid.resolve(GridCoord::new(0, 0)), None);
}
#[test]
fn all_empty_rows_yield_degenerate_zero_width_grid() {
let grid = TableGrid::from_table(&table(vec![vec![], vec![]])).unwrap();
assert_eq!(grid.dimensions(), (2, 0));
assert_eq!(grid.iter_anchors().count(), 0);
}
#[test]
fn pathological_span_rejected_as_too_large() {
let t = table(vec![vec![cell(u16::MAX, u16::MAX)]]);
assert!(matches!(TableGrid::from_table(&t), Err(GridError::TooLarge { .. })));
}
#[test]
fn zero_span_normalized_to_one() {
let t = table(vec![vec![cell(0, 0)]]);
let grid = TableGrid::from_table(&t).unwrap();
assert_eq!(grid.dimensions(), (1, 1));
}
#[test]
fn row_span_overhang_rejected() {
let t = table(vec![vec![cell(2, 1)]]);
assert_eq!(
TableGrid::from_table(&t),
Err(GridError::OverhangsBottom { at: GridCoord::new(1, 0) })
);
}
#[test]
fn ragged_rows_rejected_as_not_tiled() {
let t = table(vec![vec![cell(1, 1), cell(1, 1)], vec![cell(1, 1)]]);
assert_eq!(
TableGrid::from_table(&t),
Err(GridError::NotTiled { at: GridCoord::new(1, 1) })
);
}
#[test]
fn uncovered_empty_row_rejected_as_not_tiled() {
let t = table(vec![vec![cell(1, 1)], vec![]]);
assert_eq!(
TableGrid::from_table(&t),
Err(GridError::NotTiled { at: GridCoord::new(1, 0) })
);
}
#[test]
fn overlapping_spans_rejected() {
let t = table(vec![vec![cell(2, 1), cell(1, 1), cell(2, 1)], vec![cell(1, 2)]]);
assert_eq!(TableGrid::from_table(&t), Err(GridError::Overlap { at: GridCoord::new(1, 2) }));
}
#[test]
fn fully_covered_empty_row_accepted() {
let t = table(vec![vec![cell(2, 1)], vec![]]);
let grid = TableGrid::from_table(&t).unwrap();
assert_eq!(grid.dimensions(), (2, 1));
let anchor = grid.resolve(GridCoord::new(1, 0)).unwrap();
assert_eq!(anchor.anchor, GridCoord::new(0, 0));
assert_eq!((anchor.row_idx, anchor.cell_idx), (0, 0));
}
#[test]
fn hpc_form_layout_resolves_covered_positions_to_anchors() {
let t = table(vec![
vec![cell(2, 1), cell(1, 2)],
vec![cell(1, 1), cell(1, 1)],
vec![cell(1, 1), cell(1, 1), cell(1, 1)],
vec![cell(1, 3)],
]);
let grid = TableGrid::from_table(&t).unwrap();
assert_eq!(grid.dimensions(), (4, 3));
assert_eq!(grid.iter_anchors().count(), 8);
let a = grid.resolve(GridCoord::new(1, 0)).unwrap();
assert_eq!(a.anchor, GridCoord::new(0, 0));
let b = grid.resolve(GridCoord::new(0, 2)).unwrap();
assert_eq!(b.anchor, GridCoord::new(0, 1));
let c = grid.resolve(GridCoord::new(3, 2)).unwrap();
assert_eq!(c.anchor, GridCoord::new(3, 0));
assert_eq!((c.row_idx, c.cell_idx), (3, 0));
let x = grid.resolve(GridCoord::new(1, 1)).unwrap();
assert_eq!((x.row_idx, x.cell_idx), (1, 0));
assert_eq!(x.anchor, GridCoord::new(1, 1));
assert_eq!(grid.resolve(GridCoord::new(4, 0)), None);
assert_eq!(grid.resolve(GridCoord::new(0, 3)), None);
}
#[test]
fn lenient_placement_matches_strict_for_well_formed_tables() {
let t = table(vec![
vec![cell(2, 1), cell(1, 2)],
vec![cell(1, 1), cell(1, 1)],
vec![cell(1, 1), cell(1, 1), cell(1, 1)],
vec![cell(1, 3)],
]);
let placements = grid_placements(&t);
let grid = TableGrid::from_table(&t).unwrap();
assert_eq!(placements.cols, grid.dimensions().1);
let strict: Vec<_> =
grid.iter_anchors().map(|a| (a.anchor, a.row_idx, a.cell_idx)).collect();
let lenient: Vec<_> =
placements.cells.iter().map(|p| (p.at, p.row_idx, p.cell_idx)).collect();
assert_eq!(strict, lenient);
}
#[test]
fn lenient_placement_tolerates_malformed_tables() {
let t = table(vec![vec![cell(2, 1), cell(1, 1), cell(2, 1)], vec![cell(1, 2)]]);
let placements = grid_placements(&t);
assert_eq!(placements.cells.len(), 4);
assert_eq!(placements.cells[3].at, GridCoord::new(1, 1));
assert_eq!(placements.cols, 3);
}
}