1 """

2 Alignment and distribution manager for pyPhotoAlbum

3 """

4

5 from typing import List, Tuple

6 from pyPhotoAlbum.models import BaseLayoutElement

7

8

9 class ElementMaximizer:

10 """

11 Handles element maximization using a crystal growth algorithm.

12 Breaks down the complex maximize_pattern logic into atomic, testable methods.

13 """

14

15 def __init__(self, elements: List[BaseLayoutElement], page_size: Tuple[float, float], min_gap: float):

16 """

17 Initialize the maximizer with elements and constraints.

18

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()

29

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))

34

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.

38

39 Args:

40 elem_idx: Index of the element to check

41 new_size: Proposed new size (width, height)

42

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

49

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

57

58 # Check collision with other elements

59 for i, other in enumerate(self.elements):

60 if i == elem_idx:

61 continue

62

63 other_x, other_y = other.position

64 other_w, other_h = other.size

65

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 )

70

71 vertical_gap = max(other_y - (y + h), y - (other_y + other_h)) # Other is below # Other is above

72

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

76

77 return False

78

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.

89

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

96

97 Returns:

98 Maximum scale factor that doesn't cause collision

99 """

100 old_size = self.changes[elem_idx][2]

101

102 # Binary search for maximum scale

103 low, high = current_scale, current_scale * max_search_scale

104 best_scale = current_scale

105

106 for _ in range(max_iterations):

107 mid = (low + high) / 2.0

108 test_size = (old_size[0] * mid, old_size[1] * mid)

109

110 if self.check_collision(elem_idx, test_size):

111 high = mid

112 else:

113 best_scale = mid

114 low = mid

115

116 if high - low < tolerance:

117 break

118

119 return best_scale

120

121 def grow_iteration(self, scales: List[float], growth_rate: float) -> bool:

122 """

123 Perform one iteration of the growth algorithm.

124

125 Args:

126 scales: Current scale factors for each element

127 growth_rate: Percentage to grow each iteration (0.05 = 5%)

128

129 Returns:

130 True if any element grew, False otherwise

131 """

132 any_growth = False

133

134 for i, elem in enumerate(self.elements):

135 old_size = self.changes[i][2]

136

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)

140

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

152

153 return any_growth

154

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.

158

159 Args:

160 elem: The element to check

161 new_pos: Proposed new position (x, y)

162

163 Returns:

164 True if collision detected, False otherwise

165 """

166 x, y = new_pos

167 w, h = elem.size

168

169 for other in self.elements:

170 if other is elem:

171 continue

172 ox, oy = other.position

173 ow, oh = other.size

174

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

181

182 return False

183

184 def center_element_horizontally(self, elem: BaseLayoutElement) -> None:

185 """

186 Micro-adjust element position to center horizontally in available space.

187

188 Args:

189 elem: Element to center

190 """

191 x, y = elem.position

192 w, h = elem.size

193

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)

197

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))

201

202 # Verify this doesn't cause collision

203 old_pos = elem.position

204 new_pos = (new_x, y)

205

206 if not self.check_element_collision(elem, new_pos):

207 elem.position = new_pos

208

209 def center_element_vertically(self, elem: BaseLayoutElement) -> None:

210 """

211 Micro-adjust element position to center vertically in available space.

212

213 Args:

214 elem: Element to center

215 """

216 x, y = elem.position

217 w, h = elem.size

218

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)

222

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))

226

227 # Verify this doesn't cause collision

228 old_pos = elem.position

229 new_pos = (x, new_y)

230

231 if not self.check_element_collision(elem, new_pos):

232 elem.position = new_pos

233

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)

239

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.

245

246 Args:

247 max_iterations: Maximum number of growth iterations

248 growth_rate: Percentage to grow each iteration (0.05 = 5%)

249

250 Returns:

251 List of (element, old_position, old_size) tuples for undo

252 """

253 scales = [1.0] * len(self.elements)

254

255 # Growth algorithm - iterative expansion

256 for _ in range(max_iterations):

257 if not self.grow_iteration(scales, growth_rate):

258 break

259

260 # Center elements slightly within their constrained space

261 self.center_elements()

262

263 return self.changes

264

265

266 class AlignmentManager:

267 """Manages alignment and distribution operations on multiple elements"""

268

269 @staticmethod

270 def get_bounds(elements: List[BaseLayoutElement]) -> Tuple[float, float, float, float]:

271 """

272 Get the bounding box of multiple elements.

273

274 Returns:

275 (min_x, min_y, max_x, max_y)

276 """

