854 lines
29 KiB
Python
854 lines
29 KiB
Python
"""
|
|
Alignment and distribution manager for pyPhotoAlbum
|
|
"""
|
|
|
|
from typing import List, Tuple
|
|
from pyPhotoAlbum.models import BaseLayoutElement
|
|
|
|
|
|
class ElementMaximizer:
|
|
"""
|
|
Handles element maximization using a crystal growth algorithm.
|
|
Breaks down the complex maximize_pattern logic into atomic, testable methods.
|
|
"""
|
|
|
|
def __init__(self, elements: List[BaseLayoutElement], page_size: Tuple[float, float], min_gap: float):
|
|
"""
|
|
Initialize the maximizer with elements and constraints.
|
|
|
|
Args:
|
|
elements: List of elements to maximize
|
|
page_size: (width, height) of the page in mm
|
|
min_gap: Minimum gap to maintain between elements and borders (in mm)
|
|
"""
|
|
self.elements = elements
|
|
self.page_width, self.page_height = page_size
|
|
self.min_gap = min_gap
|
|
self.changes: List[Tuple[BaseLayoutElement, Tuple[float, float], Tuple[float, float]]] = []
|
|
self._record_initial_states()
|
|
|
|
def _record_initial_states(self) -> None:
|
|
"""Record initial positions and sizes for undo functionality."""
|
|
for elem in self.elements:
|
|
self.changes.append((elem, elem.position, elem.size))
|
|
|
|
def check_collision(self, elem_idx: int, new_size: Tuple[float, float]) -> bool:
|
|
"""
|
|
Check if element with new_size would collide with boundaries or other elements.
|
|
|
|
Args:
|
|
elem_idx: Index of the element to check
|
|
new_size: Proposed new size (width, height)
|
|
|
|
Returns:
|
|
True if collision detected, False otherwise
|
|
"""
|
|
elem = self.elements[elem_idx]
|
|
x, y = elem.position
|
|
w, h = new_size
|
|
|
|
# Check page boundaries
|
|
if x < self.min_gap or y < self.min_gap:
|
|
return True
|
|
if x + w > self.page_width - self.min_gap:
|
|
return True
|
|
if y + h > self.page_height - self.min_gap:
|
|
return True
|
|
|
|
# Check collision with other elements
|
|
for i, other in enumerate(self.elements):
|
|
if i == elem_idx:
|
|
continue
|
|
|
|
other_x, other_y = other.position
|
|
other_w, other_h = other.size
|
|
|
|
# Calculate distances between rectangles
|
|
horizontal_gap = max(
|
|
other_x - (x + w), # Other is to the right
|
|
x - (other_x + other_w) # Other is to the left
|
|
)
|
|
|
|
vertical_gap = max(
|
|
other_y - (y + h), # Other is below
|
|
y - (other_y + other_h) # Other is above
|
|
)
|
|
|
|
# If rectangles overlap or are too close in both dimensions
|
|
if horizontal_gap < self.min_gap and vertical_gap < self.min_gap:
|
|
return True
|
|
|
|
return False
|
|
|
|
def find_max_scale(self, elem_idx: int, current_scale: float, max_search_scale: float = 3.0,
|
|
tolerance: float = 0.001, max_iterations: int = 20) -> float:
|
|
"""
|
|
Use binary search to find the maximum scale factor for an element.
|
|
|
|
Args:
|
|
elem_idx: Index of the element
|
|
current_scale: Current scale factor
|
|
max_search_scale: Maximum scale to search up to (relative to current_scale)
|
|
tolerance: Convergence tolerance for binary search
|
|
max_iterations: Maximum binary search iterations
|
|
|
|
Returns:
|
|
Maximum scale factor that doesn't cause collision
|
|
"""
|
|
old_size = self.changes[elem_idx][2]
|
|
|
|
# Binary search for maximum scale
|
|
low, high = current_scale, current_scale * max_search_scale
|
|
best_scale = current_scale
|
|
|
|
for _ in range(max_iterations):
|
|
mid = (low + high) / 2.0
|
|
test_size = (old_size[0] * mid, old_size[1] * mid)
|
|
|
|
if self.check_collision(elem_idx, test_size):
|
|
high = mid
|
|
else:
|
|
best_scale = mid
|
|
low = mid
|
|
|
|
if high - low < tolerance:
|
|
break
|
|
|
|
return best_scale
|
|
|
|
def grow_iteration(self, scales: List[float], growth_rate: float) -> bool:
|
|
"""
|
|
Perform one iteration of the growth algorithm.
|
|
|
|
Args:
|
|
scales: Current scale factors for each element
|
|
growth_rate: Percentage to grow each iteration (0.05 = 5%)
|
|
|
|
Returns:
|
|
True if any element grew, False otherwise
|
|
"""
|
|
any_growth = False
|
|
|
|
for i, elem in enumerate(self.elements):
|
|
old_size = self.changes[i][2]
|
|
|
|
# Try to grow this element
|
|
new_scale = scales[i] * (1.0 + growth_rate)
|
|
new_size = (old_size[0] * new_scale, old_size[1] * new_scale)
|
|
|
|
if not self.check_collision(i, new_size):
|
|
scales[i] = new_scale
|
|
elem.size = new_size
|
|
any_growth = True
|
|
else:
|
|
# Can't grow uniformly, try to find maximum possible scale
|
|
max_scale = self.find_max_scale(i, scales[i])
|
|
if max_scale > scales[i]:
|
|
scales[i] = max_scale
|
|
elem.size = (old_size[0] * max_scale, old_size[1] * max_scale)
|
|
any_growth = True
|
|
|
|
return any_growth
|
|
|
|
def check_element_collision(self, elem: BaseLayoutElement, new_pos: Tuple[float, float]) -> bool:
|
|
"""
|
|
Check if moving an element to new_pos would cause collision with other elements.
|
|
|
|
Args:
|
|
elem: The element to check
|
|
new_pos: Proposed new position (x, y)
|
|
|
|
Returns:
|
|
True if collision detected, False otherwise
|
|
"""
|
|
x, y = new_pos
|
|
w, h = elem.size
|
|
|
|
for other in self.elements:
|
|
if other is elem:
|
|
continue
|
|
ox, oy = other.position
|
|
ow, oh = other.size
|
|
|
|
# Check if rectangles overlap (with min_gap consideration)
|
|
if (abs((x + w/2) - (ox + ow/2)) < (w + ow)/2 + self.min_gap and
|
|
abs((y + h/2) - (oy + oh/2)) < (h + oh)/2 + self.min_gap):
|
|
return True
|
|
|
|
return False
|
|
|
|
def center_element_horizontally(self, elem: BaseLayoutElement) -> None:
|
|
"""
|
|
Micro-adjust element position to center horizontally in available space.
|
|
|
|
Args:
|
|
elem: Element to center
|
|
"""
|
|
x, y = elem.position
|
|
w, h = elem.size
|
|
|
|
# Calculate available space on each side
|
|
space_left = x - self.min_gap
|
|
space_right = (self.page_width - self.min_gap) - (x + w)
|
|
|
|
if space_left >= 0 and space_right >= 0:
|
|
adjust_x = (space_right - space_left) / 4.0 # Gentle centering
|
|
new_x = max(self.min_gap, min(self.page_width - w - self.min_gap, x + adjust_x))
|
|
|
|
# Verify this doesn't cause collision
|
|
old_pos = elem.position
|
|
new_pos = (new_x, y)
|
|
|
|
if not self.check_element_collision(elem, new_pos):
|
|
elem.position = new_pos
|
|
|
|
def center_element_vertically(self, elem: BaseLayoutElement) -> None:
|
|
"""
|
|
Micro-adjust element position to center vertically in available space.
|
|
|
|
Args:
|
|
elem: Element to center
|
|
"""
|
|
x, y = elem.position
|
|
w, h = elem.size
|
|
|
|
# Calculate available space on each side
|
|
space_top = y - self.min_gap
|
|
space_bottom = (self.page_height - self.min_gap) - (y + h)
|
|
|
|
if space_top >= 0 and space_bottom >= 0:
|
|
adjust_y = (space_bottom - space_top) / 4.0
|
|
new_y = max(self.min_gap, min(self.page_height - h - self.min_gap, y + adjust_y))
|
|
|
|
# Verify this doesn't cause collision
|
|
old_pos = elem.position
|
|
new_pos = (x, new_y)
|
|
|
|
if not self.check_element_collision(elem, new_pos):
|
|
elem.position = new_pos
|
|
|
|
def center_elements(self) -> None:
|
|
"""Center all elements slightly within their constrained space."""
|
|
for elem in self.elements:
|
|
self.center_element_horizontally(elem)
|
|
self.center_element_vertically(elem)
|
|
|
|
def maximize(self, max_iterations: int = 100, growth_rate: float = 0.05) -> List[Tuple[BaseLayoutElement, Tuple[float, float], Tuple[float, float]]]:
|
|
"""
|
|
Execute the maximization algorithm.
|
|
|
|
Args:
|
|
max_iterations: Maximum number of growth iterations
|
|
growth_rate: Percentage to grow each iteration (0.05 = 5%)
|
|
|
|
Returns:
|
|
List of (element, old_position, old_size) tuples for undo
|
|
"""
|
|
scales = [1.0] * len(self.elements)
|
|
|
|
# Growth algorithm - iterative expansion
|
|
for _ in range(max_iterations):
|
|
if not self.grow_iteration(scales, growth_rate):
|
|
break
|
|
|
|
# Center elements slightly within their constrained space
|
|
self.center_elements()
|
|
|
|
return self.changes
|
|
|
|
|
|
class AlignmentManager:
|
|
"""Manages alignment and distribution operations on multiple elements"""
|
|
|
|
@staticmethod
|
|
def get_bounds(elements: List[BaseLayoutElement]) -> Tuple[float, float, float, float]:
|
|
"""
|
|
Get the bounding box of multiple elements.
|
|
|
|
Returns:
|
|
(min_x, min_y, max_x, max_y)
|
|
"""
|
|
if not elements:
|
|
return (0, 0, 0, 0)
|
|
|
|
min_x = min(elem.position[0] for elem in elements)
|
|
min_y = min(elem.position[1] for elem in elements)
|
|
max_x = max(elem.position[0] + elem.size[0] for elem in elements)
|
|
max_y = max(elem.position[1] + elem.size[1] for elem in elements)
|
|
|
|
return (min_x, min_y, max_x, max_y)
|
|
|
|
@staticmethod
|
|
def align_left(elements: List[BaseLayoutElement]) -> List[Tuple[BaseLayoutElement, Tuple[float, float]]]:
|
|
"""
|
|
Align all elements to the leftmost element.
|
|
|
|
Returns:
|
|
List of (element, old_position) tuples for undo
|
|
"""
|
|
if len(elements) < 2:
|
|
return []
|
|
|
|
min_x = min(elem.position[0] for elem in elements)
|
|
changes = []
|
|
|
|
for elem in elements:
|
|
old_pos = elem.position
|
|
elem.position = (min_x, elem.position[1])
|
|
changes.append((elem, old_pos))
|
|
|
|
return changes
|
|
|
|
@staticmethod
|
|
def align_right(elements: List[BaseLayoutElement]) -> List[Tuple[BaseLayoutElement, Tuple[float, float]]]:
|
|
"""
|
|
Align all elements to the rightmost element.
|
|
|
|
Returns:
|
|
List of (element, old_position) tuples for undo
|
|
"""
|
|
if len(elements) < 2:
|
|
return []
|
|
|
|
max_right = max(elem.position[0] + elem.size[0] for elem in elements)
|
|
changes = []
|
|
|
|
for elem in elements:
|
|
old_pos = elem.position
|
|
new_x = max_right - elem.size[0]
|
|
elem.position = (new_x, elem.position[1])
|
|
changes.append((elem, old_pos))
|
|
|
|
return changes
|
|
|
|
@staticmethod
|
|
def align_top(elements: List[BaseLayoutElement]) -> List[Tuple[BaseLayoutElement, Tuple[float, float]]]:
|
|
"""
|
|
Align all elements to the topmost element.
|
|
|
|
Returns:
|
|
List of (element, old_position) tuples for undo
|
|
"""
|
|
if len(elements) < 2:
|
|
return []
|
|
|
|
min_y = min(elem.position[1] for elem in elements)
|
|
changes = []
|
|
|
|
for elem in elements:
|
|
old_pos = elem.position
|
|
elem.position = (elem.position[0], min_y)
|
|
changes.append((elem, old_pos))
|
|
|
|
return changes
|
|
|
|
@staticmethod
|
|
def align_bottom(elements: List[BaseLayoutElement]) -> List[Tuple[BaseLayoutElement, Tuple[float, float]]]:
|
|
"""
|
|
Align all elements to the bottommost element.
|
|
|
|
Returns:
|
|
List of (element, old_position) tuples for undo
|
|
"""
|
|
if len(elements) < 2:
|
|
return []
|
|
|
|
max_bottom = max(elem.position[1] + elem.size[1] for elem in elements)
|
|
changes = []
|
|
|
|
for elem in elements:
|
|
old_pos = elem.position
|
|
new_y = max_bottom - elem.size[1]
|
|
elem.position = (elem.position[0], new_y)
|
|
changes.append((elem, old_pos))
|
|
|
|
return changes
|
|
|
|
@staticmethod
|
|
def align_horizontal_center(elements: List[BaseLayoutElement]) -> List[Tuple[BaseLayoutElement, Tuple[float, float]]]:
|
|
"""
|
|
Align all elements to horizontal center.
|
|
|
|
Returns:
|
|
List of (element, old_position) tuples for undo
|
|
"""
|
|
if len(elements) < 2:
|
|
return []
|
|
|
|
# Calculate average center
|
|
centers = [elem.position[0] + elem.size[0] / 2 for elem in elements]
|
|
avg_center = sum(centers) / len(centers)
|
|
|
|
changes = []
|
|
for elem in elements:
|
|
old_pos = elem.position
|
|
new_x = avg_center - elem.size[0] / 2
|
|
elem.position = (new_x, elem.position[1])
|
|
changes.append((elem, old_pos))
|
|
|
|
return changes
|
|
|
|
@staticmethod
|
|
def align_vertical_center(elements: List[BaseLayoutElement]) -> List[Tuple[BaseLayoutElement, Tuple[float, float]]]:
|
|
"""
|
|
Align all elements to vertical center.
|
|
|
|
Returns:
|
|
List of (element, old_position) tuples for undo
|
|
"""
|
|
if len(elements) < 2:
|
|
return []
|
|
|
|
# Calculate average center
|
|
centers = [elem.position[1] + elem.size[1] / 2 for elem in elements]
|
|
avg_center = sum(centers) / len(centers)
|
|
|
|
changes = []
|
|
for elem in elements:
|
|
old_pos = elem.position
|
|
new_y = avg_center - elem.size[1] / 2
|
|
elem.position = (elem.position[0], new_y)
|
|
changes.append((elem, old_pos))
|
|
|
|
return changes
|
|
|
|
@staticmethod
|
|
def make_same_size(elements: List[BaseLayoutElement]) -> List[Tuple[BaseLayoutElement, Tuple[float, float], Tuple[float, float]]]:
|
|
"""
|
|
Make all elements the same size as the first element.
|
|
|
|
Returns:
|
|
List of (element, old_position, old_size) tuples for undo
|
|
"""
|
|
if len(elements) < 2:
|
|
return []
|
|
|
|
target_size = elements[0].size
|
|
changes = []
|
|
|
|
for elem in elements[1:]:
|
|
old_pos = elem.position
|
|
old_size = elem.size
|
|
elem.size = target_size
|
|
changes.append((elem, old_pos, old_size))
|
|
|
|
return changes
|
|
|
|
@staticmethod
|
|
def make_same_width(elements: List[BaseLayoutElement]) -> List[Tuple[BaseLayoutElement, Tuple[float, float], Tuple[float, float]]]:
|
|
"""
|
|
Make all elements the same width as the first element.
|
|
|
|
Returns:
|
|
List of (element, old_position, old_size) tuples for undo
|
|
"""
|
|
if len(elements) < 2:
|
|
return []
|
|
|
|
target_width = elements[0].size[0]
|
|
changes = []
|
|
|
|
for elem in elements[1:]:
|
|
old_pos = elem.position
|
|
old_size = elem.size
|
|
elem.size = (target_width, elem.size[1])
|
|
changes.append((elem, old_pos, old_size))
|
|
|
|
return changes
|
|
|
|
@staticmethod
|
|
def make_same_height(elements: List[BaseLayoutElement]) -> List[Tuple[BaseLayoutElement, Tuple[float, float], Tuple[float, float]]]:
|
|
"""
|
|
Make all elements the same height as the first element.
|
|
|
|
Returns:
|
|
List of (element, old_position, old_size) tuples for undo
|
|
"""
|
|
if len(elements) < 2:
|
|
return []
|
|
|
|
target_height = elements[0].size[1]
|
|
changes = []
|
|
|
|
for elem in elements[1:]:
|
|
old_pos = elem.position
|
|
old_size = elem.size
|
|
elem.size = (elem.size[0], target_height)
|
|
changes.append((elem, old_pos, old_size))
|
|
|
|
return changes
|
|
|
|
@staticmethod
|
|
def distribute_horizontally(elements: List[BaseLayoutElement]) -> List[Tuple[BaseLayoutElement, Tuple[float, float]]]:
|
|
"""
|
|
Distribute elements evenly across horizontal span.
|
|
|
|
Returns:
|
|
List of (element, old_position) tuples for undo
|
|
"""
|
|
if len(elements) < 3:
|
|
return []
|
|
|
|
# Sort by x position
|
|
sorted_elements = sorted(elements, key=lambda e: e.position[0])
|
|
|
|
# Get leftmost and rightmost positions
|
|
min_x = sorted_elements[0].position[0]
|
|
max_x = sorted_elements[-1].position[0]
|
|
|
|
# Calculate spacing between centers
|
|
total_span = max_x - min_x
|
|
spacing = total_span / (len(sorted_elements) - 1)
|
|
|
|
changes = []
|
|
for i, elem in enumerate(sorted_elements):
|
|
old_pos = elem.position
|
|
new_x = min_x + (i * spacing)
|
|
elem.position = (new_x, elem.position[1])
|
|
changes.append((elem, old_pos))
|
|
|
|
return changes
|
|
|
|
@staticmethod
|
|
def distribute_vertically(elements: List[BaseLayoutElement]) -> List[Tuple[BaseLayoutElement, Tuple[float, float]]]:
|
|
"""
|
|
Distribute elements evenly across vertical span.
|
|
|
|
Returns:
|
|
List of (element, old_position) tuples for undo
|
|
"""
|
|
if len(elements) < 3:
|
|
return []
|
|
|
|
# Sort by y position
|
|
sorted_elements = sorted(elements, key=lambda e: e.position[1])
|
|
|
|
# Get topmost and bottommost positions
|
|
min_y = sorted_elements[0].position[1]
|
|
max_y = sorted_elements[-1].position[1]
|
|
|
|
# Calculate spacing between centers
|
|
total_span = max_y - min_y
|
|
spacing = total_span / (len(sorted_elements) - 1)
|
|
|
|
changes = []
|
|
for i, elem in enumerate(sorted_elements):
|
|
old_pos = elem.position
|
|
new_y = min_y + (i * spacing)
|
|
elem.position = (elem.position[0], new_y)
|
|
changes.append((elem, old_pos))
|
|
|
|
return changes
|
|
|
|
@staticmethod
|
|
def space_horizontally(elements: List[BaseLayoutElement]) -> List[Tuple[BaseLayoutElement, Tuple[float, float]]]:
|
|
"""
|
|
Distribute elements with equal spacing between them horizontally.
|
|
|
|
Returns:
|
|
List of (element, old_position) tuples for undo
|
|
"""
|
|
if len(elements) < 3:
|
|
return []
|
|
|
|
# Sort by x position
|
|
sorted_elements = sorted(elements, key=lambda e: e.position[0])
|
|
|
|
# Get leftmost and rightmost boundaries
|
|
min_x = sorted_elements[0].position[0]
|
|
max_right = sorted_elements[-1].position[0] + sorted_elements[-1].size[0]
|
|
|
|
# Calculate total width of all elements
|
|
total_width = sum(elem.size[0] for elem in sorted_elements)
|
|
|
|
# Calculate available space and spacing
|
|
available_space = max_right - min_x - total_width
|
|
spacing = available_space / (len(sorted_elements) - 1)
|
|
|
|
changes = []
|
|
current_x = min_x
|
|
|
|
for elem in sorted_elements:
|
|
old_pos = elem.position
|
|
elem.position = (current_x, elem.position[1])
|
|
changes.append((elem, old_pos))
|
|
current_x += elem.size[0] + spacing
|
|
|
|
return changes
|
|
|
|
@staticmethod
|
|
def space_vertically(elements: List[BaseLayoutElement]) -> List[Tuple[BaseLayoutElement, Tuple[float, float]]]:
|
|
"""
|
|
Distribute elements with equal spacing between them vertically.
|
|
|
|
Returns:
|
|
List of (element, old_position) tuples for undo
|
|
"""
|
|
if len(elements) < 3:
|
|
return []
|
|
|
|
# Sort by y position
|
|
sorted_elements = sorted(elements, key=lambda e: e.position[1])
|
|
|
|
# Get topmost and bottommost boundaries
|
|
min_y = sorted_elements[0].position[1]
|
|
max_bottom = sorted_elements[-1].position[1] + sorted_elements[-1].size[1]
|
|
|
|
# Calculate total height of all elements
|
|
total_height = sum(elem.size[1] for elem in sorted_elements)
|
|
|
|
# Calculate available space and spacing
|
|
available_space = max_bottom - min_y - total_height
|
|
spacing = available_space / (len(sorted_elements) - 1)
|
|
|
|
changes = []
|
|
current_y = min_y
|
|
|
|
for elem in sorted_elements:
|
|
old_pos = elem.position
|
|
elem.position = (elem.position[0], current_y)
|
|
changes.append((elem, old_pos))
|
|
current_y += elem.size[1] + spacing
|
|
|
|
return changes
|
|
|
|
@staticmethod
|
|
def fit_to_page_width(element: BaseLayoutElement, page_width: float) -> Tuple[BaseLayoutElement, Tuple[float, float], Tuple[float, float]]:
|
|
"""
|
|
Resize element to fit page width while maintaining aspect ratio.
|
|
|
|
Args:
|
|
element: The element to resize
|
|
page_width: The page width in mm
|
|
|
|
Returns:
|
|
Tuple of (element, old_position, old_size) for undo
|
|
"""
|
|
old_pos = element.position
|
|
old_size = element.size
|
|
|
|
# Calculate aspect ratio
|
|
aspect_ratio = old_size[1] / old_size[0]
|
|
|
|
# Set new size
|
|
new_width = page_width
|
|
new_height = page_width * aspect_ratio
|
|
element.size = (new_width, new_height)
|
|
|
|
return (element, old_pos, old_size)
|
|
|
|
@staticmethod
|
|
def fit_to_page_height(element: BaseLayoutElement, page_height: float) -> Tuple[BaseLayoutElement, Tuple[float, float], Tuple[float, float]]:
|
|
"""
|
|
Resize element to fit page height while maintaining aspect ratio.
|
|
|
|
Args:
|
|
element: The element to resize
|
|
page_height: The page height in mm
|
|
|
|
Returns:
|
|
Tuple of (element, old_position, old_size) for undo
|
|
"""
|
|
old_pos = element.position
|
|
old_size = element.size
|
|
|
|
# Calculate aspect ratio
|
|
aspect_ratio = old_size[0] / old_size[1]
|
|
|
|
# Set new size
|
|
new_height = page_height
|
|
new_width = page_height * aspect_ratio
|
|
element.size = (new_width, new_height)
|
|
|
|
return (element, old_pos, old_size)
|
|
|
|
@staticmethod
|
|
def fit_to_page(element: BaseLayoutElement, page_width: float, page_height: float) -> Tuple[BaseLayoutElement, Tuple[float, float], Tuple[float, float]]:
|
|
"""
|
|
Resize element to fit within page dimensions while maintaining aspect ratio.
|
|
|
|
Args:
|
|
element: The element to resize
|
|
page_width: The page width in mm
|
|
page_height: The page height in mm
|
|
|
|
Returns:
|
|
Tuple of (element, old_position, old_size) for undo
|
|
"""
|
|
old_pos = element.position
|
|
old_size = element.size
|
|
|
|
# Calculate aspect ratios
|
|
element_aspect = old_size[0] / old_size[1]
|
|
page_aspect = page_width / page_height
|
|
|
|
# Determine which dimension to fit to
|
|
if element_aspect > page_aspect:
|
|
# Element is wider than page - fit to width
|
|
new_width = page_width
|
|
new_height = page_width / element_aspect
|
|
else:
|
|
# Element is taller than page - fit to height
|
|
new_height = page_height
|
|
new_width = page_height * element_aspect
|
|
|
|
element.size = (new_width, new_height)
|
|
|
|
return (element, old_pos, old_size)
|
|
|
|
@staticmethod
|
|
def maximize_pattern(
|
|
elements: List[BaseLayoutElement],
|
|
page_size: Tuple[float, float],
|
|
min_gap: float = 2.0,
|
|
max_iterations: int = 100,
|
|
growth_rate: float = 0.05
|
|
) -> List[Tuple[BaseLayoutElement, Tuple[float, float], Tuple[float, float]]]:
|
|
"""
|
|
Maximize element sizes using a crystal growth algorithm.
|
|
Elements grow until they are close to borders or each other.
|
|
|
|
Args:
|
|
elements: List of elements to maximize
|
|
page_size: (width, height) of the page in mm
|
|
min_gap: Minimum gap to maintain between elements and borders (in mm)
|
|
max_iterations: Maximum number of growth iterations
|
|
growth_rate: Percentage to grow each iteration (0.05 = 5%)
|
|
|
|
Returns:
|
|
List of (element, old_position, old_size) tuples for undo
|
|
"""
|
|
if not elements:
|
|
return []
|
|
|
|
maximizer = ElementMaximizer(elements, page_size, min_gap)
|
|
return maximizer.maximize(max_iterations, growth_rate)
|
|
|
|
@staticmethod
|
|
def expand_to_bounds(
|
|
element: BaseLayoutElement,
|
|
page_size: Tuple[float, float],
|
|
other_elements: List[BaseLayoutElement],
|
|
min_gap: float = 10.0
|
|
) -> Tuple[BaseLayoutElement, Tuple[float, float], Tuple[float, float]]:
|
|
"""
|
|
Expand a single element until it is min_gap away from page edges or other elements.
|
|
|
|
This function expands an element from its current position and size, growing it
|
|
in all directions (up, down, left, right) until it reaches:
|
|
- The page boundaries (with min_gap margin)
|
|
- Another element on the same page (with min_gap spacing)
|
|
|
|
The element expands independently in width and height to fill all available space.
|
|
|
|
Args:
|
|
element: The element to expand
|
|
page_size: (width, height) of the page in mm
|
|
other_elements: List of other elements on the same page (excluding the target element)
|
|
min_gap: Minimum gap to maintain between element and boundaries/other elements (in mm)
|
|
|
|
Returns:
|
|
Tuple of (element, old_position, old_size) for undo
|
|
"""
|
|
page_width, page_height = page_size
|
|
old_pos = element.position
|
|
old_size = element.size
|
|
|
|
x, y = element.position
|
|
w, h = element.size
|
|
|
|
# Calculate maximum expansion in each direction
|
|
# Start with page boundaries
|
|
max_left = x - min_gap # How much we can expand left
|
|
max_right = (page_width - min_gap) - (x + w) # How much we can expand right
|
|
max_top = y - min_gap # How much we can expand up
|
|
max_bottom = (page_height - min_gap) - (y + h) # How much we can expand down
|
|
|
|
# Check constraints from other elements
|
|
# We need to be conservative and check ALL elements against ALL expansion directions
|
|
for other in other_elements:
|
|
ox, oy = other.position
|
|
ow, oh = other.size
|
|
|
|
# Calculate the other element's bounds
|
|
other_left = ox
|
|
other_right = ox + ow
|
|
other_top = oy
|
|
other_bottom = oy + oh
|
|
|
|
# Calculate current element's bounds
|
|
elem_left = x
|
|
elem_right = x + w
|
|
elem_top = y
|
|
elem_bottom = y + h
|
|
|
|
# Check leftward expansion
|
|
# An element blocks leftward expansion if:
|
|
# 1. It's to the left of our left edge (other_right <= elem_left)
|
|
# 2. Its vertical range would overlap with ANY part of our vertical extent
|
|
if other_right <= elem_left:
|
|
# Check if vertical ranges overlap (current OR after any vertical expansion)
|
|
# Conservative: assume we might expand vertically to page bounds
|
|
if not (other_bottom <= elem_top - min_gap or other_top >= elem_bottom + min_gap):
|
|
# This element blocks leftward expansion
|
|
available_left = elem_left - other_right - min_gap
|
|
max_left = min(max_left, available_left)
|
|
|
|
# Check rightward expansion
|
|
if other_left >= elem_right:
|
|
# Check if vertical ranges overlap
|
|
if not (other_bottom <= elem_top - min_gap or other_top >= elem_bottom + min_gap):
|
|
# This element blocks rightward expansion
|
|
available_right = other_left - elem_right - min_gap
|
|
max_right = min(max_right, available_right)
|
|
|
|
# Check upward expansion
|
|
if other_bottom <= elem_top:
|
|
# Check if horizontal ranges overlap
|
|
if not (other_right <= elem_left - min_gap or other_left >= elem_right + min_gap):
|
|
# This element blocks upward expansion
|
|
available_top = elem_top - other_bottom - min_gap
|
|
max_top = min(max_top, available_top)
|
|
|
|
# Check downward expansion
|
|
if other_top >= elem_bottom:
|
|
# Check if horizontal ranges overlap
|
|
if not (other_right <= elem_left - min_gap or other_left >= elem_right + min_gap):
|
|
# This element blocks downward expansion
|
|
available_bottom = other_top - elem_bottom - min_gap
|
|
max_bottom = min(max_bottom, available_bottom)
|
|
|
|
# Ensure non-negative expansion
|
|
max_left = max(0, max_left)
|
|
max_right = max(0, max_right)
|
|
max_top = max(0, max_top)
|
|
max_bottom = max(0, max_bottom)
|
|
|
|
# Expand to fill all available space (no aspect ratio constraint)
|
|
width_increase = max_left + max_right
|
|
height_increase = max_top + max_bottom
|
|
|
|
# Calculate new size
|
|
new_width = w + width_increase
|
|
new_height = h + height_increase
|
|
|
|
# Calculate new position (expand from center to maintain relative position)
|
|
# Distribute the expansion proportionally to available space on each side
|
|
if max_left + max_right > 0:
|
|
left_ratio = max_left / (max_left + max_right)
|
|
new_x = x - (width_increase * left_ratio)
|
|
else:
|
|
new_x = x
|
|
|
|
if max_top + max_bottom > 0:
|
|
top_ratio = max_top / (max_top + max_bottom)
|
|
new_y = y - (height_increase * top_ratio)
|
|
else:
|
|
new_y = y
|
|
|
|
# Apply the new position and size
|
|
element.position = (new_x, new_y)
|
|
element.size = (new_width, new_height)
|
|
|
|
return (element, old_pos, old_size)
|