1 """

2 Page renderer helper for pyPhotoAlbum

3

4 This module provides a unified coordinate system for rendering pages and their elements.

5 All coordinate transformations are centralized here to ensure consistency.

6

7 Coordinate Systems:

8 - Page-local: Coordinates in millimeters relative to the page's top-left corner

9 - Pixel: Coordinates in pixels at working DPI

10 - Screen: Coordinates on screen after applying zoom and pan

11 """

12

13 from typing import Tuple, Optional

14 from pyPhotoAlbum.gl_imports import glPushMatrix, glPopMatrix, glScalef, glTranslatef

15

16

17 class PageRenderer:

18 """

19 Handles rendering and coordinate transformations for a single page.

20

21 This class encapsulates all coordinate transformations needed to render

22 a page and its elements consistently.

23 """

24

25 def __init__(

26 self, page_width_mm: float, page_height_mm: float, screen_x: float, screen_y: float, dpi: int, zoom: float

27 ):

28 """

29 Initialize a page renderer.

30

31 Args:

32 page_width_mm: Page width in millimeters

33 page_height_mm: Page height in millimeters

34 screen_x: X position on screen where page should be rendered

35 screen_y: Y position on screen where page should be rendered

36 dpi: Working DPI for converting mm to pixels

37 zoom: Current zoom level

38 """

39 self.page_width_mm = page_width_mm

40 self.page_height_mm = page_height_mm

41 self.screen_x = screen_x

42 self.screen_y = screen_y

43 self.dpi = dpi

44 self.zoom = zoom

45

46 # Calculate page dimensions in pixels

47 self.page_width_px = page_width_mm * dpi / 25.4

48 self.page_height_px = page_height_mm * dpi / 25.4

49

50 # Calculate screen dimensions (with zoom applied)

51 self.screen_width = self.page_width_px * zoom

52 self.screen_height = self.page_height_px * zoom

53

54 def page_to_screen(self, page_x: float, page_y: float) -> Tuple[float, float]:

55 """

56 Convert page-local coordinates (in pixels) to screen coordinates.

57

58 Args:

59 page_x: X coordinate in page-local space (pixels)

60 page_y: Y coordinate in page-local space (pixels)

61

62 Returns:

63 Tuple of (screen_x, screen_y)

64 """

65 screen_x = self.screen_x + page_x * self.zoom

66 screen_y = self.screen_y + page_y * self.zoom

67 return (screen_x, screen_y)

68

69 def screen_to_page(self, screen_x: float, screen_y: float) -> Tuple[float, float]:

70 """

71 Convert screen coordinates to page-local coordinates (in pixels).

72

73 Args:

74 screen_x: X coordinate in screen space

75 screen_y: Y coordinate in screen space

76

77 Returns:

78 Tuple of (page_x, page_y) in pixels, or None if outside page bounds

79 """

80 page_x = (screen_x - self.screen_x) / self.zoom

81 page_y = (screen_y - self.screen_y) / self.zoom

82 return (page_x, page_y)

83

84 def is_point_in_page(self, screen_x: float, screen_y: float) -> bool:

85 """

86 Check if a screen coordinate is within the page bounds.

87

88 Args:

89 screen_x: X coordinate in screen space

90 screen_y: Y coordinate in screen space

91

92 Returns:

93 True if the point is within the page bounds

94 """

95 return (

96 self.screen_x <= screen_x <= self.screen_x + self.screen_width

97 and self.screen_y <= screen_y <= self.screen_y + self.screen_height

98 )

99

100 def get_sub_page_at(self, screen_x: float, is_facing_page: bool) -> Optional[str]:

101 """

102 For facing page spreads, determine if mouse is on left or right page.

103

104 Args:

105 screen_x: X coordinate in screen space

106 is_facing_page: Whether this is a facing page spread

107

108 Returns:

109 'left' or 'right' for facing pages, None for single pages

110 """

111 if not is_facing_page:

112 return None

113

114 # Calculate the center line of the spread

115 center_x = self.screen_x + self.screen_width / 2

116

117 if screen_x < center_x:

118 return "left"

119 else:

120 return "right"

121

122 def begin_render(self):

123 """

124 Set up OpenGL transformations for rendering this page.

125 Call this before rendering page content.

126 """

127 glPushMatrix()

128 # Apply zoom

129 glScalef(self.zoom, self.zoom, 1.0)

130 # Translate to page position (in zoomed coordinates)

131 glTranslatef(self.screen_x / self.zoom, self.screen_y / self.zoom, 0.0)

132

133 def end_render(self):

134 """

135 Clean up OpenGL transformations after rendering this page.

136 Call this after rendering page content.

137 """

138 glPopMatrix()

139

140 def get_page_bounds_screen(self) -> Tuple[float, float, float, float]:

141 """

142 Get the page bounds in screen coordinates.

143

144 Returns:

145 Tuple of (x, y, width, height) in screen space

146 """

147 return (self.screen_x, self.screen_y, self.screen_width, self.screen_height)

148

149 def get_page_bounds_page(self) -> Tuple[float, float, float, float]:

150 """

151 Get the page bounds in page-local coordinates.

152

153 Returns:

154 Tuple of (x, y, width, height) in page-local space (pixels)

155 """

156 return (0, 0, self.page_width_px, self.page_height_px)