277 if not elements:

278 return (0, 0, 0, 0)

279

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)

284

285 return (min_x, min_y, max_x, max_y)

286

287 @staticmethod

288 def align_left(elements: List[BaseLayoutElement]) -> List[Tuple[BaseLayoutElement, Tuple[float, float]]]:

289 """

290 Align all elements to the leftmost element.

291

292 Returns:

293 List of (element, old_position) tuples for undo

294 """

295 if len(elements) < 2:

296 return []

297

298 min_x = min(elem.position[0] for elem in elements)

299 changes = []

300

301 for elem in elements:

302 old_pos = elem.position

303 elem.position = (min_x, elem.position[1])

304 changes.append((elem, old_pos))

305

306 return changes

307

308 @staticmethod

309 def align_right(elements: List[BaseLayoutElement]) -> List[Tuple[BaseLayoutElement, Tuple[float, float]]]:

310 """

311 Align all elements to the rightmost element.

312

313 Returns:

314 List of (element, old_position) tuples for undo

315 """

316 if len(elements) < 2:

317 return []

318

319 max_right = max(elem.position[0] + elem.size[0] for elem in elements)

320 changes = []

321

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))

327

328 return changes

329

330 @staticmethod

331 def align_top(elements: List[BaseLayoutElement]) -> List[Tuple[BaseLayoutElement, Tuple[float, float]]]:

332 """

333 Align all elements to the topmost element.

334

335 Returns:

336 List of (element, old_position) tuples for undo

337 """

338 if len(elements) < 2:

339 return []

340

341 min_y = min(elem.position[1] for elem in elements)

342 changes = []

343

344 for elem in elements:

345 old_pos = elem.position

346 elem.position = (elem.position[0], min_y)

347 changes.append((elem, old_pos))

348

349 return changes

350

351 @staticmethod

352 def align_bottom(elements: List[BaseLayoutElement]) -> List[Tuple[BaseLayoutElement, Tuple[float, float]]]:

353 """

354 Align all elements to the bottommost element.

355

356 Returns:

357 List of (element, old_position) tuples for undo

358 """

359 if len(elements) < 2:

360 return []

361

362 max_bottom = max(elem.position[1] + elem.size[1] for elem in elements)

363 changes = []

364

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))

370

371 return changes

372

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.

379

380 Returns:

381 List of (element, old_position) tuples for undo

382 """

383 if len(elements) < 2:

384 return []

385

386 # Calculate average center

387 centers = [elem.position[0] + elem.size[0] / 2 for elem in elements]

388 avg_center = sum(centers) / len(centers)

389

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))

396

397 return changes

398

399 @staticmethod

400 def align_vertical_center(elements: List[BaseLayoutElement]) -> List[Tuple[BaseLayoutElement, Tuple[float, float]]]:

401 """

402 Align all elements to vertical center.

403

404 Returns:

405 List of (element, old_position) tuples for undo

406 """

407 if len(elements) < 2:

408 return []

409

410 # Calculate average center

411 centers = [elem.position[1] + elem.size[1] / 2 for elem in elements]

412 avg_center = sum(centers) / len(centers)

413

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))

420

421 return changes

422

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.

429

430 Returns:

431 List of (element, old_position, old_size) tuples for undo

432 """

433 if len(elements) < 2:

434 return []

435

436 target_size = elements[0].size

437 changes = []

438

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))

444

445 return changes

446

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.

453

454 Returns:

455 List of (element, old_position, old_size) tuples for undo

456 """

457 if len(elements) < 2:

458 return []

459

460 target_width = elements[0].size[0]

461 changes = []

462

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))

468

469 return changes

470

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.

477

478 Returns:

479 List of (element, old_position, old_size) tuples for undo

480 """

481 if len(elements) < 2:

482 return []

483

484 target_height = elements[0].size[1]

485 changes = []

486

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))

492

493 return changes

494

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.

501

502 Returns:

503 List of (element, old_position) tuples for undo

504 """

505 if len(elements) < 3:

506 return []

507

508 # Sort by x position

509 sorted_elements = sorted(elements, key=lambda e: e.position[0])

510

511 # Get leftmost and rightmost positions

512 min_x = sorted_elements[0].position[0]

513 max_x = sorted_elements[-1].position[0]

514

515 # Calculate spacing between centers

516 total_span = max_x - min_x

517 spacing = total_span / (len(sorted_elements) - 1)

518

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))

525

526 return changes

527

528 @staticmethod

529 def distribute_vertically(elements: List[BaseLayoutElement]) -> List[Tuple[BaseLayoutElement, Tuple[float, float]]]:

530 """

