From 98b78d29e6302de23d0ac5ababd249e2a2d24363 Mon Sep 17 00:00:00 2001 From: Harald Date: Sun, 18 Oct 2026 21:02:56 +0200 Subject: [PATCH] feat: Bilder beim Skalieren nicht verzerren (Issue #29) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enthält die Auswahl ein Bild, skaliert der Eckgriff gleichmäßig, das Seitenverhältnis bleibt erhalten. Mit gedrückter Umschalttaste gilt das auch für Striche und Formen. Der Faktor ist die Projektion der Zugposition auf die Diagonale der Auswahl. Co-Authored-By: Claude Opus 5.5 --- src/main.rs | 32 ++++++++++++++-------------- src/selection.rs | 54 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 17 deletions(-) diff --git a/src/main.rs b/src/main.rs index 36db63a..cbf0d0a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -31,7 +31,7 @@ use ink_notes::osk::{self, KeyboardRequester}; use ink_notes::pdf_doc::PdfRenderService; use ink_notes::platform::{self, DisplayBackend}; use ink_notes::selection::{ - render_selection_overlay, DragMode, SelectionHandleHit, SelectionState, + render_selection_overlay, scale_factors, DragMode, SelectionHandleHit, SelectionState, }; use ink_notes::settings::{ SidebarSide, StylusAction, StylusButton, UiSettings, MAX_STYLUS_BUTTONS, @@ -649,12 +649,18 @@ fn selection_press( .. }, _, - ) => DragMode::Scale { - fixed_corner_page, - start_pos_page, - initial_box_size, - initial_strokes: page.get_strokes(&selection_state.selected_ids), - }, + ) => { + let initial_strokes = page.get_strokes(&selection_state.selected_ids); + // Bilder nicht verzerren (Issue #29); Shift hält das Verhältnis auch bei Strichen + let keep_aspect = shift || initial_strokes.iter().any(|s| s.is_image()); + DragMode::Scale { + fixed_corner_page, + start_pos_page, + initial_box_size, + initial_strokes, + keep_aspect, + } + } (SelectionHandleHit::Body, Some(p)) => DragMode::Move { start_page: p, initial_strokes: page.get_strokes(&selection_state.selected_ids), @@ -729,18 +735,10 @@ fn selection_drag( DragMode::Scale { fixed_corner_page, start_pos_page, + keep_aspect, .. } => { - let orig = *start_pos_page - *fixed_corner_page; - let new = page_pt - *fixed_corner_page; - let factor = |n: f32, o: f32| { - if o.abs() > 1.0 { - (n / o).clamp(0.05, 20.0) - } else { - 1.0 - } - }; - let factors = Vec2::new(factor(new.x, orig.x), factor(new.y, orig.y)); + 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)); diff --git a/src/selection.rs b/src/selection.rs index c4fa1d0..24b8846 100644 --- a/src/selection.rs +++ b/src/selection.rs @@ -36,9 +36,34 @@ pub enum DragMode { start_pos_page: Vec2, initial_box_size: Vec2, initial_strokes: Vec, + /// Seitenverhältnis beibehalten (Bilder in der Auswahl oder Shift gedrückt) + keep_aspect: bool, }, } +/// Skalierungsfaktoren, wenn der Eckgriff von `start` (relativ zur festen Ecke `fixed`) +/// nach `current` gezogen wird. Mit `keep_aspect` wird gleichmäßig skaliert: der Faktor ist +/// die Projektion der Zugposition auf die Diagonale, damit das Seitenverhältnis bleibt. +pub fn scale_factors(fixed: Vec2, start: Vec2, current: Vec2, keep_aspect: bool) -> Vec2 { + let orig = start - fixed; + let new = current - fixed; + if keep_aspect { + let len_sq = orig.length_squared(); + if len_sq <= 1.0 { + return Vec2::ONE; + } + return Vec2::splat((new.dot(orig) / len_sq).clamp(0.05, 20.0)); + } + let factor = |n: f32, o: f32| { + if o.abs() > 1.0 { + (n / o).clamp(0.05, 20.0) + } else { + 1.0 + } + }; + Vec2::new(factor(new.x, orig.x), factor(new.y, orig.y)) +} + /// Ergebnis eines Hit-Tests auf Auswahl-Griffe (Handles). #[derive(Debug, Clone, Copy, PartialEq)] pub enum SelectionHandleHit { @@ -523,6 +548,35 @@ mod tests { assert!((orig_pos.x - restored_pos.x).abs() < 1e-4); } + #[test] + fn test_scale_factors_free_and_keep_aspect() { + let fixed = Vec2::new(0.0, 0.0); + let start = Vec2::new(200.0, 100.0); + // Frei: Breite und Höhe unabhängig + let free = scale_factors(fixed, start, Vec2::new(400.0, 100.0), false); + assert_eq!(free, Vec2::new(2.0, 1.0)); + // Seitenverhältnis fest: gleicher Faktor in beiden Richtungen + let locked = scale_factors(fixed, start, Vec2::new(400.0, 100.0), true); + assert!((locked.x - locked.y).abs() < 1e-6); + assert!((locked.x - 1.8).abs() < 1e-4); + // Entlang der Diagonale genau verdoppelt + let diag = scale_factors(fixed, start, Vec2::new(400.0, 200.0), true); + assert!((diag - Vec2::splat(2.0)).length() < 1e-5); + // Über die feste Ecke hinaus: kein Spiegeln + let past = scale_factors(fixed, start, Vec2::new(-50.0, -50.0), true); + assert_eq!(past, Vec2::splat(0.05)); + // Senkrechte Linie (Breite 0): frei bleibt x unverändert, fest skaliert beides + let line = Vec2::new(0.0, 100.0); + assert_eq!( + scale_factors(fixed, line, Vec2::new(30.0, 200.0), false), + Vec2::new(1.0, 2.0) + ); + assert_eq!( + scale_factors(fixed, line, Vec2::new(30.0, 200.0), true), + Vec2::splat(2.0) + ); + } + #[test] fn test_recolor_ignores_images() { let mut page = Page::new(1, PageBackground::Blank);