mirror of
https://github.com/rbreu/beeref.git
synced 2026-03-11 08:54:28 +00:00
Add action: Arrange Optimal
This commit is contained in:
parent
e8e7a0fc1e
commit
24599673ef
8 changed files with 145 additions and 0 deletions
|
|
@ -98,6 +98,14 @@ actions = [
|
|||
'group': 'active_when_selection',
|
||||
'enabled': False,
|
||||
},
|
||||
{
|
||||
'id': 'arrange_optimal',
|
||||
'text': '&Optimal',
|
||||
'shortcuts': ['Shift+O'],
|
||||
'callback': 'on_action_arrange_optimal',
|
||||
'group': 'active_when_selection',
|
||||
'enabled': False,
|
||||
},
|
||||
{
|
||||
'id': 'flip_horizontally',
|
||||
'text': 'Flip &Horizontally',
|
||||
|
|
|
|||
|
|
@ -73,6 +73,12 @@ menu_structure = [
|
|||
'normalize_size',
|
||||
],
|
||||
},
|
||||
{
|
||||
'menu': '&Arrange',
|
||||
'items': [
|
||||
'arrange_optimal',
|
||||
],
|
||||
},
|
||||
{
|
||||
'menu': '&Help',
|
||||
'items': [
|
||||
|
|
|
|||
|
|
@ -241,3 +241,25 @@ class ResetTransforms(QtGui.QUndoCommand):
|
|||
item.setRotation(old['rotation'], anchor=item.center)
|
||||
if old['flip'] == -1:
|
||||
item.do_flip(anchor=item.center)
|
||||
|
||||
|
||||
class ArrangeItems(QtGui.QUndoCommand):
|
||||
|
||||
def __init__(self, scene, items, positions):
|
||||
super().__init__('Arrange items')
|
||||
self.scene = scene
|
||||
self.items = items
|
||||
self.positions = positions
|
||||
|
||||
def redo(self):
|
||||
self.old_positions = []
|
||||
for item, pos in zip(self.items, self.positions):
|
||||
self.old_positions.append(item.pos())
|
||||
orig_topleft = item.corners_scene_coords[0]
|
||||
rect_topleft = self.scene.itemsBoundingRect(
|
||||
items=[item]).topLeft()
|
||||
item.setPos(pos + orig_topleft - rect_topleft)
|
||||
|
||||
def undo(self):
|
||||
for item, pos in zip(self.items, self.old_positions):
|
||||
item.setPos(pos)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ import math
|
|||
from PyQt6 import QtCore, QtWidgets
|
||||
from PyQt6.QtCore import Qt
|
||||
|
||||
import rpack
|
||||
|
||||
from beeref import commands
|
||||
from beeref.selection import MultiSelectItem, RubberbandItem
|
||||
|
||||
|
|
@ -99,6 +101,44 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene):
|
|||
commands.NormalizeItems(
|
||||
self.selectedItems(user_only=True), scale_factors))
|
||||
|
||||
def arrange_optimal(self):
|
||||
items = self.selectedItems(user_only=True)
|
||||
sizes = []
|
||||
for item in items:
|
||||
rect = self.itemsBoundingRect(items=[item])
|
||||
sizes.append((round(rect.width()), round(rect.height())))
|
||||
|
||||
if not sizes:
|
||||
return
|
||||
|
||||
center = self.get_selection_center()
|
||||
|
||||
# The minimal area the items need if they could be packed optimally;
|
||||
# we use this as a starting shape for the packing algorithm
|
||||
min_area = sum(map(lambda s: s[0] * s[1], sizes))
|
||||
width = math.ceil(math.sqrt(min_area))
|
||||
|
||||
positions = None
|
||||
while not positions:
|
||||
try:
|
||||
positions = rpack.pack(
|
||||
sizes, max_width=width, max_height=width)
|
||||
except rpack.PackingImpossibleError:
|
||||
width = math.ceil(width * 1.2)
|
||||
|
||||
if rpack.overlapping(sizes, positions):
|
||||
# Bug in rpack:
|
||||
# https://github.com/Penlect/rectangle-packer/issues/4#issuecomment-822411097
|
||||
positions = [(p[1], p[0]) for p in positions]
|
||||
|
||||
# We want the items to center around the selection's center,
|
||||
# not (0, 0)
|
||||
bounds = rpack.bbox_size(sizes, positions)
|
||||
diff = center - QtCore.QPointF(bounds[0]/2, bounds[1]/2)
|
||||
positions = [QtCore.QPointF(*pos) + diff for pos in positions]
|
||||
|
||||
self.undo_stack.push(commands.ArrangeItems(self, items, positions))
|
||||
|
||||
def flip_items(self, vertical=False):
|
||||
"""Flip selected items."""
|
||||
self.undo_stack.push(
|
||||
|
|
|
|||
|
|
@ -196,6 +196,9 @@ class BeeGraphicsView(QtWidgets.QGraphicsView, ActionsMixin):
|
|||
def on_action_normalize_size(self):
|
||||
self.scene.normalize_size()
|
||||
|
||||
def on_action_arrange_optimal(self):
|
||||
self.scene.arrange_optimal()
|
||||
|
||||
def on_action_flip_horizontally(self):
|
||||
self.scene.flip_items(vertical=False)
|
||||
|
||||
|
|
|
|||
1
setup.py
1
setup.py
|
|
@ -10,6 +10,7 @@ setup(
|
|||
description='A simple reference image viewer',
|
||||
install_requires=[
|
||||
'pyQt6',
|
||||
'rectangle-packer=>2.0.0',
|
||||
],
|
||||
packages=['beeref'],
|
||||
entry_points={
|
||||
|
|
|
|||
|
|
@ -395,3 +395,30 @@ class ResetTransformsTestCase(BeeTestCase):
|
|||
assert item2.flip() == 1
|
||||
assert item2.pos().x() == 0
|
||||
assert item2.pos().y() == 0
|
||||
|
||||
|
||||
class ArrangeItemsTestCase(BeeTestCase):
|
||||
|
||||
def test_redo_undo(self):
|
||||
scene = BeeGraphicsScene(None)
|
||||
item1 = BeePixmapItem(QtGui.QImage())
|
||||
item1.do_flip()
|
||||
scene.addItem(item1)
|
||||
item2 = BeePixmapItem(QtGui.QImage())
|
||||
item2.setRotation(90)
|
||||
scene.addItem(item2)
|
||||
with patch('beeref.items.BeePixmapItem.width',
|
||||
new_callable=PropertyMock, return_value=100):
|
||||
with patch('beeref.items.BeePixmapItem.height',
|
||||
new_callable=PropertyMock, return_value=80):
|
||||
command = commands.ArrangeItems(
|
||||
scene,
|
||||
[item1, item2],
|
||||
[QtCore.QPointF(1, 2), QtCore.QPointF(203, 204)])
|
||||
|
||||
command.redo()
|
||||
assert item1.pos() == QtCore.QPointF(101, 2)
|
||||
assert item2.pos() == QtCore.QPointF(283, 204)
|
||||
command.undo()
|
||||
assert item1.pos() == QtCore.QPointF(0, 0)
|
||||
assert item2.pos() == QtCore.QPointF(0, 0)
|
||||
|
|
|
|||
|
|
@ -144,6 +144,44 @@ class BeeGraphicsSceneTestCase(BeeTestCase):
|
|||
def test_normalize_size_when_no_items(self):
|
||||
self.scene.normalize_size()
|
||||
|
||||
def test_arrange_optimal(self):
|
||||
for i in range(4):
|
||||
item = BeePixmapItem(QtGui.QImage())
|
||||
self.scene.addItem(item)
|
||||
item.setSelected(True)
|
||||
with patch('beeref.items.BeePixmapItem.width',
|
||||
new_callable=PropertyMock, return_value=100):
|
||||
with patch('beeref.items.BeePixmapItem.height',
|
||||
new_callable=PropertyMock, return_value=80):
|
||||
self.scene.arrange_optimal()
|
||||
|
||||
expected_positions = {(-50, -40), (50, -40), (-50, 40), (50, 40)}
|
||||
actual_positions = {
|
||||
(i.pos().x(), i.pos().y())
|
||||
for i in self.scene.selectedItems(user_only=True)}
|
||||
assert expected_positions == actual_positions
|
||||
|
||||
def test_arrange_optimal_when_rotated(self):
|
||||
for i in range(4):
|
||||
item = BeePixmapItem(QtGui.QImage())
|
||||
self.scene.addItem(item)
|
||||
item.setRotation(90)
|
||||
item.setSelected(True)
|
||||
with patch('beeref.items.BeePixmapItem.width',
|
||||
new_callable=PropertyMock, return_value=100):
|
||||
with patch('beeref.items.BeePixmapItem.height',
|
||||
new_callable=PropertyMock, return_value=80):
|
||||
self.scene.arrange_optimal()
|
||||
|
||||
expected_positions = {(-40, -50), (40, -50), (-40, 50), (40, 50)}
|
||||
actual_positions = {
|
||||
(i.pos().x(), i.pos().y())
|
||||
for i in self.scene.selectedItems(user_only=True)}
|
||||
assert expected_positions == actual_positions
|
||||
|
||||
def test_arrange_optimal_when_no_items(self):
|
||||
self.scene.arrange_optimal()
|
||||
|
||||
def test_flip_items(self):
|
||||
item = BeePixmapItem(QtGui.QImage())
|
||||
self.scene.addItem(item)
|
||||
|
|
|
|||
Loading…
Reference in a new issue