diff --git a/src/document.rs b/src/document.rs index 1e11c47..acdb2d2 100644 --- a/src/document.rs +++ b/src/document.rs @@ -1,5 +1,6 @@ //! Notizbuch- und Seitenverwaltung mit DIN-A4-Koordinatensystem und Vektor-Stroke-Modell. +use std::collections::{HashMap, HashSet}; use std::ops::Range; use crate::canvas::InkVertex; @@ -613,18 +614,23 @@ impl Stroke { /// Berechnet die Bounding Box aus den aktuellen Punkten/Vertizes neu. pub fn recompute_aabb(&mut self) { + // Ohne Zwischenliste: läuft beim Ziehen einer Auswahl für jeden Strich und jede Bewegung + let bounds = |mut pts: Box + '_>| { + pts.next().map_or(Aabb::new(glam::Vec2::ZERO, glam::Vec2::ZERO), |first| { + pts.fold(Aabb::new(first, first), |mut aabb, p| { + aabb.expand_by_point(p); + aabb + }) + }) + }; self.aabb = if !self.points.is_empty() { - let pts: Vec = self.points.iter().map(|s| s.position).collect(); - Aabb::from_points(&pts) - } else if !self.vertices.is_empty() { - let pts: Vec = self - .vertices - .iter() - .map(|v| glam::Vec2::new(v.position[0], v.position[1])) - .collect(); - Aabb::from_points(&pts) + bounds(Box::new(self.points.iter().map(|s| s.position))) } else { - Aabb::new(glam::Vec2::ZERO, glam::Vec2::ZERO) + bounds(Box::new( + self.vertices + .iter() + .map(|v| glam::Vec2::new(v.position[0], v.position[1])), + )) }; } @@ -1066,6 +1072,7 @@ impl Page { /// Liefert Klone der Striche mit den angegebenen IDs. pub fn get_strokes(&self, ids: &[u64]) -> Vec { + let ids: HashSet = ids.iter().copied().collect(); self.strokes .iter() .filter(|s| ids.contains(&s.id)) @@ -1073,24 +1080,31 @@ impl Page { .collect() } + /// Ersetzt die Striche mit den IDs aus `modified` an ihrer Stelle. Über eine Tabelle + /// statt je Strich die ganze Seite zu durchsuchen: Beim Ziehen einer großen Auswahl + /// läuft das bei jeder Stiftbewegung. + fn replace_strokes(&mut self, modified: &[Stroke]) { + let index: HashMap = self + .strokes + .iter() + .enumerate() + .map(|(i, s)| (s.id, i)) + .collect(); + for m in modified { + if let Some(&pos) = index.get(&m.id) { + self.strokes[pos] = m.clone(); + } + } + } + /// Aktualisiert eine Menge geänderter Striche transaktional im History-Stack. pub fn modify_strokes(&mut self, modified: Vec) { if modified.is_empty() { return; } let ids: Vec = modified.iter().map(|s| s.id).collect(); - let before: Vec = self - .strokes - .iter() - .filter(|s| ids.contains(&s.id)) - .cloned() - .collect(); - - for m in modified.iter() { - if let Some(pos) = self.strokes.iter().position(|s| s.id == m.id) { - self.strokes[pos] = m.clone(); - } - } + let before = self.get_strokes(&ids); + self.replace_strokes(&modified); self.history.push(HistoryAction::ModifyStrokes { before, @@ -1101,9 +1115,33 @@ impl Page { /// Aktualisiert Striche flüchtig (z. B. während des Ziehens / Dragging), OHNE die History zu verändern. pub fn update_strokes_ephemeral(&mut self, modified: &[Stroke]) { - for m in modified { - if let Some(pos) = self.strokes.iter().position(|s| s.id == m.id) { - self.strokes[pos] = m.clone(); + self.replace_strokes(modified); + self.is_dirty = true; + } + + /// Wie [`Self::update_strokes_ephemeral`] für eine Transformation der Ausgangsstriche + /// `originals`: setzt die Striche auf der Seite auf ihren Ausgangszustand zurück und wendet + /// `transform` an. Verwendet die Puffer der Seitenstriche weiter, statt bei jeder + /// Stiftbewegung die ganze Auswahl zweimal zu kopieren. + pub fn transform_strokes_ephemeral( + &mut self, + originals: &[Stroke], + transform: impl Fn(&mut Stroke), + ) { + let index: HashMap = self + .strokes + .iter() + .enumerate() + .map(|(i, s)| (s.id, i)) + .collect(); + for original in originals { + if let Some(&pos) = index.get(&original.id) { + let stroke = &mut self.strokes[pos]; + // Nur was Verschieben, Drehen und Skalieren verändern + stroke.points.clone_from(&original.points); + stroke.vertices.clone_from(&original.vertices); + stroke.shape.clone_from(&original.shape); + transform(stroke); } } self.is_dirty = true; @@ -1114,11 +1152,7 @@ impl Page { if before.is_empty() || after.is_empty() { return; } - for m in &after { - if let Some(pos) = self.strokes.iter().position(|s| s.id == m.id) { - self.strokes[pos] = m.clone(); - } - } + self.replace_strokes(&after); self.history .push(HistoryAction::ModifyStrokes { before, after }); self.is_dirty = true; @@ -2673,6 +2707,13 @@ mod tests { // History should not have been updated yet assert!(!page.history.can_redo()); + // Ziehen über die Ausgangsstriche: jede Bewegung gilt ab dem Ausgangszustand + for delta in [glam::Vec2::new(5.0, 0.0), glam::Vec2::new(20.0, 30.0)] { + page.transform_strokes_ephemeral(std::slice::from_ref(&s), |st| st.translate(delta)); + } + assert_eq!(page.strokes[0], moved); + assert!(!page.history.can_redo()); + // Transactional commit (mouse release) let before = vec![s.clone()]; let after = vec![moved.clone()]; diff --git a/src/main.rs b/src/main.rs index acb0afa..80499d3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -691,62 +691,65 @@ fn selection_press( _ => return false, }; selection_state.drag_mode = mode; + selection_state.pending_drag = None; true } +/// Wendet die zuletzt gemeldete Zugposition der Auswahl an (siehe [`selection_drag`]). +fn flush_selection_drag( + page: &mut ink_notes::document::Page, + canvas: &mut CanvasState, + selection_state: &mut SelectionState, +) { + if let Some(page_pt) = selection_state.pending_drag.take() { + selection_drag(page_pt, page, canvas, selection_state); + } +} + /// Auswahlwerkzeug: Ziehen nach `page_pt` – Rahmen aufziehen oder die Auswahl verschieben, -/// drehen bzw. skalieren (vorläufig, ohne Undo-Schritt). +/// drehen bzw. skalieren (vorläufig, ohne Undo-Schritt). Bei Stiftbewegungen über +/// `pending_drag` und [`flush_selection_drag`] nur einmal je Bild, weil das bei großen +/// Auswahlen teuer ist. fn selection_drag( page_pt: Vec2, page: &mut ink_notes::document::Page, canvas: &mut CanvasState, selection_state: &mut SelectionState, ) { - let mut updated = match &mut selection_state.drag_mode { + match &mut selection_state.drag_mode { DragMode::BoxSelect { current_page, .. } => { *current_page = page_pt; return; } DragMode::None => return, DragMode::Move { - initial_strokes, .. - } - | DragMode::Rotate { - initial_strokes, .. - } - | DragMode::Scale { - initial_strokes, .. - } => initial_strokes.clone(), - }; - match &selection_state.drag_mode { - DragMode::Move { start_page, .. } => { + start_page, + initial_strokes, + } => { let delta = page_pt - *start_page; - updated.iter_mut().for_each(|s| s.translate(delta)); + page.transform_strokes_ephemeral(initial_strokes, |s| s.translate(delta)); } DragMode::Rotate { center_page, start_angle, - .. + initial_strokes, } => { - let angle = (page_pt.y - center_page.y).atan2(page_pt.x - center_page.x) - start_angle; - updated - .iter_mut() - .for_each(|s| s.rotate(*center_page, angle)); + let angle = (page_pt.y - center_page.y).atan2(page_pt.x - center_page.x) - *start_angle; + let center = *center_page; + page.transform_strokes_ephemeral(initial_strokes, |s| s.rotate(center, angle)); } DragMode::Scale { fixed_corner_page, start_pos_page, keep_aspect, + initial_strokes, .. } => { - let factors = scale_factors(*fixed_corner_page, *start_pos_page, page_pt, *keep_aspect); - updated - .iter_mut() - .for_each(|s| s.scale(*fixed_corner_page, factors)); + let fixed = *fixed_corner_page; + let factors = scale_factors(fixed, *start_pos_page, page_pt, *keep_aspect); + page.transform_strokes_ephemeral(initial_strokes, |s| s.scale(fixed, factors)); } - DragMode::BoxSelect { .. } | DragMode::None => return, } - page.update_strokes_ephemeral(&updated); canvas.sync_page(page); } @@ -759,7 +762,10 @@ fn selection_release( manifest: &mut NotebookManifest, autosave: &mut AutosaveController, selection_state: &mut SelectionState, + canvas: &mut CanvasState, ) -> bool { + // Die letzte Bewegung gehört noch zum Ziehen + flush_selection_drag(notebook.current_page_mut(), canvas, selection_state); match std::mem::take(&mut selection_state.drag_mode) { DragMode::BoxSelect { start_page, @@ -1618,7 +1624,7 @@ fn main() -> Result<(), Box> { } if is_stylus_selecting { is_stylus_selecting = false; - selection_release(false, &mut notebook, &mut manifest, &mut autosave, &mut selection_state); + selection_release(false, &mut notebook, &mut manifest, &mut autosave, &mut selection_state, &mut canvas); window.request_redraw(); } if let Some(tool) = tool_before_button.take() { @@ -1714,7 +1720,7 @@ fn main() -> Result<(), Box> { } } else if is_stylus_selecting { if let Some(page_pt) = canvas.screen_to_page(sx, sy) { - selection_drag(page_pt, notebook.current_page_mut(), &mut canvas, &mut selection_state); + selection_state.pending_drag = Some(page_pt); window.request_redraw(); } } else if is_stylus_drawing && is_drawing { @@ -1858,7 +1864,7 @@ fn main() -> Result<(), Box> { if is_stylus_selecting { is_stylus_selecting = false; - if selection_release(false, &mut notebook, &mut manifest, &mut autosave, &mut selection_state) { + if selection_release(false, &mut notebook, &mut manifest, &mut autosave, &mut selection_state, &mut canvas) { window.request_redraw(); } } @@ -2626,7 +2632,7 @@ fn main() -> Result<(), Box> { } if ui_state.active_tool == ActiveTool::Select && !is_stylus_selecting - && selection_release(modifiers.shift_key(), &mut notebook, &mut manifest, &mut autosave, &mut selection_state) + && selection_release(modifiers.shift_key(), &mut notebook, &mut manifest, &mut autosave, &mut selection_state, &mut canvas) { window.request_redraw(); } @@ -2682,7 +2688,7 @@ fn main() -> Result<(), Box> { window.request_redraw(); } else if ui_state.active_tool == ActiveTool::Select && selection_state.drag_mode != DragMode::None { if let Some(page_pt) = canvas.screen_to_page(sx, sy) { - selection_drag(page_pt, notebook.current_page_mut(), &mut canvas, &mut selection_state); + selection_state.pending_drag = Some(page_pt); window.request_redraw(); } } else if is_erasing { @@ -2718,9 +2724,9 @@ fn main() -> Result<(), Box> { if is_pointer_in_ui { return; } - // Durchgehende Ansicht: Mausrad scrollt (mit Umschalt waagerecht), - // Strg + Mausrad zoomt - if ui_state.continuous_scroll && !modifiers.control_key() { + // Durchgehende Ansicht und endlose Fläche: Mausrad scrollt (mit Umschalt + // waagerecht), Strg + Mausrad zoomt + if (ui_state.continuous_scroll || ui_state.endless) && !modifiers.control_key() { let (mut dx, mut dy) = match delta { MouseScrollDelta::LineDelta(x, y) => { let line = 60.0 * window.scale_factor() as f32; @@ -2983,7 +2989,7 @@ fn main() -> Result<(), Box> { } } else if ui_state.active_tool == ActiveTool::Select && selection_state.drag_mode != DragMode::None { if let Some(page_pt) = canvas.screen_to_page(tx, ty) { - selection_drag(page_pt, notebook.current_page_mut(), &mut canvas, &mut selection_state); + selection_state.pending_drag = Some(page_pt); window.request_redraw(); } } @@ -3010,7 +3016,7 @@ fn main() -> Result<(), Box> { } if ui_state.active_tool == ActiveTool::Select - && selection_release(false, &mut notebook, &mut manifest, &mut autosave, &mut selection_state) + && selection_release(false, &mut notebook, &mut manifest, &mut autosave, &mut selection_state, &mut canvas) { window.request_redraw(); } @@ -3053,6 +3059,7 @@ fn main() -> Result<(), Box> { } WindowEvent::RedrawRequested => { + flush_selection_drag(notebook.current_page_mut(), &mut canvas, &mut selection_state); // 0. Durchgehende Ansicht: Nach dem Scrollen oder Zoomen wird die Seite in // der Fenstermitte zur aktuellen (nicht während eines Strichs, einer // Auswahl oder beim Ziehen des Lineals). Wer auf einer Nachbarseite diff --git a/src/selection.rs b/src/selection.rs index fa6585a..35eccd9 100644 --- a/src/selection.rs +++ b/src/selection.rs @@ -88,6 +88,9 @@ pub enum SelectionHandleHit { pub struct SelectionState { pub selected_ids: Vec, pub drag_mode: DragMode, + /// Letzte Zugposition, die noch nicht angewendet ist: Der Stift meldet sich öfter, als + /// gezeichnet wird; angewendet wird einmal je Bild (siehe `flush_selection_drag`). + pub pending_drag: Option, pub clipboard: Vec, /// Von Hand umgeschaltetes Seitenverhältnis-Schloss und die Auswahl (IDs), für die es /// gilt. Ändert sich die Auswahl, gilt wieder die Vorgabe (siehe [`Self::keeps_aspect`]). @@ -99,6 +102,7 @@ impl SelectionState { Self { selected_ids: Vec::new(), drag_mode: DragMode::None, + pending_drag: None, clipboard: Vec::new(), aspect_lock: None, } @@ -109,10 +113,11 @@ impl SelectionState { pub fn keeps_aspect(&self, page: &Page) -> bool { match &self.aspect_lock { Some((ids, locked)) if *ids == self.selected_ids => *locked, + // Läuft bei jedem Bild: erst die (wenigen) Bilder suchen, ohne Striche zu kopieren _ => page - .get_strokes(&self.selected_ids) + .strokes .iter() - .any(|s| s.is_image()), + .any(|s| s.is_image() && self.selected_ids.contains(&s.id)), } } @@ -159,8 +164,13 @@ impl SelectionState { /// Berechnet die gemeinsame AABB aller ausgewählten Objekte. pub fn get_selection_aabb(&self, page: &Page) -> Option { - let strokes = page.get_strokes(&self.selected_ids); - Page::selection_aabb(&strokes) + // Läuft bei jedem Bild: ohne die ausgewählten Striche zu kopieren + let ids: std::collections::HashSet = self.selected_ids.iter().copied().collect(); + page.strokes + .iter() + .filter(|s| ids.contains(&s.id)) + .map(|s| s.aabb) + .reduce(|a, b| a.union(&b)) } /// Führt Hit-Testing auf die Auswahlgriffe (Drehen, Ecken, Körper) in Bildschirmkoordinaten