diff --git a/lib/editor/layout/viewport_fit.dart b/lib/editor/layout/viewport_fit.dart new file mode 100644 index 0000000..d409846 --- /dev/null +++ b/lib/editor/layout/viewport_fit.dart @@ -0,0 +1,39 @@ +// lib/editor/layout/viewport_fit.dart +// +// Pure fit/centering math for the viewport's initial transform + the reader +// "fit width / fit page" actions (F3). Today this lives as untested ad-hoc +// arithmetic in pen_editor_screen._centerPage; extracting it here makes it +// testable and shared by the new viewport widget. +// +// Content coordinates are scale-1 logical px; the returned scale + offset place +// content inside the viewport. No widgets beyond dart:ui Size/Offset. + +import 'dart:ui' show Offset, Size; + +/// Scale so content width fills the viewport width (fit-to-width, the +/// continuous-single default). 0 for non-positive content width. +double fitWidthScale(Size content, double viewportWidth) { + if (content.width <= 0) return 0; + return viewportWidth / content.width; +} + +/// Scale so the whole content fits inside the viewport (letterboxed) — +/// min(widthFit, heightFit). 0 for non-positive content extents. +double fitPageScale(Size content, Size viewport) { + if (content.width <= 0 || content.height <= 0) return 0; + final w = viewport.width / content.width; + final h = viewport.height / content.height; + return w < h ? w : h; +} + +/// Top-left translation that centers [content] scaled by [scale] within +/// [viewport]. When the scaled content is larger than the viewport on an axis +/// the offset is negative (content overflows equally on both sides). +Offset centerOffset(Size content, Size viewport, double scale) { + final scaledW = content.width * scale; + final scaledH = content.height * scale; + return Offset( + (viewport.width - scaledW) / 2.0, + (viewport.height - scaledH) / 2.0, + ); +} diff --git a/test/viewport_fit_test.dart b/test/viewport_fit_test.dart new file mode 100644 index 0000000..4d85b18 --- /dev/null +++ b/test/viewport_fit_test.dart @@ -0,0 +1,49 @@ +// Tests for viewport fit/centering math (F3). + +import 'dart:ui' show Offset, Size; + +import 'package:flutter_test/flutter_test.dart'; + +import 'package:badnote/editor/layout/viewport_fit.dart'; + +void main() { + test('fitWidthScale fills the viewport width', () { + expect(fitWidthScale(const Size(500, 800), 1000), 2.0); + expect(fitWidthScale(const Size(0, 800), 1000), 0); + }); + + test('fitPageScale is the limiting (min) axis, letterboxed', () { + // content 500x1000 into 1000x1000: w-fit=2, h-fit=1 → min 1. + expect(fitPageScale(const Size(500, 1000), const Size(1000, 1000)), 1.0); + // content 1000x500 into 1000x1000: w-fit=1, h-fit=2 → min 1. + expect(fitPageScale(const Size(1000, 500), const Size(1000, 1000)), 1.0); + expect(fitPageScale(const Size(0, 0), const Size(100, 100)), 0); + }); + + test('centerOffset centers smaller content', () { + // content 200x100 at scale 1 in 1000x600 → offset (400, 250). + expect( + centerOffset(const Size(200, 100), const Size(1000, 600), 1.0), + const Offset(400, 250), + ); + }); + + test('centerOffset is negative when scaled content overflows', () { + // content 1000x1000 at scale 2 = 2000 in a 1000 viewport → (-500, -500). + expect( + centerOffset(const Size(1000, 1000), const Size(1000, 1000), 2.0), + const Offset(-500, -500), + ); + }); + + test('fit-to-width then center yields a left-anchored x for a tall page', () { + const content = Size(500, 2000); + const viewport = Size(1000, 800); + final scale = fitWidthScale(content, viewport.width); // 2.0 + final off = centerOffset(content, viewport, scale); + // Scaled width = 1000 == viewport width → x offset 0. + expect(off.dx, 0); + // Scaled height 4000 > 800 → negative y (overflows, scrollable). + expect(off.dy, lessThan(0)); + }); +}