Coverage for pyPhotoAlbum/mixins/interaction_validators.py: 80%

59 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-06-20 12:55 +0000

1""" 

2Decorators and validators for interaction change detection. 

3""" 

4 

5from functools import wraps 

6from typing import Optional, Tuple, Any 

7 

8 

9def significant_change(threshold: float = 0.1): 

10 """ 

11 Decorator that validates if a change is significant enough to warrant a command. 

12 

13 Args: 

14 threshold: Minimum change magnitude to be considered significant 

15 

16 Returns: 

17 None if change is insignificant, otherwise returns the command builder result 

18 """ 

19 

20 def decorator(func): 

21 @wraps(func) 

22 def wrapper(*args, **kwargs): 

23 result = func(*args, **kwargs) 

24 if result is None: 

25 return None 

26 return result 

27 

28 return wrapper 

29 

30 return decorator 

31 

32 

33class ChangeValidator: 

34 """Validates whether changes are significant enough to create commands.""" 

35 

36 @staticmethod 

37 def position_changed( 

38 old_pos: Optional[Tuple[float, float]], new_pos: Optional[Tuple[float, float]], threshold: float = 0.1 

39 ) -> bool: 

40 """Check if position changed significantly.""" 

41 if old_pos is None or new_pos is None: 

42 return False 

43 

44 dx = abs(new_pos[0] - old_pos[0]) 

45 dy = abs(new_pos[1] - old_pos[1]) 

46 return dx > threshold or dy > threshold 

47 

48 @staticmethod 

49 def size_changed( 

50 old_size: Optional[Tuple[float, float]], new_size: Optional[Tuple[float, float]], threshold: float = 0.1 

51 ) -> bool: 

52 """Check if size changed significantly.""" 

53 if old_size is None or new_size is None: 

54 return False 

55 

56 dw = abs(new_size[0] - old_size[0]) 

57 dh = abs(new_size[1] - old_size[1]) 

58 return dw > threshold or dh > threshold 

59 

60 @staticmethod 

61 def rotation_changed(old_rotation: Optional[float], new_rotation: Optional[float], threshold: float = 0.1) -> bool: 

62 """Check if rotation changed significantly.""" 

63 if old_rotation is None or new_rotation is None: 

64 return False 

65 

66 return abs(new_rotation - old_rotation) > threshold 

67 

68 @staticmethod 

69 def crop_changed( 

70 old_crop: Optional[Tuple[float, float, float, float]], 

71 new_crop: Optional[Tuple[float, float, float, float]], 

72 threshold: float = 0.001, 

73 ) -> bool: 

74 """Check if crop info changed significantly.""" 

75 if old_crop is None or new_crop is None: 

76 return False 

77 

78 if old_crop == new_crop: 

79 return False 

80 

81 return any(abs(new_crop[i] - old_crop[i]) > threshold for i in range(4)) 

82 

83 

84class InteractionChangeDetector: 

85 """Detects and quantifies changes in element properties.""" 

86 

87 def __init__(self, threshold: float = 0.1): 

88 self.threshold = threshold 

89 self.validator = ChangeValidator() 

90 

91 def detect_position_change(self, old_pos: Tuple[float, float], new_pos: Tuple[float, float]) -> Optional[dict]: 

92 """ 

93 Detect position change and return change info. 

94 

95 Returns: 

96 Dict with change info if significant, None otherwise 

97 """ 

98 if not self.validator.position_changed(old_pos, new_pos, self.threshold): 

99 return None 

100 

101 return { 

102 "old_position": old_pos, 

103 "new_position": new_pos, 

104 "delta_x": new_pos[0] - old_pos[0], 

105 "delta_y": new_pos[1] - old_pos[1], 

106 } 

107 

108 def detect_size_change(self, old_size: Tuple[float, float], new_size: Tuple[float, float]) -> Optional[dict]: 

109 """ 

110 Detect size change and return change info. 

111 

112 Returns: 

113 Dict with change info if significant, None otherwise 

114 """ 

115 if not self.validator.size_changed(old_size, new_size, self.threshold): 

116 return None 

117 

118 return { 

119 "old_size": old_size, 

120 "new_size": new_size, 

121 "delta_width": new_size[0] - old_size[0], 

122 "delta_height": new_size[1] - old_size[1], 

123 } 

124 

125 def detect_rotation_change(self, old_rotation: float, new_rotation: float) -> Optional[dict]: 

126 """ 

127 Detect rotation change and return change info. 

128 

129 Returns: 

130 Dict with change info if significant, None otherwise 

131 """ 

132 if not self.validator.rotation_changed(old_rotation, new_rotation, self.threshold): 

133 return None 

134 

135 return {"old_rotation": old_rotation, "new_rotation": new_rotation, "delta_angle": new_rotation - old_rotation} 

136 

137 def detect_crop_change( 

138 self, old_crop: Tuple[float, float, float, float], new_crop: Tuple[float, float, float, float] 

139 ) -> Optional[dict]: 

140 """ 

141 Detect crop change and return change info. 

142 

143 Returns: 

144 Dict with change info if significant, None otherwise 

145 """ 

146 if not self.validator.crop_changed(old_crop, new_crop, threshold=0.001): 

147 return None 

148 

149 return {"old_crop": old_crop, "new_crop": new_crop, "delta": tuple(new_crop[i] - old_crop[i] for i in range(4))}