531 Distribute elements evenly across vertical span.

532

533 Returns:

534 List of (element, old_position) tuples for undo

535 """

536 if len(elements) < 3:

537 return []

538

539 # Sort by y position

540 sorted_elements = sorted(elements, key=lambda e: e.position[1])

541

542 # Get topmost and bottommost positions

543 min_y = sorted_elements[0].position[1]

544 max_y = sorted_elements[-1].position[1]

545

546 # Calculate spacing between centers

547 total_span = max_y - min_y

548 spacing = total_span / (len(sorted_elements) - 1)

549

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))

556

557 return changes

558

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.

563

564 Returns:

565 List of (element, old_position) tuples for undo

566 """

567 if len(elements) < 3:

568 return []

569

570 # Sort by x position

571 sorted_elements = sorted(elements, key=lambda e: e.position[0])

572

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]

576

577 # Calculate total width of all elements

578 total_width = sum(elem.size[0] for elem in sorted_elements)

579

580 # Calculate available space and spacing

581 available_space = max_right - min_x - total_width

582 spacing = available_space / (len(sorted_elements) - 1)

583

584 changes = []

585 current_x = min_x

586

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

592

593 return changes

594

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.

599

600 Returns:

601 List of (element, old_position) tuples for undo

602 """

603 if len(elements) < 3:

604 return []

605

606 # Sort by y position

607 sorted_elements = sorted(elements, key=lambda e: e.position[1])

608

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]

612

613 # Calculate total height of all elements

614 total_height = sum(elem.size[1] for elem in sorted_elements)

615

616 # Calculate available space and spacing

617 available_space = max_bottom - min_y - total_height

618 spacing = available_space / (len(sorted_elements) - 1)

619

620 changes = []

621 current_y = min_y

622

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

628

629 return changes

630

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.

637

638 Args:

639 element: The element to resize

640 page_width: The page width in mm

641

642 Returns:

643 Tuple of (element, old_position, old_size) for undo

644 """

645 old_pos = element.position

646 old_size = element.size

647

648 # Calculate aspect ratio

649 aspect_ratio = old_size[1] / old_size[0]

650

651 # Set new size

652 new_width = page_width

653 new_height = page_width * aspect_ratio

654 element.size = (new_width, new_height)

655

656 return (element, old_pos, old_size)

657

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.

664

665 Args:

666 element: The element to resize

667 page_height: The page height in mm

668

669 Returns:

670 Tuple of (element, old_position, old_size) for undo

671 """

672 old_pos = element.position

673 old_size = element.size

674

675 # Calculate aspect ratio

676 aspect_ratio = old_size[0] / old_size[1]

677

678 # Set new size

679 new_height = page_height

680 new_width = page_height * aspect_ratio

681 element.size = (new_width, new_height)

682

683 return (element, old_pos, old_size)

684

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.

691

692 Args:

693 element: The element to resize

694 page_width: The page width in mm

695 page_height: The page height in mm

696

697 Returns:

698 Tuple of (element, old_position, old_size) for undo

699 """

700 old_pos = element.position

701 old_size = element.size

702

703 # Calculate aspect ratios

704 element_aspect = old_size[0] / old_size[1]

705 page_aspect = page_width / page_height

706

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

716

717 element.size = (new_width, new_height)

718

719 return (element, old_pos, old_size)

720

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.

732

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%)

739

740 Returns:

741 List of (element, old_position, old_size) tuples for undo

742 """

743 if not elements:

744 return []

745

746 maximizer = ElementMaximizer(elements, page_size, min_gap)

747 return maximizer.maximize(max_iterations, growth_rate)

748

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.

758

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)

763

764 The element expands independently in width and height to fill all available space.

765

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)

771

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

778

779 x, y = element.position

780 w, h = element.size

781

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

788

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

794

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

800

801 # Calculate current element's bounds

802 elem_left = x

803 elem_right = x + w

804 elem_top = y

805 elem_bottom = y + h

806

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)

818

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)

826

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)

834

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)

842

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)

848

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

852

853 # Calculate new size

854 new_width = w + width_increase

855 new_height = h + height_increase

856

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

864

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

870

871 # Apply the new position and size

872 element.position = (new_x, new_y)

873 element.size = (new_width, new_height)

874

875 return (element, old_pos, old_size)