mirror of
https://github.com/rbreu/beeref.git
synced 2026-03-11 08:54:28 +00:00
633 lines
23 KiB
Python
633 lines
23 KiB
Python
# This file is part of BeeRef.
|
|
#
|
|
# BeeRef is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# BeeRef is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with BeeRef. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
"""Classes for items that draw and handle selection stuff."""
|
|
|
|
import logging
|
|
import math
|
|
|
|
from PyQt6 import QtCore, QtGui, QtWidgets
|
|
from PyQt6.QtCore import Qt
|
|
from PyQt6.QtWidgets import QGraphicsItem
|
|
|
|
from beeref.assets import BeeAssets
|
|
from beeref import commands
|
|
from beeref.config import CommandlineArgs
|
|
from beeref import utils
|
|
|
|
|
|
commandline_args = CommandlineArgs()
|
|
logger = logging.getLogger(__name__)
|
|
SELECT_COLOR = QtGui.QColor(116, 234, 231, 255)
|
|
|
|
|
|
def with_anchor(func):
|
|
"""Decorator that adds an anchor parameter to transform operations.
|
|
|
|
The anchor is given in item coordinates.
|
|
"""
|
|
|
|
def wrapper(self, *args, **kwargs):
|
|
# We caculate where the anchor is before and after the transformation
|
|
# and then move the item accordingly to keep the anchor fixed
|
|
|
|
anchor = kwargs.pop('anchor', None)
|
|
if not anchor:
|
|
if args and isinstance(args[-1], QtCore.QPointF):
|
|
anchor = args[-1]
|
|
args = args[:-1]
|
|
|
|
anchor = anchor or QtCore.QPointF(0, 0)
|
|
prev = self.mapToScene(anchor)
|
|
func(self, *args, **kwargs)
|
|
diff = self.mapToScene(anchor) - prev
|
|
self.setPos(self.pos() - diff)
|
|
|
|
return wrapper
|
|
|
|
|
|
class BaseItemMixin:
|
|
|
|
@with_anchor
|
|
def setScale(self, value):
|
|
if value <= 0:
|
|
return
|
|
|
|
logger.debug(f'Setting scale for {self} to {value}')
|
|
self.prepareGeometryChange()
|
|
super().setScale(value)
|
|
|
|
def setZValue(self, value):
|
|
logger.debug(f'Setting z-value for {self} to {value}')
|
|
super().setZValue(value)
|
|
if self.scene():
|
|
self.scene().max_z = max(self.scene().max_z, value)
|
|
self.scene().min_z = min(self.scene().min_z, value)
|
|
|
|
def bring_to_front(self):
|
|
self.setZValue(self.scene().max_z + self.scene().Z_STEP)
|
|
|
|
@with_anchor
|
|
def setRotation(self, value):
|
|
logger.debug(f'Setting rotation for {self} to {value}')
|
|
super().setRotation(value % 360)
|
|
|
|
def flip(self):
|
|
"""Returns the flip value (1 or -1)"""
|
|
# We use the transformation matrix only for flipping, so checking
|
|
# the x scale is enough
|
|
return self.transform().m11()
|
|
|
|
@with_anchor
|
|
def do_flip(self, vertical=False):
|
|
"""Flips the item."""
|
|
self.setTransform(QtGui.QTransform.fromScale(-self.flip(), 1))
|
|
if vertical:
|
|
self.setRotation(self.rotation() + 180)
|
|
|
|
@property
|
|
def center(self):
|
|
return QtCore.QPointF(self.width, self.height) / 2
|
|
|
|
@property
|
|
def center_scene_coords(self):
|
|
"""The item's center in scene coordinates."""
|
|
return self.mapToScene(self.center)
|
|
|
|
|
|
class SelectableMixin(BaseItemMixin):
|
|
"""Common code for selectable items: Selection outline, handles etc."""
|
|
|
|
SELECT_LINE_WIDTH = 4 # line width for the selection box
|
|
SELECT_HANDLE_SIZE = 15 # size of selection handles for scaling
|
|
SELECT_RESIZE_SIZE = 20 # size of hover area for scaling
|
|
SELECT_ROTATE_SIZE = 10 # size of hover area for rotating
|
|
|
|
def init_selectable(self):
|
|
self.setAcceptHoverEvents(True)
|
|
self.setFlags(
|
|
QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable
|
|
| QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)
|
|
|
|
self.viewport_scale = 1
|
|
self.reset_actions()
|
|
|
|
def reset_actions(self):
|
|
self.scale_active = False
|
|
self.rotate_active = False
|
|
self.flip_active = False
|
|
self.just_selected = False
|
|
|
|
def is_action_active(self):
|
|
return any((self.scale_active,
|
|
self.rotate_active,
|
|
self.flip_active))
|
|
|
|
def fixed_length_for_viewport(self, value):
|
|
"""The interactable areas need to stay the same size on the
|
|
screen so we need to adjust the values according to the scale
|
|
factor sof the view and the item."""
|
|
|
|
if self.scene():
|
|
scale = self.scene().views()[0].get_scale()
|
|
self._view_scale = scale
|
|
|
|
# It can happen that the item is already removed from
|
|
# the scene but its boundingRect is still needed. Keep the
|
|
# last known scaling factor for that case
|
|
return value / getattr(self, '_view_scale', 1) / self.scale()
|
|
|
|
@property
|
|
def select_resize_size(self):
|
|
return self.fixed_length_for_viewport(self.SELECT_RESIZE_SIZE)
|
|
|
|
@property
|
|
def select_rotate_size(self):
|
|
return self.fixed_length_for_viewport(self.SELECT_ROTATE_SIZE)
|
|
|
|
def draw_debug_shape(self, painter, shape, r, g, b):
|
|
color = QtGui.QColor(r, g, b, 50)
|
|
if isinstance(shape, QtCore.QRectF):
|
|
painter.fillRect(shape, color)
|
|
else:
|
|
painter.fillPath(shape, color)
|
|
|
|
def paint_selectable(self, painter, option, widget):
|
|
if commandline_args.debug_shapes:
|
|
self.draw_debug_shape(painter, self.shape(), 255, 0, 0)
|
|
if commandline_args.debug_boundingrects:
|
|
self.draw_debug_shape(painter, self.boundingRect(), 0, 255, 0)
|
|
if (commandline_args.debug_handles and self.has_selection_handles()):
|
|
for corner in self.corners:
|
|
self.draw_debug_shape(
|
|
painter, self.get_scale_bounds(corner), 0, 0, 255)
|
|
self.draw_debug_shape(
|
|
painter, self.get_rotate_bounds(corner), 0, 255, 255)
|
|
for edge in self.get_flip_bounds():
|
|
self.draw_debug_shape(painter, edge['rect'], 255, 255, 0)
|
|
|
|
if not self.has_selection_outline():
|
|
return
|
|
|
|
pen = QtGui.QPen(SELECT_COLOR)
|
|
pen.setWidth(self.SELECT_LINE_WIDTH)
|
|
pen.setCosmetic(True)
|
|
painter.setPen(pen)
|
|
|
|
# Draw the main selection rectangle
|
|
painter.drawRect(0, 0, self.width, self.height)
|
|
|
|
# If it's a single selection, draw the handles:
|
|
if self.has_selection_handles():
|
|
pen.setWidth(self.SELECT_HANDLE_SIZE)
|
|
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
|
|
painter.setPen(pen)
|
|
for corner in self.corners:
|
|
painter.drawPoint(corner)
|
|
|
|
@property
|
|
def corners(self):
|
|
"""The corners of the item. Used for scale and rotate handles."""
|
|
return (QtCore.QPointF(0, 0),
|
|
QtCore.QPointF(self.width, 0),
|
|
QtCore.QPointF(self.width, self.height),
|
|
QtCore.QPointF(0, self.height))
|
|
|
|
@property
|
|
def corners_scene_coords(self):
|
|
"""The corners of the item mapped to scene coordinates."""
|
|
|
|
return [self.mapToScene(corner) for corner in self.corners]
|
|
|
|
def get_scale_bounds(self, corner, margin=0):
|
|
"""The interactable shape of the scale handles. The scale handles sit
|
|
centered around the visible handle."""
|
|
path = QtGui.QPainterPath()
|
|
path.addRect(QtCore.QRectF(
|
|
corner.x() - self.select_resize_size/2 - margin,
|
|
corner.y() - self.select_resize_size/2 - margin,
|
|
self.select_resize_size + 2 * margin,
|
|
self.select_resize_size + 2 * margin))
|
|
return path
|
|
|
|
def get_rotate_bounds(self, corner):
|
|
"""The interactable shape of the rotation area. It sits around the
|
|
scale area like an L shape, e.g. for the bottom right corner:
|
|
│
|
|
┌┴┬─┐
|
|
─┤S│R│
|
|
├─┘ │
|
|
│R R│
|
|
└───┘
|
|
"""
|
|
|
|
path = QtGui.QPainterPath()
|
|
|
|
# The whole square containing the rotate area:
|
|
d = self.get_corner_direction(corner)
|
|
p1 = corner - d * self.select_resize_size / 2
|
|
p2 = p1 + d * (self.select_resize_size + self.select_rotate_size)
|
|
path.addRect(utils.get_rect_from_points(p1, p2))
|
|
|
|
# Substract the scale area:
|
|
# We need to make the substracted shape slighty bigger due to this bug:
|
|
# https://bugreports.qt.io/browse/QTBUG-57567
|
|
return path - self.get_scale_bounds(corner, margin=0.001)
|
|
|
|
def get_flip_bounds(self):
|
|
"""The interactactable shape of the flip handles.
|
|
|
|
These stretch around the edge of the item filling the areas
|
|
between the scale handles, e.g. for the bottom right corner:
|
|
|
|
│F│
|
|
──┼─┼─┐
|
|
FF│S│R│
|
|
──┼─┘ │
|
|
│R R│
|
|
└───┘
|
|
"""
|
|
|
|
outer_margin = self.select_resize_size / 2
|
|
inner_margin = self.select_resize_size / 2
|
|
return [
|
|
# top:
|
|
{
|
|
'rect': QtCore.QRectF(inner_margin,
|
|
-outer_margin,
|
|
self.width - 2 * inner_margin,
|
|
outer_margin + inner_margin),
|
|
'flip_v': True,
|
|
},
|
|
# bottom:
|
|
{
|
|
'rect': QtCore.QRectF(inner_margin,
|
|
self.height - inner_margin,
|
|
self.width - 2 * inner_margin,
|
|
outer_margin + inner_margin),
|
|
'flip_v': True,
|
|
},
|
|
# left:
|
|
{
|
|
'rect': QtCore.QRectF(-outer_margin,
|
|
inner_margin,
|
|
outer_margin + inner_margin,
|
|
self.height - 2 * inner_margin),
|
|
'flip_v': False,
|
|
},
|
|
# right:
|
|
{
|
|
'rect': QtCore.QRectF(self.width - inner_margin,
|
|
inner_margin,
|
|
outer_margin + inner_margin,
|
|
self.height - 2 * inner_margin),
|
|
'flip_v': False,
|
|
}
|
|
]
|
|
|
|
def boundingRect(self):
|
|
if not self.has_selection_outline():
|
|
return super().boundingRect()
|
|
|
|
# Add extra space for the interactive areas
|
|
margin = self.select_resize_size / 2 + self.select_rotate_size
|
|
return QtCore.QRectF(
|
|
-margin, -margin,
|
|
self.width + 2 * margin,
|
|
self.height + 2 * margin)
|
|
|
|
def shape(self):
|
|
path = QtGui.QPainterPath()
|
|
if self.has_selection_handles():
|
|
margin = self.select_resize_size / 2
|
|
rect = QtCore.QRectF(
|
|
-margin, -margin,
|
|
self.width + 2 * margin,
|
|
self.height + 2 * margin)
|
|
path.addRect(rect)
|
|
for corner in self.corners:
|
|
path.addPath(self.get_rotate_bounds(corner))
|
|
else:
|
|
rect = super().boundingRect()
|
|
path.addRect(rect)
|
|
return path
|
|
|
|
def hoverMoveEvent(self, event):
|
|
if not self.has_selection_handles():
|
|
return
|
|
|
|
for corner in self.corners:
|
|
# See if we need to change the cursor for interactable areas
|
|
if self.get_scale_bounds(corner).contains(event.pos()):
|
|
self.event_anchor = self.center_scene_coords
|
|
angle = self.get_rotate_angle(self.mapToScene(corner))
|
|
if abs(angle) >= 157.5 or abs(angle) <= 22.5:
|
|
self.setCursor(Qt.CursorShape.SizeVerCursor)
|
|
elif 112.5 <= angle <= 157.5 or -67.5 <= angle <= -22.5:
|
|
self.setCursor(Qt.CursorShape.SizeFDiagCursor)
|
|
elif 67.5 <= abs(angle) <= 112.5:
|
|
self.setCursor(Qt.CursorShape.SizeHorCursor)
|
|
else:
|
|
self.setCursor(Qt.CursorShape.SizeBDiagCursor)
|
|
return
|
|
elif self.get_rotate_bounds(corner).contains(event.pos()):
|
|
self.setCursor(BeeAssets().cursor_rotate)
|
|
return
|
|
for edge in self.get_flip_bounds():
|
|
if edge['rect'].contains(event.pos()):
|
|
if self.get_edge_flips_v(edge):
|
|
self.setCursor(BeeAssets().cursor_flip_v)
|
|
else:
|
|
self.setCursor(BeeAssets().cursor_flip_h)
|
|
return
|
|
|
|
self.setCursor(Qt.CursorShape.ArrowCursor)
|
|
|
|
def hoverEnterEvent(self, event):
|
|
# Always return regular cursor when there aren't any selection handles
|
|
if not self.has_selection_handles():
|
|
self.setCursor(Qt.CursorShape.ArrowCursor)
|
|
|
|
def mousePressEvent(self, event):
|
|
self.event_start = event.scenePos()
|
|
self.scene().views()[0].reset_previous_transform(toggle_item=self)
|
|
if not self.isSelected():
|
|
self.just_selected = True
|
|
if (event.button() == Qt.MouseButton.LeftButton
|
|
and self.has_selection_handles()):
|
|
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 = True
|
|
self.event_direction = self.get_direction_from_center(
|
|
event.scenePos())
|
|
self.event_anchor = self.mapToScene(
|
|
self.get_scale_anchor(corner))
|
|
for item in self.selection_action_items():
|
|
item.scale_orig_factor = item.scale()
|
|
event.accept()
|
|
return
|
|
# Check if we are in one of the corner's rotate areas
|
|
if self.get_rotate_bounds(corner).contains(event.pos()):
|
|
# Start rotate action
|
|
self.rotate_active = True
|
|
self.event_anchor = self.center_scene_coords
|
|
self.rotate_start_angle = self.get_rotate_angle(
|
|
event.scenePos())
|
|
for item in self.selection_action_items():
|
|
item.rotate_orig_degrees = item.rotation()
|
|
event.accept()
|
|
return
|
|
# Check if we are in one of the flip edges:
|
|
for edge in self.get_flip_bounds():
|
|
if edge['rect'].contains(event.pos()):
|
|
self.flip_active = True
|
|
event.accept()
|
|
self.scene().undo_stack.push(
|
|
commands.FlipItems(
|
|
self.selection_action_items(),
|
|
self.center_scene_coords,
|
|
vertical=self.get_edge_flips_v(edge)))
|
|
return
|
|
|
|
super().mousePressEvent(event)
|
|
|
|
def get_scale_factor(self, event):
|
|
"""Get the scale factor for the current mouse movement."""
|
|
imgsize = math.sqrt(self.width**2 + self.height**2)
|
|
p = event.scenePos() - self.event_start
|
|
direction = self.event_direction
|
|
delta = QtCore.QPointF.dotProduct(direction, p) / imgsize
|
|
return (self.scale_orig_factor + delta) / self.scale_orig_factor
|
|
|
|
def get_scale_anchor(self, corner):
|
|
"""Get the anchor around which the scale for this corner operates."""
|
|
return QtCore.QPointF(self.width - corner.x(),
|
|
self.height - corner.y())
|
|
|
|
def get_corner_direction(self, corner):
|
|
"""Get the direction facing away from the center, e.g. the direction
|
|
in which the scale for this corner increases."""
|
|
return QtCore.QPointF(1 if corner.x() > 0 else -1,
|
|
1 if corner.y() > 0 else -1)
|
|
|
|
def get_direction_from_center(self, pos):
|
|
"""The direction of a point in relation to the item's center."""
|
|
diff = pos - self.center_scene_coords
|
|
length = math.sqrt(QtCore.QPointF.dotProduct(diff, diff))
|
|
return diff / length
|
|
|
|
def get_rotate_angle(self, pos):
|
|
"""Get the angle of the given position towards the event anchor."""
|
|
|
|
diff = pos - self.event_anchor
|
|
return -math.degrees(math.atan2(diff.x(), diff.y()))
|
|
|
|
def get_rotate_delta(self, pos, snap=False):
|
|
"""Get the rotate delta for the current mouse movement.
|
|
|
|
If ``snap`` is True, snap to 15 degree units."""
|
|
|
|
delta = self.get_rotate_angle(pos) - self.rotate_start_angle
|
|
if snap:
|
|
target = utils.round_to(self.rotate_orig_degrees + delta, 15)
|
|
delta = target - self.rotate_orig_degrees
|
|
|
|
return delta
|
|
|
|
def get_edge_flips_v(self, edge):
|
|
"""Returns ``True`` if the given edge invokes a horizontal flip,
|
|
``False`` if it invokes a vertacal flip."""
|
|
|
|
if 45 < self.rotation() < 135 or 225 < self.rotation() < 315:
|
|
return not edge['flip_v']
|
|
else:
|
|
return edge['flip_v']
|
|
|
|
def mouseMoveEvent(self, event):
|
|
if (event.scenePos() - self.event_start).manhattanLength() > 5:
|
|
self.scene().views()[0].reset_previous_transform()
|
|
|
|
if self.scale_active:
|
|
factor = self.get_scale_factor(event)
|
|
for item in self.selection_action_items():
|
|
item.setScale(item.scale_orig_factor * factor,
|
|
item.mapFromScene(self.event_anchor))
|
|
event.accept()
|
|
return
|
|
if self.rotate_active:
|
|
snap = (event.modifiers() == Qt.KeyboardModifier.ControlModifier
|
|
or event.modifiers() == Qt.KeyboardModifier.ShiftModifier)
|
|
delta = self.get_rotate_delta(event.scenePos(), snap)
|
|
for item in self.selection_action_items():
|
|
item.setRotation(
|
|
item.rotate_orig_degrees + delta * item.flip(),
|
|
item.mapFromScene(self.event_anchor))
|
|
event.accept()
|
|
return
|
|
if self.flip_active:
|
|
# We have already flipped on MousePress, but we
|
|
# still need to accept the event here as to not
|
|
# initiate an item move
|
|
event.accept()
|
|
return
|
|
|
|
super().mouseMoveEvent(event)
|
|
|
|
def mouseReleaseEvent(self, event):
|
|
just_selected = self.just_selected
|
|
self.just_selected = False
|
|
if self.scale_active:
|
|
if self.get_scale_factor(event) != 1:
|
|
self.scene().undo_stack.push(
|
|
commands.ScaleItemsBy(
|
|
self.selection_action_items(),
|
|
self.get_scale_factor(event),
|
|
self.event_anchor,
|
|
ignore_first_redo=True))
|
|
event.accept()
|
|
self.reset_actions()
|
|
return
|
|
elif self.rotate_active:
|
|
self.scene().on_selection_change()
|
|
if self.get_rotate_delta(event.scenePos()) != 0:
|
|
self.scene().undo_stack.push(
|
|
commands.RotateItemsBy(
|
|
self.selection_action_items(),
|
|
self.get_rotate_delta(event.scenePos()),
|
|
self.event_anchor,
|
|
ignore_first_redo=True))
|
|
event.accept()
|
|
self.reset_actions()
|
|
return
|
|
elif self.flip_active and not just_selected:
|
|
for edge in self.get_flip_bounds():
|
|
if edge['rect'].contains(event.pos()):
|
|
# We have already flipped on MousePress, but we
|
|
# still need to accept the event here as to not
|
|
# initiate an item move
|
|
event.accept()
|
|
self.reset_actions()
|
|
return
|
|
self.reset_actions()
|
|
super().mouseReleaseEvent(event)
|
|
|
|
def on_view_scale_change(self):
|
|
self.prepareGeometryChange()
|
|
|
|
def itemChange(self, change, value):
|
|
if change == QGraphicsItem.GraphicsItemChange.ItemSelectedChange:
|
|
self.prepareGeometryChange()
|
|
if hasattr(self, 'on_selected_change'):
|
|
self.on_selected_change(value)
|
|
return super().itemChange(change, value)
|
|
|
|
|
|
class MultiSelectItem(SelectableMixin,
|
|
QtWidgets.QGraphicsRectItem):
|
|
"""The multi selection outline around all selected items."""
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
logger.debug(f'Initialized {self}')
|
|
self.init_selectable()
|
|
|
|
def __str__(self):
|
|
return (f'MultiSelectItem {self.width} x {self.height}')
|
|
|
|
@property
|
|
def width(self):
|
|
return self.rect().width()
|
|
|
|
@property
|
|
def height(self):
|
|
return self.rect().height()
|
|
|
|
def paint(self, painter, option, widget):
|
|
self.paint_selectable(painter, option, widget)
|
|
|
|
def has_selection_outline(self):
|
|
return True
|
|
|
|
def has_selection_handles(self):
|
|
return True
|
|
|
|
def selection_action_items(self):
|
|
"""The items affected by selection actions like scaling and rotating.
|
|
"""
|
|
return list(self.scene().selectedItems())
|
|
|
|
def fit_selection_area(self, rect):
|
|
"""Updates itself to fit the given selection area."""
|
|
|
|
logger.debug(f'Fit selection area to {rect}')
|
|
|
|
# Only update when values have changed, otherwise we end up in an
|
|
# infinite event loop sceneChange -> itemChange -> sceneChange ...
|
|
if self.width != rect.width() or self.height != rect.height():
|
|
self.setRect(0, 0, rect.width(), rect.height())
|
|
if self.pos() != rect.topLeft():
|
|
self.setPos(rect.topLeft())
|
|
if self.scale() != 1:
|
|
self.setScale(1)
|
|
if self.rotation() != 0:
|
|
self.setRotation(0)
|
|
if not self.isSelected():
|
|
self.setSelected(True)
|
|
if self.flip() == -1:
|
|
self.setTransform(QtGui.QTransform.fromScale(1, 1))
|
|
|
|
def mousePressEvent(self, event):
|
|
if (event.button() == Qt.MouseButton.LeftButton
|
|
and event.modifiers() == Qt.KeyboardModifier.ControlModifier):
|
|
# We still need to be able to select additional images
|
|
# within/"under" the multi select rectangle, so let ctrl+click
|
|
# events pass through
|
|
event.ignore()
|
|
return
|
|
|
|
super().mousePressEvent(event)
|
|
|
|
|
|
class RubberbandItem(BaseItemMixin, QtWidgets.QGraphicsRectItem):
|
|
"""The outline for the rubber band selection."""
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
color = QtGui.QColor(SELECT_COLOR)
|
|
color.setAlpha(40)
|
|
self.setBrush(QtGui.QBrush(color))
|
|
pen = QtGui.QPen(QtGui.QColor(0, 0, 0))
|
|
pen.setWidth(1)
|
|
pen.setCosmetic(True)
|
|
self.setPen(pen)
|
|
|
|
def __str__(self):
|
|
return (f'RubberbandItem {self.width} x {self.height}')
|
|
|
|
@property
|
|
def width(self):
|
|
return self.rect().width()
|
|
|
|
@property
|
|
def height(self):
|
|
return self.rect().height()
|
|
|
|
def fit(self, point1, point2):
|
|
"""Updates itself to fit the two given points."""
|
|
|
|
self.setRect(utils.get_rect_from_points(point1, point2))
|
|
logger.debug(f'Updated rubberband {self}')
|