"""Run with python -m pytest blender/scripts/test_page_sizing.py (no Blender needed). The derivation is only usable because its own residual proves the layout is a bounds-projection. These tests pin both halves: that a real projection recovers the measured page, and that an unwrap is refused rather than silently sized. """ import unittest from page_sizing import ( DEFAULT_PX_PER_MM, DerivedPage, PageSizingError, derive_page, ) #: Measured on the shipped blends (scale-normalised view matrix, so the camera object scale #: is divided out). These are the numbers the feature exists to reproduce. FINGER_UV_1_MM = (17.5116, 15.9772) FINGER_UV_2_MM = (17.5114, 15.9820) XYNOVA_UV_1_MM = (21.1989, 23.2003) XYNOVA_UV_2_MM = (21.2001, 23.2003) def projection_samples(width_mm, height_mm, steps=40, v_flip=False): """Samples of a bounds-projection: UV fills [0,1]^2 over a `width x height` view plane.""" uvs = [] planes = [] for i in range(steps + 1): for j in range(steps + 1): x = width_mm * i / steps y = height_mm * j / steps u = i / steps v = (1.0 - j / steps) if v_flip else (j / steps) uvs.append((u, v)) planes.append((x, y)) return uvs, planes class DerivationTests(unittest.TestCase): def test_recovers_the_finger_r01_uv_1_page(self): page = derive_page(*projection_samples(*FINGER_UV_1_MM)) self.assertAlmostEqual(page.page_width_mm, FINGER_UV_1_MM[0], places=2) self.assertAlmostEqual(page.page_height_mm, FINGER_UV_1_MM[1], places=2) def test_recovers_every_measured_page(self): for width_mm, height_mm in ( FINGER_UV_1_MM, FINGER_UV_2_MM, XYNOVA_UV_1_MM, XYNOVA_UV_2_MM ): with self.subTest(size=(width_mm, height_mm)): page = derive_page(*projection_samples(width_mm, height_mm)) self.assertAlmostEqual(page.page_width_mm, width_mm, delta=0.01) self.assertAlmostEqual(page.page_height_mm, height_mm, delta=0.01) def test_xynova_page_is_portrait_where_the_configured_page_was_landscape(self): """The defect in one assertion: 0.914 vs the configured 1.128.""" page = derive_page(*projection_samples(*XYNOVA_UV_1_MM)) self.assertLess(page.page_aspect, 1.0) self.assertAlmostEqual(page.page_aspect, 0.9137, places=3) configured = 18.5 / 16.4 self.assertGreater(abs(configured - page.page_aspect), 0.2) def test_real_slopes_round_trip(self): """Feeding the slopes AND ranges measured off the blend recovers the measured extent. The measured `uv_1` affine is u = 0.057105098 x + 0.472297765 over x in [-8.270676, 9.240895]; the intercept is only consistent with that range, which is why a synthetic range would fail the [0,1]^2 bounds check. """ x_min, x_max = -8.270676, 9.240895 y_min, y_max = -7.890431, 8.086782 slope_u, intercept_u = 0.057105098, 0.472297765 slope_v, intercept_v = 0.062589139, 0.493855294 uvs, planes = [], [] for i in range(101): x = x_min + (x_max - x_min) * i / 100.0 y = y_min + (y_max - y_min) * i / 100.0 uvs.append((slope_u * x + intercept_u, slope_v * y + intercept_v)) planes.append((x, y)) page = derive_page(uvs, planes) self.assertAlmostEqual(page.page_width_mm, 17.5116, delta=0.01) self.assertAlmostEqual(page.page_height_mm, 15.9772, delta=0.01) def test_residual_is_essentially_zero_for_a_projection(self): page = derive_page(*projection_samples(20.0, 10.0)) self.assertLess(page.residual_u, 1e-12) self.assertLess(page.residual_v, 1e-12) def test_mirrored_v_axis_is_still_sized(self): page = derive_page(*projection_samples(20.0, 10.0, v_flip=True)) self.assertAlmostEqual(page.page_width_mm, 20.0, delta=0.01) self.assertAlmostEqual(page.page_height_mm, 10.0, delta=0.01) class VerificationTests(unittest.TestCase): def test_unwrap_is_refused_and_names_the_escape_hatch(self): """A non-affine u-vs-x map is the `compute` case in miniature.""" uvs, planes = [], [] for i in range(101): x = i * 0.1 uvs.append((x / 10.0 + 0.05 * (x / 10.0) ** 2, i / 100.0)) planes.append((x, i * 0.1)) with self.assertRaises(PageSizingError) as caught: derive_page(uvs, planes) message = str(caught.exception) self.assertIn("not a bounds-projection", message) self.assertIn("page: fixed", message) def test_residual_tolerance_boundary(self): uvs, planes = projection_samples(20.0, 20.0, steps=20) # Nudge a single sample just inside, then just outside, the tolerance. for offset, should_pass in ((0.5e-5, True), (5e-5, False)): nudged = list(uvs) nudged[7] = (nudged[7][0] + offset, nudged[7][1]) if should_pass: derive_page(nudged, planes) else: with self.assertRaises(PageSizingError): derive_page(nudged, planes) def test_uv_box_not_unit_square_is_refused(self): """A layout fitted to something other than the view bounds has no derivable page.""" uvs, planes = projection_samples(20.0, 10.0) squashed = [(u * 0.5, v) for u, v in uvs] with self.assertRaises(PageSizingError) as caught: derive_page(squashed, planes) self.assertIn("[0,1]^2", str(caught.exception)) def test_flat_axis_is_refused(self): planes = [(x, 0.0) for x, _ in ((i * 0.1, 0.0) for i in range(11))] uvs = [(i / 10.0, i / 10.0) for i in range(11)] with self.assertRaises(PageSizingError) as caught: derive_page(uvs, planes) self.assertIn("no extent", str(caught.exception)) def test_too_few_samples_is_refused(self): with self.assertRaises(PageSizingError): derive_page([(0.0, 0.0)], [(0.0, 0.0)]) def test_mismatched_lengths_are_refused(self): with self.assertRaises(PageSizingError) as caught: derive_page([(0.0, 0.0), (1.0, 1.0)], [(0.0, 0.0)]) self.assertIn("parallel", str(caught.exception)) def test_bad_px_per_mm_is_refused(self): uvs, planes = projection_samples(20.0, 10.0) for bad in (0, -1.0, "many"): with self.assertRaises(PageSizingError): derive_page(uvs, planes, px_per_mm=bad) class InvariantTests(unittest.TestCase): def test_page_and_raster_aspects_are_equal_by_construction(self): """This is what stops `_warn_if_page_aspect_differs` firing.""" for width_mm, height_mm in ( (17.5116, 15.9772), (21.1989, 23.2003), (1.0, 1.0), (100.0, 3.0), (3.0, 100.0) ): with self.subTest(size=(width_mm, height_mm)): page = derive_page(*projection_samples(width_mm, height_mm)) self.assertAlmostEqual(page.page_aspect, page.raster_aspect, places=12) def test_page_is_the_rounded_raster_over_the_density(self): page = derive_page(*projection_samples(17.5116, 15.9772)) self.assertAlmostEqual( page.page_width_mm, page.raster_width_px / DEFAULT_PX_PER_MM, places=12 ) self.assertAlmostEqual( page.page_height_mm, page.raster_height_px / DEFAULT_PX_PER_MM, places=12 ) def test_default_density_reproduces_the_shipped_rasters(self): """200 px/mm is what 3280x16.4 and 3700x18.5 already are, exactly.""" self.assertEqual(DEFAULT_PX_PER_MM, 200.0) self.assertAlmostEqual(3280 / 16.4, 200.0, places=9) self.assertAlmostEqual(3200 / 16.0, 200.0, places=9) self.assertAlmostEqual(3700 / 18.5, 200.0, places=9) def test_rounding_error_stays_far_below_the_alignment_tolerance(self): """At 200 px/mm the page can be off by at most 0.0025 mm; tolerance is 0.1 mm.""" page = derive_page(*projection_samples(*XYNOVA_UV_1_MM)) self.assertLessEqual(abs(page.page_width_mm - XYNOVA_UV_1_MM[0]), 0.5 / 200.0) self.assertLessEqual(abs(page.page_height_mm - XYNOVA_UV_1_MM[1]), 0.5 / 200.0) def test_describe_is_readable(self): page = derive_page(*projection_samples(20.0, 10.0)) self.assertIn("page 20.000 x 10.000 mm", page.describe()) self.assertIn("200 px/mm", page.describe()) if __name__ == "__main__": unittest.main()