pyPhotoAlbum/pyPhotoAlbum/mixins/interaction_command_builders.py
Duncan Tourolle f6ed11b0bc
All checks were successful
Python CI / test (push) Successful in 1m20s
Lint / lint (push) Successful in 1m4s
Tests / test (3.11) (push) Successful in 1m27s
Tests / test (3.12) (push) Successful in 2m25s
Tests / test (3.13) (push) Successful in 2m52s
Tests / test (3.14) (push) Successful in 1m9s
black formatting
2025-11-27 23:07:16 +01:00

204 lines
6.7 KiB
Python

"""
Command builders for different interaction types.
Each builder is responsible for:
1. Validating if a command should be created
2. Creating the appropriate command object
3. Logging the operation
"""
from abc import ABC, abstractmethod
from typing import Optional, Any
from pyPhotoAlbum.models import BaseLayoutElement
from .interaction_validators import InteractionChangeDetector
class CommandBuilder(ABC):
"""Base class for command builders."""
def __init__(self, change_detector: Optional[InteractionChangeDetector] = None):
self.change_detector = change_detector or InteractionChangeDetector()
@abstractmethod
def can_build(self, element: BaseLayoutElement, start_state: dict, **kwargs) -> bool:
"""
Check if a command should be built based on state changes.
Args:
element: The element being modified
start_state: Dict containing the initial state
**kwargs: Additional context
Returns:
True if a command should be created
"""
pass
@abstractmethod
def build(self, element: BaseLayoutElement, start_state: dict, **kwargs) -> Optional[Any]:
"""
Build and return the command object.
Args:
element: The element being modified
start_state: Dict containing the initial state
**kwargs: Additional context
Returns:
Command object or None
"""
pass
def log_command(self, command_type: str, details: str):
"""Log command creation for debugging."""
print(f"{command_type} command created: {details}")
class MoveCommandBuilder(CommandBuilder):
"""Builds MoveElementCommand objects."""
def can_build(self, element: BaseLayoutElement, start_state: dict, **kwargs) -> bool:
"""Check if position changed significantly."""
old_pos = start_state.get("position")
if old_pos is None:
return False
new_pos = element.position
return self.change_detector.detect_position_change(old_pos, new_pos) is not None
def build(self, element: BaseLayoutElement, start_state: dict, **kwargs) -> Optional[Any]:
"""Build a MoveElementCommand."""
old_pos = start_state.get("position")
if old_pos is None:
return None
new_pos = element.position
change_info = self.change_detector.detect_position_change(old_pos, new_pos)
if change_info is None:
return None
from pyPhotoAlbum.commands import MoveElementCommand
command = MoveElementCommand(element, old_pos, new_pos)
self.log_command("Move", f"{old_pos}{new_pos}")
return command
class ResizeCommandBuilder(CommandBuilder):
"""Builds ResizeElementCommand objects."""
def can_build(self, element: BaseLayoutElement, start_state: dict, **kwargs) -> bool:
"""Check if position or size changed significantly."""
old_pos = start_state.get("position")
old_size = start_state.get("size")
if old_pos is None or old_size is None:
return False
new_pos = element.position
new_size = element.size
pos_change = self.change_detector.detect_position_change(old_pos, new_pos)
size_change = self.change_detector.detect_size_change(old_size, new_size)
return pos_change is not None or size_change is not None
def build(self, element: BaseLayoutElement, start_state: dict, **kwargs) -> Optional[Any]:
"""Build a ResizeElementCommand."""
old_pos = start_state.get("position")
old_size = start_state.get("size")
if old_pos is None or old_size is None:
return None
new_pos = element.position
new_size = element.size
if not self.can_build(element, start_state):
return None
from pyPhotoAlbum.commands import ResizeElementCommand
command = ResizeElementCommand(element, old_pos, old_size, new_pos, new_size)
self.log_command("Resize", f"{old_size}{new_size}")
return command
class RotateCommandBuilder(CommandBuilder):
"""Builds RotateElementCommand objects."""
def can_build(self, element: BaseLayoutElement, start_state: dict, **kwargs) -> bool:
"""Check if rotation changed significantly."""
old_rotation = start_state.get("rotation")
if old_rotation is None:
return False
new_rotation = element.rotation
return self.change_detector.detect_rotation_change(old_rotation, new_rotation) is not None
def build(self, element: BaseLayoutElement, start_state: dict, **kwargs) -> Optional[Any]:
"""Build a RotateElementCommand."""
old_rotation = start_state.get("rotation")
if old_rotation is None:
return None
new_rotation = element.rotation
change_info = self.change_detector.detect_rotation_change(old_rotation, new_rotation)
if change_info is None:
return None
from pyPhotoAlbum.commands import RotateElementCommand
command = RotateElementCommand(element, old_rotation, new_rotation)
self.log_command("Rotation", f"{old_rotation:.1f}° → {new_rotation:.1f}°")
return command
class ImagePanCommandBuilder(CommandBuilder):
"""Builds AdjustImageCropCommand objects for image panning."""
def can_build(self, element: BaseLayoutElement, start_state: dict, **kwargs) -> bool:
"""Check if crop info changed significantly."""
from pyPhotoAlbum.models import ImageData
if not isinstance(element, ImageData):
return False
old_crop = start_state.get("crop_info")
if old_crop is None:
return False
new_crop = element.crop_info
change_detector = InteractionChangeDetector(threshold=0.001)
return change_detector.detect_crop_change(old_crop, new_crop) is not None
def build(self, element: BaseLayoutElement, start_state: dict, **kwargs) -> Optional[Any]:
"""Build an AdjustImageCropCommand."""
from pyPhotoAlbum.models import ImageData
if not isinstance(element, ImageData):
return None
old_crop = start_state.get("crop_info")
if old_crop is None:
return None
new_crop = element.crop_info
change_detector = InteractionChangeDetector(threshold=0.001)
change_info = change_detector.detect_crop_change(old_crop, new_crop)
if change_info is None:
return None
from pyPhotoAlbum.commands import AdjustImageCropCommand
command = AdjustImageCropCommand(element, old_crop, new_crop)
self.log_command("Image pan", f"{old_crop}{new_crop}")
return command