diff --git a/beeref/commands.py b/beeref/commands.py index 0975428..6de04bf 100644 --- a/beeref/commands.py +++ b/beeref/commands.py @@ -74,12 +74,15 @@ class MoveItemsBy(QtGui.QUndoCommand): item.moveBy(-self.delta_x, -self.delta_y) -class ScaleItemsBy(QtGui.QUndoCommand): +class ScaleItemsByDelta(QtGui.QUndoCommand): + """Scale Items by a given delta around the given anchor point. + Delta will be *added* to the current scale factor.""" - def __init__(self, items, factor, ignore_first_redo=False): + def __init__(self, items, delta, anchor, ignore_first_redo=False): super().__init__('Scale items') self.items = items - self.factor = factor + self.delta = delta + self.anchor = anchor self.ignore_first_redo = ignore_first_redo def redo(self): @@ -87,11 +90,15 @@ class ScaleItemsBy(QtGui.QUndoCommand): self.ignore_first_redo = False return for item in self.items: - item.setScale(item.scale() + self.factor) + item.setScale(item.scale() + self.delta) + item.translate_for_scale_anchor( + item.pos(), self.delta, self.anchor) def undo(self): for item in self.items: - item.setScale(item.scale() - self.factor) + item.setScale(item.scale() - self.delta) + item.translate_for_scale_anchor( + item.pos(), -self.delta, self.anchor) class NormalizeItems(QtGui.QUndoCommand): diff --git a/beeref/items.py b/beeref/items.py index 2ac2e05..7504399 100644 --- a/beeref/items.py +++ b/beeref/items.py @@ -35,8 +35,8 @@ class BeePixmapItem(QtWidgets.QGraphicsPixmapItem): select_color = QtGui.QColor(116, 234, 231, 255) SELECT_LINE_WIDTH = 4 # line width for the selection box SELECT_HANDLE_SIZE = 15 # size of selection handles for scaling - SELECT_RESIZE_SIZE = 30 # size of hover area for scaling - SELECT_ROTATE_SIZE = 30 # size of hover area for rotating + SELECT_RESIZE_SIZE = 20 # size of hover area for scaling + SELECT_ROTATE_SIZE = 20 # size of hover area for rotating select_debug = False # Draw debug shapes def __init__(self, image, filename=None): @@ -50,8 +50,7 @@ class BeePixmapItem(QtWidgets.QGraphicsPixmapItem): QtWidgets.QGraphicsItem.GraphicsItemFlags.ItemIsMovable | QtWidgets.QGraphicsItem.GraphicsItemFlags.ItemIsSelectable) - self.single_select_mode = False - self.scale_active = False + self.scale_active_corner = None self.viewport_scale = 1 def __str__(self): @@ -163,14 +162,22 @@ class BeePixmapItem(QtWidgets.QGraphicsPixmapItem): if single_select_mode: pen.setWidth(self.SELECT_HANDLE_SIZE) painter.setPen(pen) - painter.drawPoint(self.width, self.height) + for corner in self.corners: + painter.drawPoint(*corner) @property - def bottom_right_scale_bounds(self): - """The interactable shape of the bottom right scale handle""" + def corners(self): + """The corners of the items. Used for scale and rotate handles.""" + return ((0, 0), + (0, self.height), + (self.width, 0), + (self.width, self.height)) + + def get_scale_bounds(self, center): + """The interactable shape of the scale handles.""" return QtCore.QRectF( - self.width - self.select_resize_size/2, - self.height - self.select_resize_size/2, + center[0] - self.select_resize_size/2, + center[1] - self.select_resize_size/2, self.select_resize_size, self.select_resize_size) @@ -186,6 +193,8 @@ class BeePixmapItem(QtWidgets.QGraphicsPixmapItem): bounds = super().boundingRect() if not self.isSelected(): return bounds + + # Add extra space for scale and rotate interactive areas margin = self.select_resize_size / 2 + self.select_rotate_size return QtCore.QRectF( bounds.topLeft().x() - margin, @@ -196,8 +205,10 @@ class BeePixmapItem(QtWidgets.QGraphicsPixmapItem): def shape(self): shape_ = super().shape() if self.isSelected(): + # Add extra space for scale and rotate interactive areas path = QtGui.QPainterPath() - path.addRect(self.bottom_right_scale_bounds) + for corner in self.corners: + path.addRect(self.get_scale_bounds(corner)) path.addRect(self.bottom_right_rotate_bounds) shape_ = shape_ + path return shape_ @@ -205,48 +216,92 @@ class BeePixmapItem(QtWidgets.QGraphicsPixmapItem): def hoverMoveEvent(self, event): if not self.isSelected() or not self.scene().has_single_selection(): return - if self.bottom_right_scale_bounds.contains(event.pos()): - self.setCursor(Qt.CursorShape.SizeFDiagCursor) - elif self.bottom_right_rotate_bounds.contains(event.pos()): + + for corner in self.corners: + # See if we need to change the cursor for scale areas + if self.get_scale_bounds(corner).contains(event.pos()): + direction = self.get_scale_direction(corner) + if direction[0] == direction[1]: + self.setCursor(Qt.CursorShape.SizeFDiagCursor) + else: + self.setCursor(Qt.CursorShape.SizeBDiagCursor) + return + + if self.bottom_right_rotate_bounds.contains(event.pos()): self.setCursor(Qt.CursorShape.ForbiddenCursor) - else: - self.setCursor(Qt.CursorShape.ArrowCursor) + return + + self.setCursor(Qt.CursorShape.ArrowCursor) def hoverEnterEvent(self, event): + # Always return regular cursor when item isn't selected if not self.isSelected() or not self.scene().has_single_selection(): self.setCursor(Qt.CursorShape.ArrowCursor) def mousePressEvent(self, event): if (event.button() == Qt.MouseButtons.LeftButton - and self.bottom_right_scale_bounds.contains(event.pos()) - and self.isSelected()): - self.scale_active = True - self.orig_scale_factor = self.scale() - self.scale_start = event.scenePos() - event.accept() - else: - super().mousePressEvent(event) + and self.isSelected() and self.scene().has_single_selection()): + for corner in self.corners: + # Check if we are in one of the corner's scale areas + if self.get_scale_bounds(corner).contains(event.pos()): + # Start scale action for this corner + self.scale_active_corner = corner + self.scale_start = event.scenePos() + self.scale_orig_factor = self.scale() + self.scale_orig_pos = self.pos() + event.accept() + return - def get_scale_delta(self, event): + super().mousePressEvent(event) + + def get_scale_delta(self, event, corner): imgsize = self.width + self.height p = event.scenePos() - self.scale_start - return (p.x() + p.y()) / imgsize + direction = self.get_scale_direction(corner) + return (direction[0] * p.x() + direction[1] * p.y()) / imgsize + + def get_scale_anchor(self, corner): + """Get the anchor around which the scale for this corner operates.""" + return(self.width - corner[0], self.height - corner[1]) + + def get_scale_direction(self, corner): + """Get the direction in which the scale for this corner increases""" + x = 1 if corner[0] > 0 else -1 + y = 1 if corner[1] > 0 else -1 + return (x, y) + + def translate_for_scale_anchor(self, orig_pos, scale_delta, anchor): + """Adjust the item's position so that a scale with the given scale + delta appears to operate around the given anchor. ``setScale`` needs + to be called separately with delta *added* to the current item's scale + factor.""" + + self.setPos( + orig_pos.x() - anchor[0] * scale_delta, + orig_pos.y() - anchor[1] * scale_delta, + ) def mouseMoveEvent(self, event): - if self.scale_active: - delta = self.get_scale_delta(event) - self.setScale(self.orig_scale_factor + delta) + if self.scale_active_corner: + delta = self.get_scale_delta(event, self.scale_active_corner) + self.setScale(self.scale_orig_factor + delta) + self.translate_for_scale_anchor( + self.scale_orig_pos, + delta, + self.get_scale_anchor(self.scale_active_corner)) event.accept() else: super().mouseMoveEvent(event) def mouseReleaseEvent(self, event): - if self.scale_active: + if self.scale_active_corner: self.scene().undo_stack.push( - commands.ScaleItemsBy([self], - self.get_scale_delta(event), - ignore_first_redo=True)) - self.scale_active = False + commands.ScaleItemsByDelta( + [self], + self.get_scale_delta(event, self.scale_active_corner), + self.get_scale_anchor(self.scale_active_corner), + ignore_first_redo=True)) + self.scale_active_corner = None event.accept() else: super().mouseReleaseEvent(event) diff --git a/tests/fileio/test_sql.py b/tests/fileio/test_sql.py index afa3e9a..477d515 100644 --- a/tests/fileio/test_sql.py +++ b/tests/fileio/test_sql.py @@ -93,7 +93,6 @@ class SQLiteIOWriteTestCase(BeeTestCase): item.setPos(44, 55) item.setZValue(0.22) item.pixmap_to_bytes = MagicMock(return_value=b'abc') - self.scene.addItem(item) self.io.write() assert item.save_id == 1 @@ -130,7 +129,6 @@ class SQLiteIOWriteTestCase(BeeTestCase): item.setPos(44, 55) item.setZValue(0.22) item.save_id = 1 - self.scene.addItem(item) item.pixmap_to_bytes = MagicMock(return_value=b'abc') self.io.write() item.setScale(0.7) diff --git a/tests/test_commands.py b/tests/test_commands.py index 3f9a5f3..32f4eaf 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -95,26 +95,39 @@ class ScaleItemsByTestCase(BeeTestCase): item1.setScale(1) item2 = BeePixmapItem(QtGui.QImage()) item2.setScale(3) - command = commands.ScaleItemsBy([item1, item2], 2) + command = commands.ScaleItemsByDelta([item1, item2], 2, (100, 100)) command.redo() assert item1.scale() == 3 + assert item1.pos().x() == -200 + assert item1.pos().y() == -200 assert item2.scale() == 5 + assert item2.pos().x() == -200 + assert item2.pos().y() == -200 command.undo() assert item1.scale() == 1 + assert item1.pos().x() == 0 + assert item1.pos().y() == 0 assert item2.scale() == 3 + assert item2.pos().x() == 0 + assert item2.pos().y() == 0 def test_ignore_first_redo(self): item1 = BeePixmapItem(QtGui.QImage()) item1.setScale(1) item2 = BeePixmapItem(QtGui.QImage()) item2.setScale(3) - command = commands.ScaleItemsBy([item1, item2], 2, True) + command = commands.ScaleItemsByDelta([item1, item2], 2, (100, 100), + ignore_first_redo=True) command.redo() assert item1.scale() == 1 assert item2.scale() == 3 + assert item1.pos().x() == 0 + assert item1.pos().y() == 0 command.redo() assert item1.scale() == 3 assert item2.scale() == 5 + assert item1.pos().x() == -200 + assert item1.pos().y() == -200 class NormalizeItemsTestCase(BeeTestCase): diff --git a/tests/test_items.py b/tests/test_items.py index 94bbf71..51e5c44 100644 --- a/tests/test_items.py +++ b/tests/test_items.py @@ -1,6 +1,7 @@ from unittest.mock import patch, MagicMock, PropertyMock from PyQt6 import QtCore, QtGui, QtWidgets +from PyQt6.QtCore import Qt from beeref.items import BeePixmapItem from beeref.scene import BeeGraphicsScene @@ -25,10 +26,13 @@ class BeePixmapItemTestCase(BeeTestCase): assert item.filename == self.imgfilename3x3 def test_set_scale(self): - item = BeePixmapItem(QtGui.QImage()) + item = BeePixmapItem( + QtGui.QImage(self.imgfilename3x3), self.imgfilename3x3) item.prepareGeometryChange = MagicMock() item.setScale(3) assert item.scale() == 3 + assert item.pos().x() == 0 + assert item.pos().y() == 0 item.prepareGeometryChange.assert_called_once() def test_set_scale_ignores_zero(self): @@ -76,18 +80,37 @@ class BeePixmapItemTestCase(BeeTestCase): assert item2.zValue() > item1.zValue() assert item2.zValue() == self.scene.max_z + def test_on_view_scale_change(self): + item = BeePixmapItem(QtGui.QImage()) + with patch('beeref.items.BeePixmapItem.prepareGeometryChange') as m: + item.on_view_scale_change() + m.assert_called_once() -class BeePixmapItemPaintstuffTestCase(BeeTestCase): + +class BeePixmapItemWithViewBaseTestCase(BeeTestCase): def setUp(self): self.scene = BeeGraphicsScene(None) self.item = BeePixmapItem(QtGui.QImage()) self.scene.addItem(self.item) self.view = MagicMock(get_scale=MagicMock(return_value=1)) - views_patcher = patch('beeref.scene.BeeGraphicsScene.views') - views_mock = views_patcher.start() - views_mock.return_value = [self.view] + views_patcher = patch('beeref.scene.BeeGraphicsScene.views', + return_value=[self.view]) + views_patcher.start() self.addCleanup(views_patcher.stop) + width_patcher = patch('beeref.items.BeePixmapItem.width', + new_callable=PropertyMock, + return_value=100) + width_patcher.start() + self.addCleanup(width_patcher.stop) + height_patcher = patch('beeref.items.BeePixmapItem.height', + new_callable=PropertyMock, + return_value=80) + height_patcher.start() + self.addCleanup(height_patcher.stop) + + +class BeePixmapItemPaintstuffTestCase(BeePixmapItemWithViewBaseTestCase): def test_fixed_length_for_viewport_when_default_scales(self): self.view.get_scale = MagicMock(return_value=1) @@ -150,32 +173,31 @@ class BeePixmapItemPaintstuffTestCase(BeeTestCase): painter.drawRect.assert_called_once() painter.drawPoint.assert_not_called() - def test_bottom_right_scale_bounds(self): + def test_corners(self): + assert set(self.item.corners) == set(( + (0, 0), + (100, 0), + (0, 80), + (100, 80))) + + def test_get_scale_bounds(self): self.view.get_scale = MagicMock(return_value=1) self.item.SELECT_RESIZE_SIZE = 10 - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): - rect = self.item.bottom_right_scale_bounds - assert rect.topLeft().x() == 95 - assert rect.topLeft().y() == 75 - assert rect.bottomRight().x() == 105 - assert rect.bottomRight().y() == 85 + rect = self.item.get_scale_bounds((100, 100)) + assert rect.topLeft().x() == 95 + assert rect.topLeft().y() == 95 + assert rect.bottomRight().x() == 105 + assert rect.bottomRight().y() == 105 def test_bottom_right_rotate_bounds(self): self.view.get_scale = MagicMock(return_value=1) self.item.SELECT_RESIZE_SIZE = 10 self.item.SELECT_ROTATE_SIZE = 10 - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): - rect = self.item.bottom_right_rotate_bounds - assert rect.topLeft().x() == 105 - assert rect.topLeft().y() == 85 - assert rect.bottomRight().x() == 115 - assert rect.bottomRight().y() == 95 + rect = self.item.bottom_right_rotate_bounds + assert rect.topLeft().x() == 105 + assert rect.topLeft().y() == 85 + assert rect.bottomRight().x() == 115 + assert rect.bottomRight().y() == 95 def test_bounding_rect_when_not_selected(self): self.view.get_scale = MagicMock(return_value=1) @@ -209,15 +231,11 @@ class BeePixmapItemPaintstuffTestCase(BeeTestCase): with patch('PyQt6.QtWidgets.QGraphicsPixmapItem.shape', return_value=path): - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): - shape = self.item.shape().boundingRect() - assert shape.topLeft().x() == 0 - assert shape.topLeft().y() == 0 - assert shape.bottomRight().x() == 100 - assert shape.bottomRight().y() == 80 + shape = self.item.shape().boundingRect() + assert shape.topLeft().x() == 0 + assert shape.topLeft().y() == 0 + assert shape.bottomRight().x() == 100 + assert shape.bottomRight().y() == 80 def test_shape_when_selected(self): self.item.SELECT_RESIZE_SIZE = 10 @@ -229,12 +247,186 @@ class BeePixmapItemPaintstuffTestCase(BeeTestCase): with patch('PyQt6.QtWidgets.QGraphicsPixmapItem.shape', return_value=path): - with patch('beeref.items.BeePixmapItem.width', - new_callable=PropertyMock, return_value=100): - with patch('beeref.items.BeePixmapItem.height', - new_callable=PropertyMock, return_value=80): - shape = self.item.shape().boundingRect() - assert shape.topLeft().x() == 0 - assert shape.topLeft().y() == 0 - assert shape.bottomRight().x() == 115 - assert shape.bottomRight().y() == 95 + shape = self.item.shape().boundingRect() + assert shape.topLeft().x() == -5 + assert shape.topLeft().y() == -5 + assert shape.bottomRight().x() == 115 + assert shape.bottomRight().y() == 95 + + +class BeePixmapItemScalingTestCase(BeePixmapItemWithViewBaseTestCase): + + def test_get_scale_delta_bottomright(self): + self.item.scale_start = QtCore.QPoint(10, 10) + event = MagicMock() + event.scenePos = MagicMock(return_value=QtCore.QPoint(20, 90)) + assert self.item.get_scale_delta(event, (100, 80)) == 0.5 + + def test_get_scale_delta_topleft(self): + self.item.scale_start = QtCore.QPoint(10, 10) + event = MagicMock() + event.scenePos = MagicMock(return_value=QtCore.QPoint(-10, -60)) + assert self.item.get_scale_delta(event, (0, 0)) == 0.5 + + def test_get_scale_anchor_topleft(self): + assert self.item.get_scale_anchor((0, 0)) == (100, 80) + + def test_get_scale_anchor_bottomright(self): + assert self.item.get_scale_anchor((100, 80)) == (0, 0) + + def test_get_scale_anchor_topright(self): + assert self.item.get_scale_anchor((100, 0)) == (0, 80) + + def test_get_scale_anchor_bottomleft(self): + assert self.item.get_scale_anchor((0, 80)) == (100, 0) + + def test_get_scale_direction_topleft(self): + assert self.item.get_scale_direction((0, 0)) == (-1, -1) + + def test_get_scale_direction_bottomright(self): + assert self.item.get_scale_direction((100, 80)) == (1, 1) + + def test_get_scale_direction_topright(self): + assert self.item.get_scale_direction((100, 0)) == (1, -1) + + def test_get_scale_direction_bottomleft(self): + assert self.item.get_scale_direction((0, 80)) == (-1, 1) + + def test_translate_for_scale_anchor(self): + pos = QtCore.QPoint(50, 70) + self.item.translate_for_scale_anchor(pos, 2, (100, 80)) + assert self.item.pos().x() == -150 + assert self.item.pos().y() == -90 + + +class BeePixmapItemEventsstuffTestCase(BeePixmapItemWithViewBaseTestCase): + + def setUp(self): + super().setUp() + self.event = MagicMock() + self.item.setCursor = MagicMock() + + def test_hover_move_event_no_selection(self): + self.event.pos = MagicMock(return_value=QtCore.QPointF(0, 0)) + self.item.hoverMoveEvent(self.event) + self.item.setCursor.assert_not_called() + + def test_hover_move_event_topleft_scale(self): + self.item.setSelected(True) + self.event.pos = MagicMock(return_value=QtCore.QPointF(0, 0)) + self.item.hoverMoveEvent(self.event) + self.item.setCursor.assert_called_once_with( + Qt.CursorShape.SizeFDiagCursor) + + def test_hover_move_event_bottomright_scale(self): + self.item.setSelected(True) + self.event.pos = MagicMock(return_value=QtCore.QPointF(100, 80)) + self.item.hoverMoveEvent(self.event) + self.item.setCursor.assert_called_once_with( + Qt.CursorShape.SizeFDiagCursor) + + def test_hover_move_event_topright_scale(self): + self.item.setSelected(True) + self.event.pos = MagicMock(return_value=QtCore.QPointF(100, 0)) + self.item.hoverMoveEvent(self.event) + self.item.setCursor.assert_called_once_with( + Qt.CursorShape.SizeBDiagCursor) + + def test_hover_move_event_not_in_handles(self): + self.item.setSelected(True) + self.event.pos = MagicMock(return_value=QtCore.QPointF(50, 50)) + self.item.hoverMoveEvent(self.event) + self.item.setCursor.assert_called_once_with( + Qt.CursorShape.ArrowCursor) + + def test_hover_enter_event_when_selected(self): + self.item.setSelected(True) + self.item.hoverEnterEvent(self.event) + self.item.setCursor.assert_not_called() + + def test_hover_enter_event_when_not_selected(self): + self.item.setSelected(False) + self.item.hoverEnterEvent(self.event) + self.item.setCursor.assert_called_once_with( + Qt.CursorShape.ArrowCursor) + + def test_mouse_press_event_topleft_scale(self): + self.item.setSelected(True) + self.event.pos = MagicMock(return_value=QtCore.QPointF(2, 2)) + self.event.scenePos = MagicMock(return_value=QtCore.QPointF(66, 99)) + self.event.button = MagicMock( + return_value=Qt.MouseButtons.LeftButton) + self.item.mousePressEvent(self.event) + assert self.item.scale_active_corner == (0, 0) + assert self.item.scale_start == QtCore.QPointF(66, 99) + assert self.item.scale_orig_factor == 1 + assert self.item.scale_orig_pos == QtCore.QPointF(0, 0) + + def test_mouse_press_event_bottomright_scale(self): + self.item.setSelected(True) + self.event.pos = MagicMock(return_value=QtCore.QPointF(99, 79)) + self.event.scenePos = MagicMock(return_value=QtCore.QPointF(66, 99)) + self.event.button = MagicMock( + return_value=Qt.MouseButtons.LeftButton) + self.item.mousePressEvent(self.event) + assert self.item.scale_active_corner == (100, 80) + assert self.item.scale_start == QtCore.QPointF(66, 99) + assert self.item.scale_orig_factor == 1 + assert self.item.scale_orig_pos == QtCore.QPointF(0, 0) + + def test_mouse_press_event_not_selected(self): + self.item.setSelected(False) + with patch('PyQt6.QtWidgets.QGraphicsPixmapItem.mousePressEvent') as m: + self.item.mousePressEvent(self.event) + m.assert_called_once_with(self.event) + assert self.item.scale_active_corner is None + + def test_mouse_press_not_in_handles(self): + self.item.setSelected(True) + self.event.pos = MagicMock(return_value=QtCore.QPointF(50, 40)) + self.event.button = MagicMock( + return_value=Qt.MouseButtons.LeftButton) + with patch('PyQt6.QtWidgets.QGraphicsPixmapItem.mousePressEvent') as m: + self.item.mousePressEvent(self.event) + m.assert_called_once_with(self.event) + assert self.item.scale_active_corner is None + + def test_mouse_move_event_when_no_action(self): + with patch('PyQt6.QtWidgets.QGraphicsPixmapItem.mouseMoveEvent') as m: + self.item.mouseMoveEvent(self.event) + m.assert_called_once_with(self.event) + + def test_move_event_when_scale_action(self): + self.event.scenePos = MagicMock(return_value=QtCore.QPointF(20, 90)) + self.item.scale_active_corner = (100, 80) + self.item.scale_start = QtCore.QPointF(10, 10) + self.item.scale_orig_factor = 1 + self.item.scale_orig_pos = QtCore.QPointF(0, 0) + + self.item.mouseMoveEvent(self.event) + assert self.item.scale() == 1.5 + + def test_mouse_release_event_when_no_action(self): + with patch('PyQt6.QtWidgets.QGraphicsPixmapItem' + '.mouseReleaseEvent') as m: + self.item.mouseReleaseEvent(self.event) + m.assert_called_once_with(self.event) + + def test_mouse_release_event_when_scale_action(self): + self.event.scenePos = MagicMock(return_value=QtCore.QPointF(20, 90)) + self.item.scale_active_corner = (100, 80) + self.item.scale_start = QtCore.QPointF(10, 10) + self.item.scale_orig_factor = 1 + self.item.scale_orig_pos = QtCore.QPointF(0, 0) + self.scene.undo_stack = MagicMock() + self.scene.undo_stack.push = MagicMock() + + self.item.mouseReleaseEvent(self.event) + self.scene.undo_stack.push.assert_called_once() + args = self.scene.undo_stack.push.call_args_list[0][0] + cmd = args[0] + assert cmd.items == [self.item] + assert cmd.delta == 0.5 + assert cmd.anchor == (0, 0) + assert cmd.ignore_first_redo is True + assert self.item.scale_active_corner is None