Perf: prefix-sum LineIndex for grid geometry (O(log n) with many custom row heights)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
b8ff9648a9
commit
2618a233bf
|
|
@ -83,7 +83,7 @@ fn stroke(s: &mut Session, p: &Json) -> Result<Json> {
|
|||
id,
|
||||
kind: ShapeKind::Ink,
|
||||
anchor: Anchor { cell, dx, dy, width: (maxx - minx).max(1.0) as f32, height: (maxy - miny).max(1.0) as f32 },
|
||||
fill: Color::from_hex(&format!("#{:02X}0000", (width.round() as u8).min(255))).unwrap_or_default(),
|
||||
fill: Color::from_hex(&format!("#{:02X}0000", width.round().clamp(0.0, 255.0) as u8)).unwrap_or_default(),
|
||||
line: Color::from_hex(&color).unwrap_or(Color::rgb(0x1F, 0x5F, 0xC9)),
|
||||
text,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ pub mod workbook;
|
|||
|
||||
pub use cell::{Cell, Formula};
|
||||
pub use features::*;
|
||||
pub use sheet::{DEFAULT_COL_WIDTH, DEFAULT_ROW_HEIGHT, Sheet, Visibility};
|
||||
pub use sheet::{DEFAULT_COL_WIDTH, DEFAULT_ROW_HEIGHT, LineIndex, Sheet, Visibility};
|
||||
pub use store::CellStore;
|
||||
pub use style::{
|
||||
Alignment, BorderLine, BorderStyle, Borders, Color, Fill, Font, HAlign, NumFmt, PatternType, Style, StyleId, StyleTable, Theme, Underline,
|
||||
|
|
|
|||
|
|
@ -209,6 +209,67 @@ impl Sheet {
|
|||
}
|
||||
}
|
||||
|
||||
/// Prefix sums over the custom-sized lines of an axis: O(log n) position ↔ line lookups even
|
||||
/// when every row has an explicit height (common in imported files).
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct LineIndex {
|
||||
default: f64,
|
||||
max: u32,
|
||||
/// (line, start position of that line, its size), sorted by line.
|
||||
lines: Vec<(u32, f64, f64)>,
|
||||
}
|
||||
|
||||
impl LineIndex {
|
||||
pub fn new(custom: &BTreeMap<u32, LineInfo>, default: f32, max: u32) -> LineIndex {
|
||||
let default = default as f64;
|
||||
let mut lines = Vec::with_capacity(custom.len());
|
||||
let mut pos = 0.0;
|
||||
let mut prev = 0u32;
|
||||
for (&line, info) in custom {
|
||||
pos += (line - prev) as f64 * default;
|
||||
let size = if info.hidden { 0.0 } else { info.size.map_or(default, |s| s as f64) };
|
||||
lines.push((line, pos, size));
|
||||
pos += size;
|
||||
prev = line + 1;
|
||||
}
|
||||
LineIndex { default, max, lines }
|
||||
}
|
||||
/// Start position of `line`.
|
||||
pub fn start(&self, line: u32) -> f64 {
|
||||
match self.lines.binary_search_by(|e| e.0.cmp(&line)) {
|
||||
Ok(i) => self.lines.get(i).map_or(0.0, |e| e.1),
|
||||
Err(0) => line as f64 * self.default,
|
||||
Err(i) => match self.lines.get(i - 1) {
|
||||
Some(&(l, p, s)) => p + s + (line - l - 1) as f64 * self.default,
|
||||
None => line as f64 * self.default,
|
||||
},
|
||||
}
|
||||
}
|
||||
/// The line containing `pos`.
|
||||
pub fn at(&self, pos: f64) -> u32 {
|
||||
if pos <= 0.0 {
|
||||
return 0;
|
||||
}
|
||||
// Last custom line starting at or before pos.
|
||||
let i = self.lines.partition_point(|e| e.1 <= pos);
|
||||
let (base_line, base_pos) = match i.checked_sub(1).and_then(|k| self.lines.get(k)) {
|
||||
Some(&(l, p, s)) => {
|
||||
if pos < p + s {
|
||||
return l.min(self.max - 1);
|
||||
}
|
||||
(l + 1, p + s)
|
||||
}
|
||||
None => (0, 0.0),
|
||||
};
|
||||
if self.default <= 0.0 {
|
||||
return base_line.min(self.max - 1);
|
||||
}
|
||||
let k = ((pos - base_pos) / self.default).floor();
|
||||
let r = base_line as f64 + k;
|
||||
if r >= self.max as f64 { self.max - 1 } else { r as u32 }
|
||||
}
|
||||
}
|
||||
|
||||
/// Inverse of the cumulative size along an axis with sparse custom sizes.
|
||||
fn line_at(pos: f64, default: f64, custom: &BTreeMap<u32, LineInfo>, max: u32) -> u32 {
|
||||
if pos <= 0.0 {
|
||||
|
|
@ -243,6 +304,21 @@ fn line_at(pos: f64, default: f64, custom: &BTreeMap<u32, LineInfo>, max: u32) -
|
|||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn line_index_matches_linear() {
|
||||
let mut s = Sheet::new("S");
|
||||
for r in (0..5000).step_by(3) {
|
||||
s.rows.insert(r, LineInfo { size: Some(10.0 + (r % 7) as f32), hidden: r % 11 == 0, ..Default::default() });
|
||||
}
|
||||
let idx = LineIndex::new(&s.rows, s.default_row_height, sheetcraft_core::MAX_ROWS);
|
||||
for r in [0u32, 1, 2, 3, 4, 100, 999, 4998, 5000, 6000] {
|
||||
assert!((idx.start(r) - s.row_top(r)).abs() < 1e-6, "start {r}");
|
||||
}
|
||||
for y in [0.0, 5.0, 33.3, 1000.0, 47_000.0, 90_000.0] {
|
||||
assert_eq!(idx.at(y), s.row_at(y), "at {y}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn geometry() {
|
||||
let mut s = Sheet::new("S");
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use egui::{Align2, Color32, CursorIcon, FontId, Painter, Pos2, Rect, Sense, Stro
|
|||
use serde_json::json;
|
||||
use sheetcraft_engine::cf::{CfCache, CfLook};
|
||||
use sheetcraft_engine::core::{CellRef, MAX_COLS, MAX_ROWS, RangeRef, Value, col_to_letters};
|
||||
use sheetcraft_engine::model::{BorderStyle, Color, HAlign, PatternType, Sheet, Style, Underline, VAlign, Workbook};
|
||||
use sheetcraft_engine::model::{BorderStyle, Color, HAlign, LineIndex, PatternType, Sheet, Style, Underline, VAlign, Workbook};
|
||||
|
||||
use crate::SheetApp;
|
||||
use crate::editor::{EditState, REF_COLORS};
|
||||
|
|
@ -96,7 +96,7 @@ impl GridState {
|
|||
}
|
||||
|
||||
/// Geometry of the grid for one frame (handles zoom and frozen panes).
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(Clone)]
|
||||
pub struct Geo {
|
||||
pub rect: Rect,
|
||||
pub cells: Rect,
|
||||
|
|
@ -108,6 +108,9 @@ pub struct Geo {
|
|||
pub frozen_w: f32,
|
||||
pub frozen_h: f32,
|
||||
pub scroll: egui::Vec2,
|
||||
/// Prefix sums over custom row heights / column widths, built once per frame.
|
||||
ri: std::sync::Arc<LineIndex>,
|
||||
ci: std::sync::Arc<LineIndex>,
|
||||
}
|
||||
|
||||
impl Geo {
|
||||
|
|
@ -115,27 +118,29 @@ impl Geo {
|
|||
let zoom = (sh.zoom as f32 / 100.0).clamp(0.1, 4.0);
|
||||
let z = zoom * DISPLAY_SCALE;
|
||||
let header_h = if sh.show_headings { (COL_HEADER_H * zoom.max(0.75)).round() } else { 0.0 };
|
||||
let last_row = sh.row_at((scroll.y + rect.height() / z) as f64 + 200.0) + 1;
|
||||
let ri = std::sync::Arc::new(LineIndex::new(&sh.rows, sh.default_row_height, MAX_ROWS));
|
||||
let ci = std::sync::Arc::new(LineIndex::new(&sh.cols, sh.default_col_width, MAX_COLS));
|
||||
let last_row = ri.at((scroll.y + rect.height() / z) as f64 + 200.0) + 1;
|
||||
let digits = (last_row as f32).log10().floor() as i32 + 1;
|
||||
let header_w = if sh.show_headings { ((digits.max(2) as f32 * 7.0 + 14.0) * zoom.max(0.75)).round() } else { 0.0 };
|
||||
let (fr, fc) = sh.freeze.unwrap_or((0, 0));
|
||||
let frozen_w = (sh.col_left(fc) as f32) * z;
|
||||
let frozen_h = (sh.row_top(fr) as f32) * z;
|
||||
let frozen_w = (ci.start(fc) as f32) * z;
|
||||
let frozen_h = (ri.start(fr) as f32) * z;
|
||||
let cells = Rect::from_min_max(pos2(rect.left() + header_w, rect.top() + header_h), rect.max);
|
||||
Geo { rect, cells, z, header_w, header_h, fr, fc, frozen_w, frozen_h, scroll }
|
||||
Geo { rect, cells, z, header_w, header_h, fr, fc, frozen_w, frozen_h, scroll, ri, ci }
|
||||
}
|
||||
pub fn x(&self, sh: &Sheet, col: u32) -> f32 {
|
||||
pub fn x(&self, _sh: &Sheet, col: u32) -> f32 {
|
||||
if col < self.fc {
|
||||
self.cells.left() + sh.col_left(col) as f32 * self.z
|
||||
self.cells.left() + self.ci.start(col) as f32 * self.z
|
||||
} else {
|
||||
self.cells.left() + self.frozen_w + (sh.col_left(col) as f32 - sh.col_left(self.fc) as f32 - self.scroll.x) * self.z
|
||||
self.cells.left() + self.frozen_w + (self.ci.start(col) as f32 - self.ci.start(self.fc) as f32 - self.scroll.x) * self.z
|
||||
}
|
||||
}
|
||||
pub fn y(&self, sh: &Sheet, row: u32) -> f32 {
|
||||
pub fn y(&self, _sh: &Sheet, row: u32) -> f32 {
|
||||
if row < self.fr {
|
||||
self.cells.top() + sh.row_top(row) as f32 * self.z
|
||||
self.cells.top() + self.ri.start(row) as f32 * self.z
|
||||
} else {
|
||||
self.cells.top() + self.frozen_h + (sh.row_top(row) as f32 - sh.row_top(self.fr) as f32 - self.scroll.y) * self.z
|
||||
self.cells.top() + self.frozen_h + (self.ri.start(row) as f32 - self.ri.start(self.fr) as f32 - self.scroll.y) * self.z
|
||||
}
|
||||
}
|
||||
pub fn cell_rect(&self, sh: &Sheet, c: CellRef) -> Rect {
|
||||
|
|
@ -149,36 +154,36 @@ impl Geo {
|
|||
let b = self.cell_rect(sh, r.end);
|
||||
Rect::from_min_max(a.min, b.max)
|
||||
}
|
||||
pub fn col_at(&self, sh: &Sheet, x: f32) -> u32 {
|
||||
pub fn col_at(&self, _sh: &Sheet, x: f32) -> u32 {
|
||||
let dx = (x - self.cells.left()) / self.z;
|
||||
if dx < self.frozen_w / self.z {
|
||||
sh.col_at(dx.max(0.0) as f64)
|
||||
self.ci.at(dx.max(0.0) as f64)
|
||||
} else {
|
||||
sh.col_at(sh.col_left(self.fc) + self.scroll.x as f64 + (dx - self.frozen_w / self.z) as f64)
|
||||
self.ci.at(self.ci.start(self.fc) + self.scroll.x as f64 + (dx - self.frozen_w / self.z) as f64)
|
||||
}
|
||||
}
|
||||
pub fn row_at(&self, sh: &Sheet, y: f32) -> u32 {
|
||||
pub fn row_at(&self, _sh: &Sheet, y: f32) -> u32 {
|
||||
let dy = (y - self.cells.top()) / self.z;
|
||||
if dy < self.frozen_h / self.z {
|
||||
sh.row_at(dy.max(0.0) as f64)
|
||||
self.ri.at(dy.max(0.0) as f64)
|
||||
} else {
|
||||
sh.row_at(sh.row_top(self.fr) + self.scroll.y as f64 + (dy - self.frozen_h / self.z) as f64)
|
||||
self.ri.at(self.ri.start(self.fr) + self.scroll.y as f64 + (dy - self.frozen_h / self.z) as f64)
|
||||
}
|
||||
}
|
||||
pub fn cell_at(&self, sh: &Sheet, p: Pos2) -> CellRef {
|
||||
CellRef::new(self.row_at(sh, p.y), self.col_at(sh, p.x))
|
||||
}
|
||||
/// Visible scrolled rows (from the first visible row after the frozen ones).
|
||||
fn scroll_rows(&self, sh: &Sheet) -> (u32, u32) {
|
||||
let top = sh.row_top(self.fr) + self.scroll.y as f64;
|
||||
let r0 = sh.row_at(top).max(self.fr);
|
||||
let r1 = sh.row_at(top + ((self.cells.height() - self.frozen_h) / self.z) as f64).min(MAX_ROWS - 1);
|
||||
fn scroll_rows(&self, _sh: &Sheet) -> (u32, u32) {
|
||||
let top = self.ri.start(self.fr) + self.scroll.y as f64;
|
||||
let r0 = self.ri.at(top).max(self.fr);
|
||||
let r1 = self.ri.at(top + ((self.cells.height() - self.frozen_h) / self.z) as f64).min(MAX_ROWS - 1);
|
||||
(r0, r1)
|
||||
}
|
||||
fn scroll_cols(&self, sh: &Sheet) -> (u32, u32) {
|
||||
let left = sh.col_left(self.fc) + self.scroll.x as f64;
|
||||
let c0 = sh.col_at(left).max(self.fc);
|
||||
let c1 = sh.col_at(left + ((self.cells.width() - self.frozen_w) / self.z) as f64).min(MAX_COLS - 1);
|
||||
fn scroll_cols(&self, _sh: &Sheet) -> (u32, u32) {
|
||||
let left = self.ci.start(self.fc) + self.scroll.x as f64;
|
||||
let c0 = self.ci.at(left).max(self.fc);
|
||||
let c1 = self.ci.at(left + ((self.cells.width() - self.frozen_w) / self.z) as f64).min(MAX_COLS - 1);
|
||||
(c0, c1)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue