Compare commits

..
5 changed files with 50 additions and 122 deletions
-11
View File
@@ -433,17 +433,6 @@ pub(crate) fn shape_of(s: &Session, id: ShapeId) -> Result<deckcraft_model::Shap
pub fn xfrm_of(doc: &deckcraft_model::Presentation, sel: &crate::Selection, shape: &deckcraft_model::Shape) -> Xfrm {
let mut x = shape.xfrm.unwrap_or_else(|| ctx_xfrm(doc, sel, shape));
if let deckcraft_model::ShapeKind::Table(t) = &shape.kind {
// A loaded table may not be normalized yet: measure the bounded grid an edit will
// leave, not every cell the file stored.
let normalized;
let t = if t.is_normalized() {
t
} else {
let mut n = t.clone();
n.normalize();
normalized = n;
&normalized
};
let drawn: f64 = table_drawn_heights(doc, sel, t).iter().sum();
if drawn.is_finite() && drawn > x.h {
x.h = drawn;
-2
View File
@@ -285,8 +285,6 @@ fn with_table(s: &mut Session, p: &Value, cmd: &str, f: impl Fn(&mut Table) -> R
};
let r = edit_shapes(s, &json!({"id": id}), cmd, |sh| match &mut sh.kind {
ShapeKind::Table(t) => {
// Edits walk the grid: bound it first (a loaded table need not be normalized yet).
t.normalize();
f(t)?;
t.normalize();
let (w, h) = (t.width(), t.height());
+11 -97
View File
@@ -77,12 +77,6 @@ impl Default for Table {
}
}
/// Cells a table may hold after [`Table::normalize`] (PowerPoint's own limit is 75 x 75).
pub const MAX_CELLS: usize = 100_000;
/// Cell visits [`Table::merge`] may spend growing its rectangle over existing merges: a few
/// passes over the largest grid, where a real table needs one or two.
const MAX_MERGE_WORK: usize = 16 * MAX_CELLS;
impl Table {
pub fn new(rows: usize, cols: usize, width: f64, row_height: f64) -> Self {
let rows = rows.clamp(1, 1000);
@@ -170,12 +164,8 @@ impl Table {
true
}
/// The merged areas: every uncovered cell spanning more than one cell, clipped to the grid.
/// Merges of a well-formed table do not overlap, so together they cover at most the grid; a
/// file's overlapping spans past that are dropped, so rebuilding them cannot revisit cells
/// without bound.
fn merges(&self) -> Vec<Merge> {
let (nr, nc) = (self.rows.len(), self.cols.len());
let mut area = nr.saturating_mul(nc);
let mut out = vec![];
for (r, row) in self.rows.iter().enumerate() {
for (c, cell) in row.cells.iter().enumerate().take(nc) {
@@ -184,10 +174,7 @@ impl Table {
}
let rs = (cell.row_span.max(1) as usize).min(nr - r);
let gs = (cell.grid_span.max(1) as usize).min(nc - c);
if (rs > 1 || gs > 1)
&& let Some(left) = area.checked_sub(rs.saturating_mul(gs))
{
area = left;
if rs > 1 || gs > 1 {
out.push(Merge { r, c, rs, gs });
}
}
@@ -240,17 +227,9 @@ impl Table {
if r1 >= self.rows.len() || c1 >= self.cols.len() || (r0 == r1 && c0 == c1) {
return false;
}
// Grow the rectangle until no existing merge sticks out of it. Each pass reads the whole
// grid and may grow it by one row, so the passes are charged against
// [`MAX_MERGE_WORK`]: past it the merge is refused, before anything changed.
// Grow the rectangle until no existing merge sticks out of it.
let (mut r0, mut c0, mut r1, mut c1) = (r0, c0, r1, c1);
let pass = self.rows.len().saturating_mul(self.cols.len()).max(1);
let mut work = 0usize;
loop {
work = work.saturating_add(pass);
if work > MAX_MERGE_WORK {
return false;
}
let before = (r0, c0, r1, c1);
for (r, row) in self.rows.iter().enumerate() {
for (c, cell) in row.cells.iter().enumerate().take(self.cols.len()) {
@@ -304,31 +283,21 @@ impl Table {
return false;
}
// Spans come from the file and may reach past the table; only walk the cells that exist.
let end = r.saturating_add(rs).min(self.rows.len());
let gs = gs.min(self.cols.len().saturating_sub(c));
for row in self.rows.get_mut(r..end).unwrap_or_default() {
for x in row.cells.iter_mut().skip(c).take(gs) {
x.h_merge = false;
x.v_merge = false;
x.grid_span = 1;
x.row_span = 1;
for rr in r..r.saturating_add(rs).min(self.rows.len()) {
for cc in c..c.saturating_add(gs).min(self.cols.len()) {
if let Some(x) = self.cell_mut(rr, cc) {
x.h_merge = false;
x.v_merge = false;
x.grid_span = 1;
x.row_span = 1;
}
}
}
true
}
/// Whether the grid is as [`Table::normalize`] leaves it: within [`MAX_CELLS`], every row
/// exactly `cols.len()` cells wide.
pub fn is_normalized(&self) -> bool {
let n = self.cols.len();
n <= MAX_CELLS && self.rows.len() <= (MAX_CELLS / n.max(1)).max(1) && self.rows.iter().all(|r| r.cells.len() == n)
}
/// Make every row have exactly `cols.len()` cells (repair after loading). A grid over
/// [`MAX_CELLS`] loses its last rows (and columns) first, so a file naming many rows and many
/// columns but few cells cannot make this allocate their product.
/// Make every row have exactly `cols.len()` cells (repair after loading).
pub fn normalize(&mut self) {
self.cols.truncate(MAX_CELLS);
let n = self.cols.len();
self.rows.truncate((MAX_CELLS / n.max(1)).max(1));
for r in &mut self.rows {
r.cells.resize_with(n, || Cell::new(""));
for c in &mut r.cells {
@@ -544,61 +513,6 @@ mod tests {
}
}
/// Each cell spans two rows, uncovered: merging the last two rows would grow the rectangle a
/// row per pass over the whole grid, 10,000 passes. The merge is refused at its work bound
/// instead, leaving the table as it was; a small merge still works.
#[test]
fn merge_expansion_stops_at_its_work_bound() {
let mut t = Table::new(10_000, 10, 400.0, 30.0);
for row in &mut t.rows {
for c in &mut row.cells {
c.row_span = 2;
}
}
let before = t.clone();
assert!(!t.merge(9_998, 0, 9_999, 9));
assert_eq!(t, before);
let mut small = Table::new(4, 4, 400.0, 30.0);
assert!(small.merge(0, 0, 1, 1));
assert!(small.is_normalized() && !Table { cols: vec![1.0; 3], rows: vec![Row::default(); 2], ..Default::default() }.is_normalized());
}
#[test]
fn normalize_caps_a_sparse_grid_at_max_cells() {
// 1,000 columns and 1,000 empty rows would be a million cells.
let mut t = Table { cols: vec![10.0; 1000], rows: vec![Row::default(); 1000], ..Default::default() };
t.normalize();
assert_eq!(t.cols.len(), 1000);
assert_eq!(t.rows.len(), MAX_CELLS / 1000);
assert!(t.rows.iter().all(|r| r.cells.len() == 1000));
// Columns alone past the cap keep one row of MAX_CELLS cells.
let mut t = Table { cols: vec![1.0; MAX_CELLS + 5], rows: vec![Row::default(); 3], ..Default::default() };
t.normalize();
assert_eq!((t.cols.len(), t.rows.len()), (MAX_CELLS, 1));
// A normal table is untouched.
let mut t = Table::new(4, 4, 400.0, 30.0);
t.normalize();
assert_eq!((t.n_rows(), t.n_cols()), (4, 4));
}
#[test]
fn overlapping_file_spans_rebuild_in_bounded_work() {
// Every cell an uncovered anchor spanning to the far corner: their areas add up to about
// n^4 / 4, but only what fits the grid is kept, so an edit's rebuild stays near n^2.
let n = 200;
let mut t = Table::new(n, n, 400.0, 30.0);
for row in &mut t.rows {
for c in &mut row.cells {
c.grid_span = u32::MAX;
c.row_span = u32::MAX;
}
}
assert_eq!(t.merges().len(), 1, "the first anchor already covers the whole grid");
t.insert_row(n);
assert_eq!(t.n_rows(), n + 1);
assert!(t.split(0, 0));
}
fn merged() -> Table {
// 4x4 with a 2x2 merge at (1,1) holding "M".
let mut t = Table::new(4, 4, 400.0, 30.0);
+29 -12
View File
@@ -260,12 +260,15 @@ pub(crate) struct Tab {
align: TabAlign,
}
/// The paragraph's tab stops in position order (a stop at no finite position is left out), so
/// [`tab_advance`] finds the next one by binary search instead of a scan per tab.
pub(crate) fn tabs_of(props: &ParaProps) -> Vec<Tab> {
props
let mut tabs: Vec<Tab> = props
.tabs
.as_ref()
.map(|t| {
t.iter()
.filter(|s| s.pos.is_finite())
.map(|s| Tab {
pos: s.pos,
align: match s.align.as_str() {
@@ -277,7 +280,9 @@ pub(crate) fn tabs_of(props: &ParaProps) -> Vec<Tab> {
})
.collect()
})
.unwrap_or_default()
.unwrap_or_default();
tabs.sort_by(|a, b| a.pos.total_cmp(&b.pos));
tabs
}
/// Width of the text after the tab at cell `i` up to the next tab or line break, and the width of
@@ -301,18 +306,30 @@ pub(crate) fn tab_segment(cells: &[Cell], i: usize) -> (f64, f64) {
/// (`seg`: its width, and its width before the decimal point) as the stop aligns it but never
/// moving back, as PowerPoint does; past the last stop, to the next default stop.
pub(crate) fn tab_advance(x: f64, stops: &[Tab], default_tab: f64, seg: (f64, f64)) -> f64 {
for s in stops {
if s.pos > x + 0.5 {
let to = s.pos - x;
return match s.align {
TabAlign::Left => to,
TabAlign::Right => (to - seg.0).max(0.0),
TabAlign::Center => (to - seg.0 / 2.0).max(0.0),
TabAlign::Decimal => (to - seg.1).max(0.0),
};
}
// `stops` is in position order (`tabs_of`): the next stop is the first past x.
// A NaN `x` (a NaN spacing upstream) skips the lookup and takes the finite default-stop path.
if let Some(s) = stops.get(stops.partition_point(|s| s.pos <= x + 0.5)).filter(|_| !x.is_nan()) {
let to = s.pos - x;
return match s.align {
TabAlign::Left => to,
TabAlign::Right => (to - seg.0).max(0.0),
TabAlign::Center => (to - seg.0 / 2.0).max(0.0),
TabAlign::Decimal => (to - seg.1).max(0.0),
};
}
let d = if default_tab > 1.0 { default_tab } else { 72.0 };
let next = ((x / d).floor() + 1.0) * d;
(next - x).max(1.0)
}
#[cfg(test)]
mod tab_tests {
use super::*;
#[test]
fn a_nan_position_takes_the_finite_default_stop() {
let stops = [Tab { pos: 100.0, align: TabAlign::Left }];
assert!(tab_advance(f64::NAN, &stops, 72.0, (0.0, 0.0)).is_finite());
assert_eq!(tab_advance(10.0, &stops, 72.0, (0.0, 0.0)), 90.0);
}
}
+10
View File
@@ -1416,6 +1416,16 @@ fn tab_stops_align_the_text_after_them() {
assert!((l.lines[0].edges[4].0 - x0(&l) - 72.0).abs() < 0.01);
}
#[test]
fn many_tabs_over_many_stops_lay_out_by_position() {
// 20,000 stops given out of order, and 2,000 tabs: each tab finds the next stop by position.
let mut stops: Vec<(f64, &str)> = (1..=20_000).rev().map(|k| (k as f64 * 0.05 + 30.0, "l")).collect();
stops.push((f64::NAN, "l"));
let text = "a\t".repeat(2_000);
let l = tabbed(&text, &stops, 1.0e6);
assert!((l.lines[0].edges[2].0 - l.inner.x0 - 30.05).abs() < 0.01, "{:?}", &l.lines[0].edges[..3]);
}
#[test]
fn a_tab_past_the_end_moves_to_the_next_line() {
// PowerPoint: "abcdefghij\tk\tl\tm…" in a narrow box puts the tab that doesn't fit on the next