diff --git a/README.de.md b/README.de.md index b4bb408..ad05c38 100644 --- a/README.de.md +++ b/README.de.md @@ -52,7 +52,7 @@ Die Oberfläche gibt es auf Deutsch und Englisch; ink-notes folgt der Systemspra ### Seiten und Ansicht - **Hintergründe:** blanko, kariert (5 mm), liniert (7,5 mm), Punktraster (5 mm). Die **Farbe von Karos, Linien und Punkten** ist wählbar (hell, mittel, kräftig, blau, grün) und gilt auch im Export. - **Papierfarbe** der Anzeige: weiß, creme, dunkelgrau, schwarz; auf dunklem Papier werden dunkle Stiftfarben auf Wunsch aufgehellt. Gespeichert und exportiert wird immer auf weißem Papier. -- **Durchgehendes Scrollen, endlose Fläche oder feste Seiten, je Notizbuch:** Beim Anlegen wählbar (Vorgabe: durchgehend; oder endlos), im ☰-Menü umschaltbar – dort auch auf feste Seiten (Seite für Seite blättern mit Pfeilen in der Werkzeugleiste). Durchgehend hängen die Seiten lückenlos untereinander, eine dunkle Linie markiert die Grenze; schnelles Wischen gleitet nach, und mit der Bildlaufleiste am Rand gegenüber der Werkzeugleiste springt man samt Seitenzahl durch lange PDFs; wer im unteren Fünftel der letzten Seite schreibt, bekommt automatisch eine neue Seite. Endlos ist eine einzige Fläche ohne Rand, auf der man in alle Richtungen schreibt; der PDF-Export enthält nur den beschriebenen Bereich. +- **Durchgehendes Scrollen, endlose Fläche oder feste Seiten, je Notizbuch:** Beim Anlegen wählbar (Vorgabe: durchgehend; oder endlos), im ☰-Menü umschaltbar – dort auch auf feste Seiten (Seite für Seite blättern mit Pfeilen in der Werkzeugleiste). Durchgehend hängen die Seiten lückenlos untereinander, eine dunkle Linie markiert die Grenze; schnelles Wischen gleitet nach, und mit der Bildlaufleiste am Rand gegenüber der Werkzeugleiste springt man samt Seitenzahl durch lange PDFs; wer im unteren Fünftel der letzten Seite schreibt, bekommt automatisch eine neue Seite. Endlos ist eine einzige Fläche ohne Rand, die oben links beginnt und nach rechts und unten beliebig weitergeht; der PDF-Export enthält nur den beschriebenen Bereich. - **Seiten einfügen und löschen:** ➕ in der Werkzeugleiste fügt nach der aktuellen Seite eine leere Seite ein. Löschen im ☰-Menü (mit Rückfrage); die zuletzt gelöschte Seite lässt sich dort wiederherstellen, solange die App läuft. - **Zoom** von 25 % bis 1000 % (`Strg + Mausrad`, zwei Finger, `Strg + +/-/0`), Verschieben mit mittlerer Maustaste, `Leertaste + Ziehen` oder einem Finger. - **Seiten** anhängen oder direkt nach der aktuellen Seite einfügen, **Seitentitel** vergeben; die Seitenübersicht zeigt Vorschaubilder oder ein Inhaltsverzeichnis. Seitentitel werden im PDF zu Lesezeichen. diff --git a/README.md b/README.md index c35a3e4..cd4e2c6 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ The interface is available in English and German; ink-notes follows the system l ### Pages and view - **Backgrounds:** blank, squared (5 mm), ruled (7.5 mm), dot grid (5 mm). The **color of squares, lines and dots** is selectable (light, medium, strong, blue, green) and also applies to exports. - **Paper color** on screen: white, cream, dark gray, black; on dark paper, dark pen colors can optionally be brightened. Saving and exporting always use white paper. -- **Continuous scrolling, endless canvas or fixed pages, per notebook:** chosen when creating a notebook (default: continuous; or endless) and switchable in the ☰ menu – where fixed pages are available too (page by page with arrows in the toolbar). In continuous mode the pages hang seamlessly below each other with a dark line marking the boundary. A quick swipe keeps gliding, and the scrollbar on the edge opposite the toolbar jumps through long PDFs with the page number shown. Writing in the bottom fifth of the last page adds a new page automatically. Endless is a single borderless canvas you can write on in every direction; the PDF export only contains the area you used. +- **Continuous scrolling, endless canvas or fixed pages, per notebook:** chosen when creating a notebook (default: continuous; or endless) and switchable in the ☰ menu – where fixed pages are available too (page by page with arrows in the toolbar). In continuous mode the pages hang seamlessly below each other with a dark line marking the boundary. A quick swipe keeps gliding, and the scrollbar on the edge opposite the toolbar jumps through long PDFs with the page number shown. Writing in the bottom fifth of the last page adds a new page automatically. Endless is a single borderless canvas that grows to the right and down as far as you like, starting at the top left; the PDF export only contains the area you used. - **Insert and delete pages:** ➕ in the toolbar inserts an empty page after the current one. Delete it in the ☰ menu (with confirmation); the most recently deleted page can be restored there while the app is running. - **Zoom** from 25 % to 1000 % (`Ctrl + mouse wheel`, two fingers, `Ctrl + +/-/0`), pan with the middle mouse button, `Space + drag` or one finger. - **Pages** can be appended or inserted right after the current page, and given **page titles**; the page overview shows thumbnails or a table of contents. Page titles become bookmarks in the PDF. diff --git a/docs/ARCHITECTURE.de.md b/docs/ARCHITECTURE.de.md index 899813e..8625f12 100644 --- a/docs/ARCHITECTURE.de.md +++ b/docs/ARCHITECTURE.de.md @@ -108,6 +108,8 @@ Schnelles Wischen mit dem Finger gleitet nach (`Fling` in `viewport.rs`): Die Ge ### 3.4 Endlose Fläche Ein endloses Notizbuch hat eine einzige Seite von 10 × 10 m (`ENDLESS_PAGE_SIZE`). Damit bleibt alles wie bei jeder anderen Seite – Eingabe, Treffertests, Undo, Speichern –, nur der Rand ist praktisch unerreichbar. `PageViewport::calculate_endless` wählt Maßstab und Lage so, als läge eine DIN-A4-Seite am Startausschnitt (`ENDLESS_HOME`, ein A4-Rahmen in der Mitte); 100 % Zoom und „Ansicht zurücksetzen“ zeigen also diesen Rahmen. Bilder und Textfelder richten ihre Größe bzw. ihren Umbruch an DIN A4 aus (`Page::reference_size`, `Page::text_right_edge`). +Die Fläche wächst nur nach rechts und unten: Nach jedem Verschieben oder Zoomen hält `CanvasState::clamp_endless` die linke obere Ecke des sichtbaren Bereichs auf oder hinter `Page::endless_scroll_limit` – 10 mm links und oberhalb des Startausschnitts bzw. von Inhalt, der weiter oben oder links liegt (in Notizbüchern aus 0.29/0.30). + Beim Wechsel von und zu Seiten (`Notebook::make_endless`/`make_paged`) wird der Inhalt um `ENDLESS_HOME` verschoben und der Verlauf der Seite geleert. Zurück zu Seiten geht nur, solange alles im Startausschnitt liegt. Export und Bilder einer Auswahl rastern bzw. exportieren nur einen Ausschnitt (`Page::cropped`), beim Export auf das Musterraster gelegt (`Page::endless_export_bounds`). ## 4. Rendering diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 79cf36c..5e21f8a 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -108,6 +108,8 @@ A quick finger swipe keeps gliding (`Fling` in `viewport.rs`): the speed is meas ### 3.4 Endless canvas An endless notebook has a single page of 10 × 10 m (`ENDLESS_PAGE_SIZE`). This keeps everything the same as with any other page – input, hit tests, undo, saving – only the edge is practically unreachable. `PageViewport::calculate_endless` chooses scale and position as if an A4 page lay at the starting area (`ENDLESS_HOME`, an A4 frame in the middle); 100 % zoom and "Reset view" therefore show this frame. Images and text boxes base their size and wrapping on A4 (`Page::reference_size`, `Page::text_right_edge`). +The canvas only grows to the right and down: after every pan or zoom, `CanvasState::clamp_endless` keeps the top left corner of the visible area at or beyond `Page::endless_scroll_limit` – 10 mm above and left of the starting area, or of content lying further up or left (in notebooks from 0.29/0.30). + When switching to and from pages (`Notebook::make_endless`/`make_paged`), the content is shifted by `ENDLESS_HOME` and the page's history is cleared. Switching back to pages is only possible while everything lies within the starting area. Export and images of a selection only rasterize or export a section (`Page::cropped`), aligned to the pattern grid on export (`Page::endless_export_bounds`). ## 4. Rendering diff --git a/src/canvas.rs b/src/canvas.rs index 1dc951b..1d5303a 100644 --- a/src/canvas.rs +++ b/src/canvas.rs @@ -843,6 +843,18 @@ impl CanvasState { changed } + /// Endless page: keeps the view right of and below `limit` (see + /// [`crate::document::Page::endless_scroll_limit`]). `true` if the view was corrected. + pub fn clamp_endless(&mut self, limit: glam::Vec2) -> bool { + let changed = self + .camera + .clamp_top_left([self.ui_inset_left, 0.0], limit, &self.viewport); + if changed { + self.update_uniforms(); + } + changed + } + /// Durchgehende Ansicht: Scrollt senkrecht, bis die Fenstermitte auf Höhe `y` /// (Koordinaten der aktuellen Seite) liegt. pub fn scroll_center_to(&mut self, y: f32) { diff --git a/src/document.rs b/src/document.rs index b290f4d..0720572 100644 --- a/src/document.rs +++ b/src/document.rs @@ -833,6 +833,15 @@ impl Page { page } + /// Endless page: the view never shows anything left of or above this point – the start + /// frame or the content, whichever reaches further, with a 10 mm margin. The canvas + /// therefore only grows to the right and down (Issue #30). + pub fn endless_scroll_limit(&self) -> glam::Vec2 { + let top_left = Self::selection_aabb(&self.strokes) + .map_or(ENDLESS_HOME, |content| content.min.min(ENDLESS_HOME)); + (top_left - ENDLESS_EXPORT_MARGIN).max(glam::Vec2::ZERO) + } + /// Exportausschnitt einer endlosen Seite: der Inhalt mit 10 mm Rand (leer: der /// Startausschnitt). Die linke obere Ecke liegt auf dem Musterraster. pub fn endless_export_bounds(&self) -> (glam::Vec2, glam::Vec2) { @@ -2800,6 +2809,30 @@ mod tests { assert!(endless.screen_to_page(-5000.0, -5000.0).is_some()); } + #[test] + fn test_endless_scroll_limit_includes_content_left_of_the_start() { + let mut page = Page::new(1, PageBackground::Grid5mm).with_size(ENDLESS_PAGE_SIZE); + assert_eq!( + page.endless_scroll_limit(), + ENDLESS_HOME - ENDLESS_EXPORT_MARGIN + ); + // Content right of and below the start frame does not move the limit + page.add_stroke(line(1, ENDLESS_HOME + 500.0, ENDLESS_HOME + 900.0)); + assert_eq!( + page.endless_scroll_limit(), + ENDLESS_HOME - ENDLESS_EXPORT_MARGIN + ); + // Older notebooks may have content further up and left: it stays reachable + let from = glam::Vec2::new(30_000.0, 40_000.0); + page.add_stroke(line(2, from, ENDLESS_HOME)); + let limit = page.endless_scroll_limit(); + assert!(limit.cmple(from - ENDLESS_EXPORT_MARGIN).all(), "{limit:?}"); + assert!( + limit.cmpgt(from - 2.0 * ENDLESS_EXPORT_MARGIN).all(), + "{limit:?}" + ); + } + #[test] fn test_endless_export_bounds_follow_content() { let mut page = Page::new(1, PageBackground::Grid5mm).with_size(ENDLESS_PAGE_SIZE); diff --git a/src/main.rs b/src/main.rs index d22666e..b30e7d6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3156,7 +3156,11 @@ fn main() -> Result<(), Box> { // Auswahl oder beim Ziehen des Lineals). Wer auf einer Nachbarseite // schreibt, bleibt dort, bis wieder gescrollt wird (Undo betrifft sie). canvas.set_fit_box(notebook.fit_box()); - if ui_state.continuous_scroll { + if notebook.is_endless() { + if canvas.clamp_endless(notebook.current_page().endless_scroll_limit()) { + fling.stop(); + } + } else if ui_state.continuous_scroll { let stack = notebook.page_stack(); if canvas.clamp_scroll(&stack) { // Am Anfang oder Ende des Notizbuchs angekommen diff --git a/src/viewport.rs b/src/viewport.rs index 03116b8..4b9229b 100644 --- a/src/viewport.rs +++ b/src/viewport.rs @@ -157,6 +157,24 @@ impl ViewportState { true } + /// Endless page: shifts the view so that the screen point `corner` (top left of the free + /// area) shows nothing left of or above `limit`. `true` if the view was corrected. + pub fn clamp_top_left( + &mut self, + corner: [f32; 2], + limit: Vec2, + page_viewport: &PageViewport, + ) -> bool { + let c = self.screen_to_page_unclamped(corner[0], corner[1], page_viewport); + let clamped = c.max(limit); + if (clamped - c).length() < 1e-3 { + return false; + } + self.pan_offset[0] += c.x - clamped.x; + self.pan_offset[1] += c.y - clamped.y; + true + } + /// Passt den Pan-Offset um eine Bildschirmverschiebung (in Fensterpixeln) an. pub fn pan_by_screen_delta( &mut self, @@ -567,6 +585,28 @@ mod tests { assert!((c.x - A4.x).abs() < 0.1, "{c:?}"); } + #[test] + fn test_endless_view_stays_right_of_and_below_the_limit() { + use crate::document::{ENDLESS_HOME, ENDLESS_PAGE_SIZE}; + let page_vp = PageViewport::calculate_endless(1280.0, 850.0, 24.0, (0.0, 0.0)); + let limit = ENDLESS_HOME - 100.0; + let mut vp = ViewportState::new(); + // The A4 start frame is centered: the wide window shows space left of it + assert!(vp.clamp_top_left([0.0, 0.0], limit, &page_vp)); + let corner = vp.screen_to_page_unclamped(0.0, 0.0, &page_vp); + assert!((corner.x - limit.x).abs() < 0.1, "{corner:?}"); + assert!(corner.y >= limit.y - 0.1, "{corner:?}"); + // Right and down stay free + vp.pan_by_screen_delta(-5000.0, -5000.0, &page_vp); + assert!(!vp.clamp_top_left([0.0, 0.0], limit, &page_vp)); + // Back up and left: stops at the limit + vp.pan_by_screen_delta(20000.0, 20000.0, &page_vp); + assert!(vp.clamp_top_left([0.0, 0.0], limit, &page_vp)); + let corner = vp.screen_to_page_unclamped(0.0, 0.0, &page_vp); + assert!((corner - limit).length() < 0.1, "{corner:?}"); + assert!(corner.cmplt(ENDLESS_PAGE_SIZE).all()); + } + #[test] fn test_viewport_zoom_limits() { let mut vp = ViewportState::new();