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
« 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"""
5from functools import wraps
6from typing import Optional, Tuple, Any
9def significant_change(threshold: float = 0.1):
10 """
11 Decorator that validates if a change is significant enough to warrant a command.
13 Args:
14 threshold: Minimum change magnitude to be considered significant
16 Returns:
17 None if change is insignificant, otherwise returns the command builder result
18 """
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
28 return wrapper
30 return decorator
33class ChangeValidator:
34 """Validates whether changes are significant enough to create commands."""
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
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
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
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
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
66 return abs(new_rotation - old_rotation) > threshold
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
78 if old_crop == new_crop:
79 return False
81 return any(abs(new_crop[i] - old_crop[i]) > threshold for i in range(4))
84class InteractionChangeDetector:
85 """Detects and quantifies changes in element properties."""
87 def __init__(self, threshold: float = 0.1):
88 self.threshold = threshold
89 self.validator = ChangeValidator()
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.
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
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 }
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.
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
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 }
125 def detect_rotation_change(self, old_rotation: float, new_rotation: float) -> Optional[dict]:
126 """
127 Detect rotation change and return change info.
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
135 return {"old_rotation": old_rotation, "new_rotation": new_rotation, "delta_angle": new_rotation - old_rotation}
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.
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
149 return {"old_crop": old_crop, "new_crop": new_crop, "delta": tuple(new_crop[i] - old_crop[i] for i in range(4))}