Coverage for pyPhotoAlbum/alignment.py: 91%
388 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"""
2Alignment and distribution manager for pyPhotoAlbum
3"""
5from typing import List, Tuple
6from pyPhotoAlbum.models import BaseLayoutElement
9class ElementMaximizer:
10 """
11 Handles element maximization using a crystal growth algorithm.
12 Breaks down the complex maximize_pattern logic into atomic, testable methods.
13 """
15 def __init__(self, elements: List[BaseLayoutElement], page_size: Tuple[float, float], min_gap: float):
16 """
17 Initialize the maximizer with elements and constraints.
19 Args:
20 elements: List of elements to maximize
21 page_size: (width, height) of the page in mm
22 min_gap: Minimum gap to maintain between elements and borders (in mm)
23 """
24 self.elements = elements
25 self.page_width, self.page_height = page_size
26 self.min_gap = min_gap
27 self.changes: List[Tuple[BaseLayoutElement, Tuple[float, float], Tuple[float, float]]] = []
28 self._record_initial_states()
30 def _record_initial_states(self) -> None:
31 """Record initial positions and sizes for undo functionality."""
32 for elem in self.elements:
33 self.changes.append((elem, elem.position, elem.size))
35 def check_collision(self, elem_idx: int, new_size: Tuple[float, float]) -> bool:
36 """
37 Check if element with new_size would collide with boundaries or other elements.
39 Args:
40 elem_idx: Index of the element to check
41 new_size: Proposed new size (width, height)
43 Returns:
44 True if collision detected, False otherwise
45 """
46 elem = self.elements[elem_idx]
47 x, y = elem.position
48 w, h = new_size
50 # Check page boundaries
51 if x < self.min_gap or y < self.min_gap:
52 return True
53 if x + w > self.page_width - self.min_gap:
54 return True
55 if y + h > self.page_height - self.min_gap:
56 return True
58 # Check collision with other elements
59 for i, other in enumerate(self.elements):
60 if i == elem_idx:
61 continue
63 other_x, other_y = other.position
64 other_w, other_h = other.size
66 # Calculate distances between rectangles
67 horizontal_gap = max(
68 other_x - (x + w), x - (other_x + other_w) # Other is to the right # Other is to the left
69 )
71 vertical_gap = max(other_y - (y + h), y - (other_y + other_h)) # Other is below # Other is above
73 # If rectangles overlap or are too close in both dimensions
74 if horizontal_gap < self.min_gap and vertical_gap < self.min_gap:
75 return True
77 return False
79 def find_max_scale(
80 self,
81 elem_idx: int,
82 current_scale: float,
83 max_search_scale: float = 3.0,
84 tolerance: float = 0.001,
85 max_iterations: int = 20,
86 ) -> float:
87 """
88 Use binary search to find the maximum scale factor for an element.
90 Args:
91 elem_idx: Index of the element
92 current_scale: Current scale factor
93 max_search_scale: Maximum scale to search up to (relative to current_scale)
94 tolerance: Convergence tolerance for binary search
95 max_iterations: Maximum binary search iterations
97 Returns:
98 Maximum scale factor that doesn't cause collision
99 """
100 old_size = self.changes[elem_idx][2]
102 # Binary search for maximum scale
103 low, high = current_scale, current_scale * max_search_scale
104 best_scale = current_scale
106 for _ in range(max_iterations):
107 mid = (low + high) / 2.0
108 test_size = (old_size[0] * mid, old_size[1] * mid)
110 if self.check_collision(elem_idx, test_size):
111 high = mid
112 else:
113 best_scale = mid
114 low = mid
116 if high - low < tolerance:
117 break
119 return best_scale
121 def grow_iteration(self, scales: List[float], growth_rate: float) -> bool:
122 """
123 Perform one iteration of the growth algorithm.
125 Args:
126 scales: Current scale factors for each element
127 growth_rate: Percentage to grow each iteration (0.05 = 5%)
129 Returns:
130 True if any element grew, False otherwise
131 """
132 any_growth = False
134 for i, elem in enumerate(self.elements):
135 old_size = self.changes[i][2]
137 # Try to grow this element
138 new_scale = scales[i] * (1.0 + growth_rate)
139 new_size = (old_size[0] * new_scale, old_size[1] * new_scale)
141 if not self.check_collision(i, new_size):
142 scales[i] = new_scale
143 elem.size = new_size
144 any_growth = True
145 else:
146 # Can't grow uniformly, try to find maximum possible scale
147 max_scale = self.find_max_scale(i, scales[i])
148 if max_scale > scales[i]:
149 scales[i] = max_scale
150 elem.size = (old_size[0] * max_scale, old_size[1] * max_scale)
151 any_growth = True
153 return any_growth
155 def check_element_collision(self, elem: BaseLayoutElement, new_pos: Tuple[float, float]) -> bool:
156 """
157 Check if moving an element to new_pos would cause collision with other elements.
159 Args:
160 elem: The element to check
161 new_pos: Proposed new position (x, y)
163 Returns:
164 True if collision detected, False otherwise
165 """
166 x, y = new_pos
167 w, h = elem.size
169 for other in self.elements:
170 if other is elem:
171 continue
172 ox, oy = other.position
173 ow, oh = other.size
175 # Check if rectangles overlap (with min_gap consideration)
176 if (
177 abs((x + w / 2) - (ox + ow / 2)) < (w + ow) / 2 + self.min_gap
178 and abs((y + h / 2) - (oy + oh / 2)) < (h + oh) / 2 + self.min_gap
179 ):
180 return True
182 return False
184 def center_element_horizontally(self, elem: BaseLayoutElement) -> None:
185 """
186 Micro-adjust element position to center horizontally in available space.
188 Args:
189 elem: Element to center
190 """
191 x, y = elem.position
192 w, h = elem.size
194 # Calculate available space on each side
195 space_left = x - self.min_gap
196 space_right = (self.page_width - self.min_gap) - (x + w)
198 if space_left >= 0 and space_right >= 0:
199 adjust_x = (space_right - space_left) / 4.0 # Gentle centering
200 new_x = max(self.min_gap, min(self.page_width - w - self.min_gap, x + adjust_x))
202 # Verify this doesn't cause collision
203 old_pos = elem.position
204 new_pos = (new_x, y)
206 if not self.check_element_collision(elem, new_pos):
207 elem.position = new_pos
209 def center_element_vertically(self, elem: BaseLayoutElement) -> None:
210 """
211 Micro-adjust element position to center vertically in available space.
213 Args:
214 elem: Element to center
215 """
216 x, y = elem.position
217 w, h = elem.size
219 # Calculate available space on each side
220 space_top = y - self.min_gap
221 space_bottom = (self.page_height - self.min_gap) - (y + h)
223 if space_top >= 0 and space_bottom >= 0:
224 adjust_y = (space_bottom - space_top) / 4.0
225 new_y = max(self.min_gap, min(self.page_height - h - self.min_gap, y + adjust_y))
227 # Verify this doesn't cause collision
228 old_pos = elem.position
229 new_pos = (x, new_y)
231 if not self.check_element_collision(elem, new_pos):
232 elem.position = new_pos
234 def center_elements(self) -> None:
235 """Center all elements slightly within their constrained space."""
236 for elem in self.elements:
237 self.center_element_horizontally(elem)
238 self.center_element_vertically(elem)
240 def maximize(
241 self, max_iterations: int = 100, growth_rate: float = 0.05
242 ) -> List[Tuple[BaseLayoutElement, Tuple[float, float], Tuple[float, float]]]:
243 """
244 Execute the maximization algorithm.
246 Args:
247 max_iterations: Maximum number of growth iterations
248 growth_rate: Percentage to grow each iteration (0.05 = 5%)
250 Returns:
251 List of (element, old_position, old_size) tuples for undo
252 """
253 scales = [1.0] * len(self.elements)
255 # Growth algorithm - iterative expansion
256 for _ in range(max_iterations):
257 if not self.grow_iteration(scales, growth_rate):
258 break
260 # Center elements slightly within their constrained space
261 self.center_elements()
263 return self.changes
266class AlignmentManager:
267 """Manages alignment and distribution operations on multiple elements"""
269 @staticmethod
270 def get_bounds(elements: List[BaseLayoutElement]) -> Tuple[float, float, float, float]:
271 """
272 Get the bounding box of multiple elements.
274 Returns:
275 (min_x, min_y, max_x, max_y)
276 """
277 if not elements:
278 return (0, 0, 0, 0)
280 min_x = min(elem.position[0] for elem in elements)
281 min_y = min(elem.position[1] for elem in elements)
282 max_x = max(elem.position[0] + elem.size[0] for elem in elements)
283 max_y = max(elem.position[1] + elem.size[1] for elem in elements)
285 return (min_x, min_y, max_x, max_y)
287 @staticmethod
288 def align_left(elements: List[BaseLayoutElement]) -> List[Tuple[BaseLayoutElement, Tuple[float, float]]]:
289 """
290 Align all elements to the leftmost element.
292 Returns:
293 List of (element, old_position) tuples for undo
294 """
295 if len(elements) < 2:
296 return []
298 min_x = min(elem.position[0] for elem in elements)
299 changes = []
301 for elem in elements:
302 old_pos = elem.position
303 elem.position = (min_x, elem.position[1])
304 changes.append((elem, old_pos))
306 return changes
308 @staticmethod
309 def align_right(elements: List[BaseLayoutElement]) -> List[Tuple[BaseLayoutElement, Tuple[float, float]]]:
310 """
311 Align all elements to the rightmost element.
313 Returns:
314 List of (element, old_position) tuples for undo
315 """
316 if len(elements) < 2:
317 return []
319 max_right = max(elem.position[0] + elem.size[0] for elem in elements)
320 changes = []
322 for elem in elements:
323 old_pos = elem.position
324 new_x = max_right - elem.size[0]
325 elem.position = (new_x, elem.position[1])
326 changes.append((elem, old_pos))
328 return changes
330 @staticmethod
331 def align_top(elements: List[BaseLayoutElement]) -> List[Tuple[BaseLayoutElement, Tuple[float, float]]]:
332 """
333 Align all elements to the topmost element.
335 Returns:
336 List of (element, old_position) tuples for undo
337 """
338 if len(elements) < 2:
339 return []
341 min_y = min(elem.position[1] for elem in elements)
342 changes = []
344 for elem in elements:
345 old_pos = elem.position
346 elem.position = (elem.position[0], min_y)
347 changes.append((elem, old_pos))
349 return changes
351 @staticmethod
352 def align_bottom(elements: List[BaseLayoutElement]) -> List[Tuple[BaseLayoutElement, Tuple[float, float]]]:
353 """
354 Align all elements to the bottommost element.
356 Returns:
357 List of (element, old_position) tuples for undo
358 """
359 if len(elements) < 2:
360 return []
362 max_bottom = max(elem.position[1] + elem.size[1] for elem in elements)
363 changes = []
365 for elem in elements:
366 old_pos = elem.position
367 new_y = max_bottom - elem.size[1]
368 elem.position = (elem.position[0], new_y)
369 changes.append((elem, old_pos))
371 return changes
373 @staticmethod
374 def align_horizontal_center(
375 elements: List[BaseLayoutElement],
376 ) -> List[Tuple[BaseLayoutElement, Tuple[float, float]]]:
377 """
378 Align all elements to horizontal center.
380 Returns:
381 List of (element, old_position) tuples for undo
382 """
383 if len(elements) < 2:
384 return []
386 # Calculate average center
387 centers = [elem.position[0] + elem.size[0] / 2 for elem in elements]
388 avg_center = sum(centers) / len(centers)
390 changes = []
391 for elem in elements:
392 old_pos = elem.position
393 new_x = avg_center - elem.size[0] / 2
394 elem.position = (new_x, elem.position[1])
395 changes.append((elem, old_pos))
397 return changes
399 @staticmethod
400 def align_vertical_center(elements: List[BaseLayoutElement]) -> List[Tuple[BaseLayoutElement, Tuple[float, float]]]:
401 """
402 Align all elements to vertical center.
404 Returns:
405 List of (element, old_position) tuples for undo
406 """
407 if len(elements) < 2:
408 return []
410 # Calculate average center
411 centers = [elem.position[1] + elem.size[1] / 2 for elem in elements]
412 avg_center = sum(centers) / len(centers)
414 changes = []
415 for elem in elements:
416 old_pos = elem.position
417 new_y = avg_center - elem.size[1] / 2
418 elem.position = (elem.position[0], new_y)
419 changes.append((elem, old_pos))
421 return changes
423 @staticmethod
424 def make_same_size(
425 elements: List[BaseLayoutElement],
426 ) -> List[Tuple[BaseLayoutElement, Tuple[float, float], Tuple[float, float]]]:
427 """
428 Make all elements the same size as the first element.
430 Returns:
431 List of (element, old_position, old_size) tuples for undo
432 """
433 if len(elements) < 2:
434 return []
436 target_size = elements[0].size
437 changes = []
439 for elem in elements[1:]:
440 old_pos = elem.position
441 old_size = elem.size
442 elem.size = target_size
443 changes.append((elem, old_pos, old_size))
445 return changes
447 @staticmethod
448 def make_same_width(
449 elements: List[BaseLayoutElement],
450 ) -> List[Tuple[BaseLayoutElement, Tuple[float, float], Tuple[float, float]]]:
451 """
452 Make all elements the same width as the first element.
454 Returns:
455 List of (element, old_position, old_size) tuples for undo
456 """
457 if len(elements) < 2:
458 return []
460 target_width = elements[0].size[0]
461 changes = []
463 for elem in elements[1:]:
464 old_pos = elem.position
465 old_size = elem.size
466 elem.size = (target_width, elem.size[1])
467 changes.append((elem, old_pos, old_size))
469 return changes
471 @staticmethod
472 def make_same_height(
473 elements: List[BaseLayoutElement],
474 ) -> List[Tuple[BaseLayoutElement, Tuple[float, float], Tuple[float, float]]]:
475 """
476 Make all elements the same height as the first element.
478 Returns:
479 List of (element, old_position, old_size) tuples for undo
480 """
481 if len(elements) < 2:
482 return []
484 target_height = elements[0].size[1]
485 changes = []
487 for elem in elements[1:]:
488 old_pos = elem.position
489 old_size = elem.size
490 elem.size = (elem.size[0], target_height)
491 changes.append((elem, old_pos, old_size))
493 return changes
495 @staticmethod
496 def distribute_horizontally(
497 elements: List[BaseLayoutElement],
498 ) -> List[Tuple[BaseLayoutElement, Tuple[float, float]]]:
499 """
500 Distribute elements evenly across horizontal span.
502 Returns:
503 List of (element, old_position) tuples for undo
504 """
505 if len(elements) < 3:
506 return []
508 # Sort by x position
509 sorted_elements = sorted(elements, key=lambda e: e.position[0])
511 # Get leftmost and rightmost positions
512 min_x = sorted_elements[0].position[0]
513 max_x = sorted_elements[-1].position[0]
515 # Calculate spacing between centers
516 total_span = max_x - min_x
517 spacing = total_span / (len(sorted_elements) - 1)
519 changes = []
520 for i, elem in enumerate(sorted_elements):
521 old_pos = elem.position
522 new_x = min_x + (i * spacing)
523 elem.position = (new_x, elem.position[1])
524 changes.append((elem, old_pos))
526 return changes
528 @staticmethod
529 def distribute_vertically(elements: List[BaseLayoutElement]) -> List[Tuple[BaseLayoutElement, Tuple[float, float]]]:
530 """
531 Distribute elements evenly across vertical span.
533 Returns:
534 List of (element, old_position) tuples for undo
535 """
536 if len(elements) < 3:
537 return []
539 # Sort by y position
540 sorted_elements = sorted(elements, key=lambda e: e.position[1])
542 # Get topmost and bottommost positions
543 min_y = sorted_elements[0].position[1]
544 max_y = sorted_elements[-1].position[1]
546 # Calculate spacing between centers
547 total_span = max_y - min_y
548 spacing = total_span / (len(sorted_elements) - 1)
550 changes = []
551 for i, elem in enumerate(sorted_elements):
552 old_pos = elem.position
553 new_y = min_y + (i * spacing)
554 elem.position = (elem.position[0], new_y)
555 changes.append((elem, old_pos))
557 return changes
559 @staticmethod
560 def space_horizontally(elements: List[BaseLayoutElement]) -> List[Tuple[BaseLayoutElement, Tuple[float, float]]]:
561 """
562 Distribute elements with equal spacing between them horizontally.
564 Returns:
565 List of (element, old_position) tuples for undo
566 """
567 if len(elements) < 3:
568 return []
570 # Sort by x position
571 sorted_elements = sorted(elements, key=lambda e: e.position[0])
573 # Get leftmost and rightmost boundaries
574 min_x = sorted_elements[0].position[0]
575 max_right = sorted_elements[-1].position[0] + sorted_elements[-1].size[0]
577 # Calculate total width of all elements
578 total_width = sum(elem.size[0] for elem in sorted_elements)
580 # Calculate available space and spacing
581 available_space = max_right - min_x - total_width
582 spacing = available_space / (len(sorted_elements) - 1)
584 changes = []
585 current_x = min_x
587 for elem in sorted_elements:
588 old_pos = elem.position
589 elem.position = (current_x, elem.position[1])
590 changes.append((elem, old_pos))
591 current_x += elem.size[0] + spacing
593 return changes
595 @staticmethod
596 def space_vertically(elements: List[BaseLayoutElement]) -> List[Tuple[BaseLayoutElement, Tuple[float, float]]]:
597 """
598 Distribute elements with equal spacing between them vertically.
600 Returns:
601 List of (element, old_position) tuples for undo
602 """
603 if len(elements) < 3:
604 return []
606 # Sort by y position
607 sorted_elements = sorted(elements, key=lambda e: e.position[1])
609 # Get topmost and bottommost boundaries
610 min_y = sorted_elements[0].position[1]
611 max_bottom = sorted_elements[-1].position[1] + sorted_elements[-1].size[1]
613 # Calculate total height of all elements
614 total_height = sum(elem.size[1] for elem in sorted_elements)
616 # Calculate available space and spacing
617 available_space = max_bottom - min_y - total_height
618 spacing = available_space / (len(sorted_elements) - 1)
620 changes = []
621 current_y = min_y
623 for elem in sorted_elements:
624 old_pos = elem.position
625 elem.position = (elem.position[0], current_y)
626 changes.append((elem, old_pos))
627 current_y += elem.size[1] + spacing
629 return changes
631 @staticmethod
632 def fit_to_page_width(
633 element: BaseLayoutElement, page_width: float
634 ) -> Tuple[BaseLayoutElement, Tuple[float, float], Tuple[float, float]]:
635 """
636 Resize element to fit page width while maintaining aspect ratio.
638 Args:
639 element: The element to resize
640 page_width: The page width in mm
642 Returns:
643 Tuple of (element, old_position, old_size) for undo
644 """
645 old_pos = element.position
646 old_size = element.size
648 # Calculate aspect ratio
649 aspect_ratio = old_size[1] / old_size[0]
651 # Set new size
652 new_width = page_width
653 new_height = page_width * aspect_ratio
654 element.size = (new_width, new_height)
656 return (element, old_pos, old_size)
658 @staticmethod
659 def fit_to_page_height(
660 element: BaseLayoutElement, page_height: float
661 ) -> Tuple[BaseLayoutElement, Tuple[float, float], Tuple[float, float]]:
662 """
663 Resize element to fit page height while maintaining aspect ratio.
665 Args:
666 element: The element to resize
667 page_height: The page height in mm
669 Returns:
670 Tuple of (element, old_position, old_size) for undo
671 """
672 old_pos = element.position
673 old_size = element.size
675 # Calculate aspect ratio
676 aspect_ratio = old_size[0] / old_size[1]
678 # Set new size
679 new_height = page_height
680 new_width = page_height * aspect_ratio
681 element.size = (new_width, new_height)
683 return (element, old_pos, old_size)
685 @staticmethod
686 def fit_to_page(
687 element: BaseLayoutElement, page_width: float, page_height: float
688 ) -> Tuple[BaseLayoutElement, Tuple[float, float], Tuple[float, float]]:
689 """
690 Resize element to fit within page dimensions while maintaining aspect ratio.
692 Args:
693 element: The element to resize
694 page_width: The page width in mm
695 page_height: The page height in mm
697 Returns:
698 Tuple of (element, old_position, old_size) for undo
699 """
700 old_pos = element.position
701 old_size = element.size
703 # Calculate aspect ratios
704 element_aspect = old_size[0] / old_size[1]
705 page_aspect = page_width / page_height
707 # Determine which dimension to fit to
708 if element_aspect > page_aspect:
709 # Element is wider than page - fit to width
710 new_width = page_width
711 new_height = page_width / element_aspect
712 else:
713 # Element is taller than page - fit to height
714 new_height = page_height
715 new_width = page_height * element_aspect
717 element.size = (new_width, new_height)
719 return (element, old_pos, old_size)
721 @staticmethod
722 def maximize_pattern(
723 elements: List[BaseLayoutElement],
724 page_size: Tuple[float, float],
725 min_gap: float = 2.0,
726 max_iterations: int = 100,
727 growth_rate: float = 0.05,
728 ) -> List[Tuple[BaseLayoutElement, Tuple[float, float], Tuple[float, float]]]:
729 """
730 Maximize element sizes using a crystal growth algorithm.
731 Elements grow until they are close to borders or each other.
733 Args:
734 elements: List of elements to maximize
735 page_size: (width, height) of the page in mm
736 min_gap: Minimum gap to maintain between elements and borders (in mm)
737 max_iterations: Maximum number of growth iterations
738 growth_rate: Percentage to grow each iteration (0.05 = 5%)
740 Returns:
741 List of (element, old_position, old_size) tuples for undo
742 """
743 if not elements:
744 return []
746 maximizer = ElementMaximizer(elements, page_size, min_gap)
747 return maximizer.maximize(max_iterations, growth_rate)
749 @staticmethod
750 def expand_to_bounds(
751 element: BaseLayoutElement,
752 page_size: Tuple[float, float],
753 other_elements: List[BaseLayoutElement],
754 min_gap: float = 10.0,
755 ) -> Tuple[BaseLayoutElement, Tuple[float, float], Tuple[float, float]]:
756 """
757 Expand a single element until it is min_gap away from page edges or other elements.
759 This function expands an element from its current position and size, growing it
760 in all directions (up, down, left, right) until it reaches:
761 - The page boundaries (with min_gap margin)
762 - Another element on the same page (with min_gap spacing)
764 The element expands independently in width and height to fill all available space.
766 Args:
767 element: The element to expand
768 page_size: (width, height) of the page in mm
769 other_elements: List of other elements on the same page (excluding the target element)
770 min_gap: Minimum gap to maintain between element and boundaries/other elements (in mm)
772 Returns:
773 Tuple of (element, old_position, old_size) for undo
774 """
775 page_width, page_height = page_size
776 old_pos = element.position
777 old_size = element.size
779 x, y = element.position
780 w, h = element.size
782 # Calculate maximum expansion in each direction
783 # Start with page boundaries
784 max_left = x - min_gap # How much we can expand left
785 max_right = (page_width - min_gap) - (x + w) # How much we can expand right
786 max_top = y - min_gap # How much we can expand up
787 max_bottom = (page_height - min_gap) - (y + h) # How much we can expand down
789 # Check constraints from other elements
790 # We need to be conservative and check ALL elements against ALL expansion directions
791 for other in other_elements:
792 ox, oy = other.position
793 ow, oh = other.size
795 # Calculate the other element's bounds
796 other_left = ox
797 other_right = ox + ow
798 other_top = oy
799 other_bottom = oy + oh
801 # Calculate current element's bounds
802 elem_left = x
803 elem_right = x + w
804 elem_top = y
805 elem_bottom = y + h
807 # Check leftward expansion
808 # An element blocks leftward expansion if:
809 # 1. It's to the left of our left edge (other_right <= elem_left)
810 # 2. Its vertical range would overlap with ANY part of our vertical extent
811 if other_right <= elem_left:
812 # Check if vertical ranges overlap (current OR after any vertical expansion)
813 # Conservative: assume we might expand vertically to page bounds
814 if not (other_bottom <= elem_top - min_gap or other_top >= elem_bottom + min_gap):
815 # This element blocks leftward expansion
816 available_left = elem_left - other_right - min_gap
817 max_left = min(max_left, available_left)
819 # Check rightward expansion
820 if other_left >= elem_right:
821 # Check if vertical ranges overlap
822 if not (other_bottom <= elem_top - min_gap or other_top >= elem_bottom + min_gap):
823 # This element blocks rightward expansion
824 available_right = other_left - elem_right - min_gap
825 max_right = min(max_right, available_right)
827 # Check upward expansion
828 if other_bottom <= elem_top:
829 # Check if horizontal ranges overlap
830 if not (other_right <= elem_left - min_gap or other_left >= elem_right + min_gap):
831 # This element blocks upward expansion
832 available_top = elem_top - other_bottom - min_gap
833 max_top = min(max_top, available_top)
835 # Check downward expansion
836 if other_top >= elem_bottom:
837 # Check if horizontal ranges overlap
838 if not (other_right <= elem_left - min_gap or other_left >= elem_right + min_gap):
839 # This element blocks downward expansion
840 available_bottom = other_top - elem_bottom - min_gap
841 max_bottom = min(max_bottom, available_bottom)
843 # Ensure non-negative expansion
844 max_left = max(0, max_left)
845 max_right = max(0, max_right)
846 max_top = max(0, max_top)
847 max_bottom = max(0, max_bottom)
849 # Expand to fill all available space (no aspect ratio constraint)
850 width_increase = max_left + max_right
851 height_increase = max_top + max_bottom
853 # Calculate new size
854 new_width = w + width_increase
855 new_height = h + height_increase
857 # Calculate new position (expand from center to maintain relative position)
858 # Distribute the expansion proportionally to available space on each side
859 if max_left + max_right > 0:
860 left_ratio = max_left / (max_left + max_right)
861 new_x = x - (width_increase * left_ratio)
862 else:
863 new_x = x
865 if max_top + max_bottom > 0:
866 top_ratio = max_top / (max_top + max_bottom)
867 new_y = y - (height_increase * top_ratio)
868 else:
869 new_y = y
871 # Apply the new position and size
872 element.position = (new_x, new_y)
873 element.size = (new_width, new_height)
875 return (element, old_pos, old_size)