mirror of
https://github.com/storytold/deckcraft.git
synced 2026-11-05 09:59:36 -06:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ad81f944f | ||
|
|
d021e2baed | ||
|
|
bb0048e78d |
@@ -811,10 +811,32 @@ fn clamp_selection(st: &mut DocState) {
|
||||
/// Default for [`History::byte_limit`].
|
||||
pub const UNDO_BYTE_BUDGET: usize = 256 << 20;
|
||||
|
||||
/// A rough size of a text body in memory: its paragraphs and runs, at their inline struct sizes.
|
||||
fn text_cost(t: &deckcraft_model::text::TextBody) -> usize {
|
||||
use deckcraft_model::text::{Paragraph, Run};
|
||||
let (para, run) = (std::mem::size_of::<Paragraph>(), std::mem::size_of::<Run>());
|
||||
t.paragraphs.iter().map(|p| para + p.runs.iter().map(|r| run + r.text.len()).sum::<usize>()).sum()
|
||||
}
|
||||
|
||||
/// A rough size of comments in memory, replies included.
|
||||
fn comments_cost(cs: &[deckcraft_model::Comment], depth: usize) -> usize {
|
||||
let kids = |c: &deckcraft_model::Comment| if depth < 64 { comments_cost(&c.replies, depth + 1) } else { 0 };
|
||||
cs.iter().map(|c| 256 + c.author.len() + c.initials.len() + c.text.len() + c.date.len() + kids(c)).sum()
|
||||
}
|
||||
|
||||
/// A rough size of a slide's own data besides its shapes: notes, comments, tags and kept XML,
|
||||
/// all copied with the slide when an edit changes it.
|
||||
fn slide_extra_cost(s: &deckcraft_model::Slide) -> usize {
|
||||
text_cost(&s.notes)
|
||||
+ comments_cost(&s.comments, 0)
|
||||
+ s.name.len()
|
||||
+ s.tags.iter().map(|(k, v)| 48 + k.len() + v.len()).sum::<usize>()
|
||||
+ s.raw_ext.as_ref().map_or(0, String::len)
|
||||
}
|
||||
|
||||
/// A rough size of a shape in memory: the struct, its text and kept XML, its children.
|
||||
fn shape_cost(sh: &deckcraft_model::Shape, depth: usize) -> usize {
|
||||
let text: usize =
|
||||
sh.text.as_ref().map_or(0, |t| t.paragraphs.iter().map(|p| 96 + p.runs.iter().map(|r| 128 + r.text.len()).sum::<usize>()).sum());
|
||||
let text: usize = sh.text.as_ref().map_or(0, text_cost);
|
||||
let kids: usize = if depth < 64 { sh.children().iter().map(|c| shape_cost(c, depth + 1)).sum() } else { 0 };
|
||||
768 + text + sh.raw_ext.as_ref().map_or(0, String::len) + kids
|
||||
}
|
||||
@@ -832,7 +854,7 @@ pub fn undo_cost(doc: &Presentation, newer: &Presentation) -> usize {
|
||||
let media: HashSet<*const Vec<u8>> = newer.media.iter().map(|m| Arc::as_ptr(&m.data)).collect();
|
||||
let mut n = 64 * doc.slides.len();
|
||||
for s in doc.slides.iter().filter(|s| !slides.contains(&Arc::as_ptr(s))) {
|
||||
n += 512 + shapes_cost(&s.shapes);
|
||||
n += 512 + shapes_cost(&s.shapes) + slide_extra_cost(s);
|
||||
}
|
||||
for m in doc.masters.iter().filter(|m| !masters.contains(&Arc::as_ptr(m))) {
|
||||
n += 4096 + shapes_cost(&m.shapes) + m.layouts.iter().map(|l| 512 + shapes_cost(&l.shapes)).sum::<usize>();
|
||||
|
||||
+19
-32
@@ -3338,6 +3338,25 @@ fn undo_history_is_capped_by_memory_as_well_as_steps() {
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), 120);
|
||||
}
|
||||
|
||||
/// A slide's notes count toward the undo budget: small edits to a slide with large notes keep a
|
||||
/// copy of those notes per step, and must not slip past the budget as if the slide were empty.
|
||||
#[test]
|
||||
fn undo_history_counts_slide_notes() {
|
||||
let mut s = Session::with_new();
|
||||
let notes = "n".repeat(1 << 20);
|
||||
s.edit(|doc, _| {
|
||||
std::sync::Arc::make_mut(&mut doc.slides[0]).notes = deckcraft_model::text::TextBody::from_text(¬es);
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
s.doc_mut().unwrap().history = History { limit: 500, byte_limit: 8 << 20, ..Default::default() };
|
||||
for i in 0..40 {
|
||||
s.execute("slide.rename", &json!({"name": format!("v{i}"), "index": 0})).unwrap();
|
||||
}
|
||||
let n = s.doc().unwrap().history.undo.len();
|
||||
assert!((1..=8).contains(&n), "{n} steps of 1 MB notes kept under an 8 MB budget");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn picture_styles_replace_each_other_in_one_step() {
|
||||
const PNG: &[u8] = &[
|
||||
@@ -5023,38 +5042,6 @@ fn pen_pressure_is_kept_and_a_mouse_draws_at_full_width() {
|
||||
assert_eq!(got, vec![vec![0.2, 1.0, 0.4], vec![1.0, 1.0]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_long_ink_stroke_is_extended_in_place_and_capped() {
|
||||
let mut s = session();
|
||||
s.set_tool(tools::ToolKind::Ink { mode: "pen".into(), color: deckcraft_model::Rgba::rgb(20, 20, 20), width: 4.0 });
|
||||
let ev = |kind, x: f64| tools::PointerEvent { kind, x, y: 80.0, mods: Mods::default(), tol: 3.0, pressure: Some(0.5) };
|
||||
s.pointer(ev(PointerKind::Down, 0.0)).unwrap();
|
||||
let n = tools::MAX_GESTURE_POINTS + 5_000;
|
||||
for i in 1..n {
|
||||
s.pointer(ev(PointerKind::Drag, (i % 500) as f64)).unwrap();
|
||||
}
|
||||
assert_eq!(s.tool.ink_preview.len(), tools::MAX_GESTURE_POINTS);
|
||||
s.pointer(ev(PointerKind::Up, 0.0)).unwrap();
|
||||
let kept: usize = ink_pressures(&s).iter().map(Vec::len).sum();
|
||||
assert!((2..=tools::MAX_GESTURE_POINTS).contains(&kept), "{kept}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn double_clicking_a_capped_freeform_keeps_its_vertices() {
|
||||
let mut s = session();
|
||||
let ev = |kind, x: f64, y: f64| tools::PointerEvent { kind, x, y, mods: Default::default(), tol: 3.0, pressure: None };
|
||||
s.set_tool(tools::ToolKind::Shape { preset: "freeform".into() });
|
||||
s.pointer(ev(tools::PointerKind::Down, 100.0, 100.0)).unwrap();
|
||||
// Densely clicked vertices, each within the double-click tolerance of the last.
|
||||
for i in 0..tools::MAX_GESTURE_POINTS + 10 {
|
||||
s.pointer(ev(tools::PointerKind::Down, 300.0 + i as f64 * 0.001, 100.0)).unwrap();
|
||||
}
|
||||
s.pointer(ev(tools::PointerKind::DoubleClick, 300.0, 100.0)).unwrap();
|
||||
let st = s.doc().unwrap();
|
||||
let deckcraft_model::Geom::Custom { paths } = &st.shapes().last().unwrap().geom else { panic!("no freeform") };
|
||||
assert_eq!(paths[0].d.matches(" L ").count(), tools::MAX_GESTURE_POINTS - 1);
|
||||
}
|
||||
|
||||
fn ink_pressures(s: &Session) -> Vec<Vec<f32>> {
|
||||
let st = s.doc().unwrap();
|
||||
let Some(sh) = st.shapes().iter().rev().find(|sh| matches!(sh.kind, deckcraft_model::ShapeKind::Ink { .. })) else {
|
||||
|
||||
@@ -17,7 +17,6 @@ mod geometry;
|
||||
mod hit;
|
||||
mod pointer;
|
||||
|
||||
pub(crate) use drag::MAX_GESTURE_POINTS;
|
||||
pub use hit::{line_xfrm, resize_box, table_rows, text_pos};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -276,8 +275,6 @@ pub struct ToolState {
|
||||
pub path_preview: Vec<Point>,
|
||||
pub path_smooth: bool,
|
||||
pub path_closing: bool,
|
||||
/// Whether the last freeform press added a vertex (it adds none at the point cap).
|
||||
pub press_added_vertex: bool,
|
||||
/// The shape in Edit Points: its vertices are handles instead of the box's.
|
||||
pub edit_points: Option<ShapeId>,
|
||||
/// The vertex (path, index) whose control handles show in Edit Points.
|
||||
|
||||
@@ -4,12 +4,8 @@ use super::{Drag, Hit, Point, Result, Session, ShapeId, ShapeKind, TextSel, Tool
|
||||
impl Session {
|
||||
pub(super) fn double_click(&mut self, p: Point, tol: f64) -> Result<Value> {
|
||||
if let Some(Drag::Freeform { mode, mut points, .. }) = self.tool.drag.clone() {
|
||||
// The double-click's own click added a duplicate vertex (none at the point cap, where
|
||||
// the existing vertices are all kept).
|
||||
while self.tool.press_added_vertex
|
||||
&& points.len() > 2
|
||||
&& points.last().zip(points.get(points.len() - 2)).is_some_and(|(a, b)| (*a - *b).hypot() <= tol)
|
||||
{
|
||||
// The double-click's own click added a duplicate vertex.
|
||||
while points.len() > 2 && points.last().zip(points.get(points.len() - 2)).is_some_and(|(a, b)| (*a - *b).hypot() <= tol) {
|
||||
points.pop();
|
||||
}
|
||||
self.tool.drag = Some(Drag::Freeform { mode, points, pressed: false });
|
||||
|
||||
@@ -12,16 +12,9 @@ struct MoveDrag {
|
||||
enter_text: Option<(ShapeId, Point)>,
|
||||
}
|
||||
|
||||
/// Samples one ink or freeform gesture keeps; later pointer moves extend it no further.
|
||||
pub(crate) const MAX_GESTURE_POINTS: usize = 20_000;
|
||||
|
||||
impl Session {
|
||||
/// Update the active drag interaction using the pointer position and modifiers.
|
||||
pub(super) fn drag(&mut self, p: Point, mods: Mods, tol: f64) -> Result<Value> {
|
||||
if matches!(self.tool.drag, Some(Drag::Ink { .. })) {
|
||||
self.extend_ink(p);
|
||||
return Ok(Value::Null);
|
||||
}
|
||||
let Some(drag) = self.tool.drag.clone() else { return Ok(Value::Null) };
|
||||
match drag {
|
||||
Drag::Guide { index } => {
|
||||
@@ -188,7 +181,7 @@ impl Session {
|
||||
}
|
||||
Drag::Freeform { mode, mut points, pressed } => {
|
||||
// Dragging scribbles (except for curves, which only take clicked points).
|
||||
if pressed && mode != "curve" && points.len() < MAX_GESTURE_POINTS && points.last().is_none_or(|l| (*l - p).hypot() >= 1.5) {
|
||||
if pressed && mode != "curve" && points.last().is_none_or(|l| (*l - p).hypot() >= 1.5) {
|
||||
points.push(p);
|
||||
}
|
||||
let mut v = points.clone();
|
||||
@@ -198,24 +191,15 @@ impl Session {
|
||||
self.tool.path_preview = v;
|
||||
self.tool.drag = Some(Drag::Freeform { mode, points, pressed });
|
||||
}
|
||||
// Handled above.
|
||||
Drag::Ink { .. } => {}
|
||||
Drag::Ink { mut points } => {
|
||||
points.push(self.ink_sample(p));
|
||||
self.tool.ink_preview = points.clone();
|
||||
self.tool.drag = Some(Drag::Ink { points });
|
||||
}
|
||||
}
|
||||
Ok(Value::Null)
|
||||
}
|
||||
|
||||
/// Ink grows by one sample per move: extend it (and its preview) in place rather than
|
||||
/// copying the whole stroke twice per event, up to [`MAX_GESTURE_POINTS`].
|
||||
fn extend_ink(&mut self, p: Point) {
|
||||
let sample = self.ink_sample(p);
|
||||
if let Some(Drag::Ink { points }) = self.tool.drag.as_mut()
|
||||
&& points.len() < MAX_GESTURE_POINTS
|
||||
{
|
||||
points.push(sample);
|
||||
self.tool.ink_preview.push(sample);
|
||||
}
|
||||
}
|
||||
|
||||
fn drag_move(&mut self, drag: MoveDrag, p: Point, mods: Mods, tol: f64) -> Result<Value> {
|
||||
let MoveDrag { ids, start, origs, moved, duplicate, enter_text } = drag;
|
||||
let mut d = p - start;
|
||||
|
||||
@@ -228,10 +228,7 @@ impl Session {
|
||||
self.tool.drag = Some(Drag::Freeform { mode, points, pressed: false });
|
||||
return self.finish_freeform(true);
|
||||
}
|
||||
self.tool.press_added_vertex = points.len() < super::MAX_GESTURE_POINTS;
|
||||
if self.tool.press_added_vertex {
|
||||
points.push(p);
|
||||
}
|
||||
points.push(p);
|
||||
self.tool.path_preview = points.clone();
|
||||
self.tool.drag = Some(Drag::Freeform { mode, points, pressed: true });
|
||||
return Ok(Value::Null);
|
||||
|
||||
Reference in New Issue
Block a user