Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions anylabeling/views/labeling/widgets/canvas.py
Original file line number Diff line number Diff line change
Expand Up @@ -632,14 +632,12 @@ def bounded_move_shapes(self, shapes, pos):
return False # No need to move
o1 = pos + self.offsets[0]
if self.out_off_pixmap(o1):
pos -= QtCore.QPointF(
float(min(0, int(o1.x()))), float(min(0, int(o1.y())))
)
pos -= QtCore.QPointF(min(0.0, o1.x()), min(0.0, o1.y()))
o2 = pos + self.offsets[1]
if self.out_off_pixmap(o2):
pos += QtCore.QPointF(
float(min(0, int(self.pixmap.width() - o2.x()))),
float(min(0, int(self.pixmap.height() - o2.y()))),
min(0.0, (self.pixmap.width() - 1) - o2.x()),
min(0.0, (self.pixmap.height() - 1) - o2.y()),
)
# XXX: The next line tracks the new position of the cursor
# relative to the shape, but also results in making it
Expand Down
83 changes: 83 additions & 0 deletions tests/test_canvas_bounded_move.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"""Regression tests for Canvas.bounded_move_shapes edge-clamping.

Covers two bugs in the same lines, found while fixing #237/#238/#240/#241:
1. QPoint/QPointF TypeError when a selected shape is dragged past the
pixmap border (fixed in #241).
2. int() truncation silently dropping sub-pixel corrections, and a
related off-by-one against out_off_pixmap's (w-1, h-1) bound, both
of which could leave a shape slightly outside the pixmap after the
"correction" ran.

Run:
QT_QPA_PLATFORM=offscreen python -m unittest tests.test_canvas_bounded_move -v
"""
import os
import unittest

os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")

from PyQt6 import QtCore, QtGui
from PyQt6.QtWidgets import QApplication

_APP = QApplication.instance() or QApplication([])

from anylabeling.views.labeling.shape import Shape
from anylabeling.views.labeling.widgets.canvas import Canvas


class TestBoundedMoveShapes(unittest.TestCase):
def _make_canvas(self, w=100, h=100):
canvas = Canvas(parent=None)
canvas.pixmap = QtGui.QPixmap(w, h)
return canvas

def test_does_not_raise_typeerror_at_left_edge(self):
canvas = self._make_canvas()
shape = Shape(shape_type="polygon")
shape.add_point(QtCore.QPointF(5, 5))
shape.add_point(QtCore.QPointF(15, 5))
shape.add_point(QtCore.QPointF(5, 15))
canvas.offsets = (QtCore.QPointF(-5, -5), QtCore.QPointF(5, 5))
canvas.prev_point = QtCore.QPointF(3, 5)
canvas.bounded_move_shapes([shape], QtCore.QPointF(3, 5)) # must not raise

def test_sub_pixel_overflow_at_left_top_is_corrected(self):
canvas = self._make_canvas()
shape = Shape(shape_type="polygon")
shape.add_point(QtCore.QPointF(5.3, 5.3))
canvas.offsets = (QtCore.QPointF(-5.6, -5.6), QtCore.QPointF(5.6, 5.6))
canvas.prev_point = QtCore.QPointF(5.3, 5.3)
pos = QtCore.QPointF(5.3, 5.3)
self.assertTrue(canvas.out_off_pixmap(pos + canvas.offsets[0]))

canvas.bounded_move_shapes([shape], pos)

o1_after = canvas.prev_point + canvas.offsets[0]
self.assertFalse(
canvas.out_off_pixmap(o1_after),
"sub-pixel overflow at the left/top edge must be corrected",
)

def test_sub_pixel_overflow_at_right_bottom_is_corrected(self):
canvas = self._make_canvas()
shape = Shape(shape_type="polygon")
shape.add_point(QtCore.QPointF(94.0, 94.0))
canvas.offsets = (QtCore.QPointF(-2.0, -2.0), QtCore.QPointF(5.3, 5.3))
canvas.prev_point = QtCore.QPointF(94.0, 94.0)
pos = QtCore.QPointF(94.0, 94.0)
self.assertTrue(canvas.out_off_pixmap(pos + canvas.offsets[1]))

canvas.bounded_move_shapes([shape], pos)

o2_after = canvas.prev_point + canvas.offsets[1]
self.assertFalse(
canvas.out_off_pixmap(o2_after),
"sub-pixel overflow at the right/bottom edge must be corrected",
)
# Corrected position should land exactly on the boundary out_off_pixmap
# itself uses (w - 1), not one past it.
self.assertAlmostEqual(o2_after.x(), canvas.pixmap.width() - 1)


if __name__ == "__main__":
unittest.main()
Loading