mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-12 05:12:39 +00:00
Small bounds: outline simplification, table row insertion, note ids, xtask stats (#2232)
* fix: iterative outline simplification, bulk row insertion, linear note ids, no symlink walking in xtask stats * Apply review fixes - crates/doc/src/tests.rs - crates/geom/src/trace.rs * Apply review fixes - crates/geom/src/trace.rs * address review: cover a maximal note id in tests, format trace test --------- Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
This commit is contained in:
1 parent
eba00a0dec
commit
dc9eea2695
7 files changed
+107
-28
No files matched your search
@@ -103,8 +103,12 @@ impl Story {
|
||||
|
||||
/// Insert empty endnotes for `n` new reference characters before which `k` references sit.
|
||||
pub(crate) fn endnotes_inserted(&mut self, k: usize, n: usize) {
|
||||
// As for footnotes: the largest id kept as ids are given out, and the free-id search past
|
||||
// the largest one (`+ 1` overflowed there).
|
||||
let mut max = self.endnotes.iter().map(|x| x.id).max().unwrap_or(0);
|
||||
for i in 0..n {
|
||||
let id = self.endnotes.iter().map(|x| x.id).max().unwrap_or(0) + 1;
|
||||
let id = max.checked_add(1).unwrap_or_else(|| crate::ids::next_local_id(self.endnotes.iter().map(|x| x.id)));
|
||||
max = max.max(id);
|
||||
let mut text = Story::new(StoryId(0));
|
||||
text.chars[0].format = CharFormat::default();
|
||||
self.endnotes.insert((k + i).min(self.endnotes.len()), Arc::new(Footnote { id, text }));
|
||||
|
||||
@@ -186,8 +186,12 @@ impl Story {
|
||||
|
||||
/// Insert empty footnotes for `n` new reference characters before which `k` references sit.
|
||||
pub(crate) fn notes_inserted(&mut self, k: usize, n: usize) {
|
||||
// The largest id so far, kept as ids are given out (finding it again for every new note
|
||||
// was quadratic in a batch); past the largest id, the free-id search as before.
|
||||
let mut max = self.notes.iter().map(|x| x.id).max().unwrap_or(0);
|
||||
for i in 0..n {
|
||||
let id = self.next_note_id();
|
||||
let id = max.checked_add(1).unwrap_or_else(|| self.next_note_id());
|
||||
max = max.max(id);
|
||||
let mut text = Story::new(StoryId(0));
|
||||
text.chars[0].format = CharFormat::default();
|
||||
self.notes.insert((k + i).min(self.notes.len()), Arc::new(Footnote { id, text }));
|
||||
|
||||
@@ -264,6 +264,13 @@ mod tests {
|
||||
t.check().unwrap();
|
||||
assert_eq!(t.cell(1, 1).unwrap().row_span, 3);
|
||||
assert_eq!(t.nrows(), 5);
|
||||
// Several rows at once go in together, in order, inside the merged region.
|
||||
t.insert_rows(2, 3);
|
||||
t.check().unwrap();
|
||||
assert_eq!(t.cell(1, 1).unwrap().row_span, 6);
|
||||
assert_eq!(t.nrows(), 8);
|
||||
t.delete_rows(2, 3);
|
||||
t.check().unwrap();
|
||||
// Header stays first.
|
||||
t.insert_rows(0, 1);
|
||||
t.check().unwrap();
|
||||
|
||||
@@ -35,13 +35,11 @@ impl Table {
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
for k in 0..n {
|
||||
self.rows.insert(at + k, row.clone());
|
||||
let i = (at + k) * nc;
|
||||
for (c, cell) in template.iter().enumerate() {
|
||||
self.cells.insert(i + c, cell.clone());
|
||||
}
|
||||
}
|
||||
// All the new rows and cells in one move each (one at a time moved every later cell
|
||||
// again for each one).
|
||||
self.rows.splice(at..at, std::iter::repeat_n(row, n));
|
||||
let i = at * nc;
|
||||
self.cells.splice(i..i, (0..n).flat_map(|_| template.iter().cloned()));
|
||||
self.set_regions(®ions);
|
||||
}
|
||||
|
||||
|
||||
@@ -568,6 +568,35 @@ fn set_story_text_prunes_story_hyperlinks_and_placeholders() {
|
||||
assert_eq!(d.data_merge.placeholders[0].id, 20);
|
||||
}
|
||||
|
||||
/// Many note references put in at once each get a note with an id of its own, in one pass.
|
||||
#[test]
|
||||
fn many_note_references_get_their_notes_at_once() {
|
||||
let n = 20_000;
|
||||
let mut st = Story::new(StoryId(1));
|
||||
let refs = format!("{}{}", FOOTNOTE_REF.to_string().repeat(n), crate::ENDNOTE_REF.to_string().repeat(n));
|
||||
st.insert(0, &refs);
|
||||
assert_eq!((st.notes.len(), st.endnotes.len()), (n, n));
|
||||
let ids: std::collections::HashSet<u64> = st.notes.iter().map(|x| x.id).collect();
|
||||
assert_eq!(ids.len(), n);
|
||||
let ids: std::collections::HashSet<u64> = st.endnotes.iter().map(|x| x.id).collect();
|
||||
assert_eq!(ids.len(), n);
|
||||
}
|
||||
|
||||
/// An existing note whose id is `u64::MAX` can't be counted past: the next note still gets an id
|
||||
/// of its own.
|
||||
#[test]
|
||||
fn note_ids_survive_a_maximal_existing_id() {
|
||||
let mut st = Story::new(StoryId(1));
|
||||
st.insert(0, &FOOTNOTE_REF.to_string());
|
||||
st.insert(0, &crate::ENDNOTE_REF.to_string());
|
||||
Arc::make_mut(&mut st.notes[0]).id = u64::MAX;
|
||||
Arc::make_mut(&mut st.endnotes[0]).id = u64::MAX;
|
||||
st.insert(0, &FOOTNOTE_REF.to_string());
|
||||
st.insert(0, &crate::ENDNOTE_REF.to_string());
|
||||
assert_ne!(st.notes[0].id, st.notes[1].id);
|
||||
assert_ne!(st.endnotes[0].id, st.endnotes[1].id);
|
||||
}
|
||||
|
||||
/// Exceptions are read in one pass: breaks are char positions of the lowercased word, a break
|
||||
/// at the start, at the end or twice in a row counts once or not at all, and a long exception
|
||||
/// full of breaks takes no longer than its length.
|
||||
|
||||
+53
-18
@@ -81,33 +81,68 @@ fn simplify_closed(p: &[Point], tol: f64) -> Vec<Point> {
|
||||
a
|
||||
}
|
||||
|
||||
/// Ramer–Douglas–Peucker on `p`, keeping its ends. Spans wait on a stack of their own rather
|
||||
/// than in nested calls: an outline of many points can split one point at a time, deeper than
|
||||
/// the call stack goes. A tolerance that isn't a number or is below zero counts as zero (each
|
||||
/// split must take a point strictly inside its span, or the span would never get shorter).
|
||||
fn rdp(p: &[Point], tol: f64) -> Vec<Point> {
|
||||
if p.len() < 3 {
|
||||
return p.to_vec();
|
||||
}
|
||||
let (a, b) = (p[0], p[p.len() - 1]);
|
||||
let d = b - a;
|
||||
let len = d.hypot();
|
||||
let (mut best, mut idx) = (0.0, 0);
|
||||
for (i, q) in p.iter().enumerate().take(p.len() - 1).skip(1) {
|
||||
let dist = if len < 1e-12 { (*q - a).hypot() } else { ((*q - a).cross(d) / len).abs() };
|
||||
if dist > best {
|
||||
(best, idx) = (dist, i);
|
||||
let (Some(_), Some(last)) = (p.first(), p.len().checked_sub(1)) else { return Vec::new() };
|
||||
let tol = if tol >= 0.0 { tol } else { 0.0 };
|
||||
let mut keep = vec![false; p.len()];
|
||||
keep[0] = true;
|
||||
keep[last] = true;
|
||||
let mut spans = vec![(0, last)];
|
||||
while let Some((s, e)) = spans.pop() {
|
||||
if e < s + 2 {
|
||||
continue;
|
||||
}
|
||||
let (a, b) = (p[s], p[e]);
|
||||
let d = b - a;
|
||||
let len = d.hypot();
|
||||
let (mut best, mut idx) = (0.0, s);
|
||||
for (i, q) in p.iter().enumerate().take(e).skip(s + 1) {
|
||||
let dist = if len < 1e-12 { (*q - a).hypot() } else { ((*q - a).cross(d) / len).abs() };
|
||||
if dist > best {
|
||||
(best, idx) = (dist, i);
|
||||
}
|
||||
}
|
||||
if best > tol && idx > s {
|
||||
keep[idx] = true;
|
||||
spans.push((idx, e));
|
||||
spans.push((s, idx));
|
||||
}
|
||||
}
|
||||
if best <= tol {
|
||||
return vec![a, b];
|
||||
}
|
||||
let mut left = rdp(&p[..=idx], tol);
|
||||
left.pop();
|
||||
left.extend(rdp(&p[idx..], tol));
|
||||
left
|
||||
p.iter().zip(keep).filter_map(|(q, k)| k.then_some(*q)).collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A tolerance below zero or NaN ends (each split shrinks its span), and a 200,000-point
|
||||
/// path simplifies to completion.
|
||||
#[test]
|
||||
fn simplification_ends_for_any_tolerance_and_depth() {
|
||||
let line: Vec<Point> = (0..10).map(|i| Point::new(f64::from(i), 0.0)).collect();
|
||||
for tol in [-1.0, f64::NAN, 0.0] {
|
||||
assert_eq!(rdp(&line, tol).len(), 2, "{tol}");
|
||||
}
|
||||
// A long convex curve simplifies to completion.
|
||||
let n = 200_000;
|
||||
let curve: Vec<Point> = (0..n).map(|i| Point::new(f64::from(i), (f64::from(i) * 1e-3).powi(3))).collect();
|
||||
assert!(rdp(&curve, 1e-12).len() > 2);
|
||||
// A zigzag of growing amplitude: every split takes the point next to the span's end.
|
||||
let zigzag: Vec<Point> = (0..10_000)
|
||||
.map(|i| {
|
||||
let x = f64::from(i);
|
||||
Point::new(x, if i % 2 == 0 { x } else { -x })
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(rdp(&zigzag, 0.0).len(), zigzag.len());
|
||||
assert!(rdp(&[], 1.0).is_empty());
|
||||
assert_eq!(rdp(&line[..1], 1.0).len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn traces_a_disc_and_its_hole() {
|
||||
let (w, h) = (40, 40);
|
||||
|
||||
+3
-1
@@ -18,7 +18,9 @@ fn walk(dir: &Path, c: &mut Counts) {
|
||||
let p = e.path();
|
||||
let name = e.file_name();
|
||||
let name = name.to_string_lossy();
|
||||
if p.is_dir() {
|
||||
// A link to a folder isn't followed: links can lead back up the tree or to the same
|
||||
// folder many times over.
|
||||
if e.file_type().is_ok_and(|t| t.is_dir()) {
|
||||
// Nested fuzz workspaces and build output are not part of the crate.
|
||||
if name != "target" && name != "fuzz" && !name.starts_with('.') {
|
||||
walk(&p, c);
|
||||
|
||||
Reference in new issue
Block a user