feat: endless canvas grows only to the right and down (Issue #30)

The view stops 10 mm left of and above the start frame, or above and left
of content that lies further out in notebooks from 0.29/0.30, so a new
endless notebook starts at the top left like a sheet of paper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
haraldandClaude Opus 5.5 committed 2026-10-21 11:57:26 +02:00
1 parent 08a5b014e0
commit 630fe31b76
8 files changed
+96 -3

No files matched your search

+12
View File
@@ -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) {
+33
View File
@@ -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);
+5 -1
View File
@@ -3156,7 +3156,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// 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
+40
View File
@@ -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();