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
204 lines
6.7 KiB
Python
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
|