Files
OmaPhoto/tests/TransformOverlayTests.cpp
T

223 lines
10 KiB
C++

#include "CanvasFixtures.h"
#include "Rendering/TransformOverlay.h"
// The Move tool's box: its handles, hits, cursors and drawing.
namespace {
bool near(QPointF a, QPointF b)
{
return std::hypot(a.x() - b.x(), a.y() - b.y()) < 0.0001;
}
}
class TransformOverlayTests : public QObject {
Q_OBJECT
private slots:
void hoverRegionsMatchRotatedEdgesCornersAndRotationHandle();
void documentMappingAndRotatedHitTesting();
void resizeCursorsTurnWithTheBox();
void theBoxShowsForAMovableLayerAndHidesWithTheControls();
void handlesAndGuidesArePaintedWhereTheyBelong();
void anotherLayersHandlesRepaintOnlyTheirRects();
};
namespace {
// Every paint's region, to see how much a change repaints.
struct PaintSpy : QObject {
std::vector<QRect> rects;
bool eventFilter(QObject *, QEvent *event) override
{
if (event->type() == QEvent::Paint)
rects.push_back(static_cast<QPaintEvent *>(event)->region().boundingRect());
return false;
}
};
}
void TransformOverlayTests::hoverRegionsMatchRotatedEdgesCornersAndRotationHandle()
{
CanvasViewport viewport;
const QSizeF size(1000, 800);
viewport.resize(QSizeF(1000, 800), 1, size);
const LayerTransform transform{.origin = {100, 100}, .size = {400, 300}, .rotation = 37};
const TransformOverlayGeometry geometry(transform, viewport, size);
for (const auto &[start, end, expected] : {std::tuple(0, 2, 1), std::tuple(2, 4, 3), std::tuple(4, 6, 5), std::tuple(6, 0, 7)}) {
const QPointF a = geometry.handles[size_t(start)], b = geometry.handles[size_t(end)];
const QPointF point(a.x() * 0.75 + b.x() * 0.25, a.y() * 0.75 + b.y() * 0.25);
const TransformDrag::Mode hit = geometry.hit(point).value();
QVERIFY(hit.kind == TransformDrag::Kind::resize);
QCOMPARE(hit.index, expected);
}
for (const int index : {0, 2, 4, 6}) {
const TransformDrag::Mode hit = geometry.hit(geometry.handles[size_t(index)]).value();
QVERIFY(hit.kind == TransformDrag::Kind::resize);
QCOMPARE(hit.index, index);
}
QVERIFY(geometry.hit(geometry.rotationHandle).value().kind == TransformDrag::Kind::rotate);
QVERIFY(!geometry.hit(viewport.viewPoint(transform.center(), size)).has_value());
// The grip stands 28 points off the top edge, turned.
const double radians = transform.radians();
QVERIFY(near(geometry.rotationHandle, geometry.handles[1] + QPointF(std::sin(radians) * 28, -std::cos(radians) * 28)));
}
void TransformOverlayTests::documentMappingAndRotatedHitTesting()
{
CanvasViewport viewport;
const QSizeF size(1000, 800);
viewport.resize(QSizeF(900, 600), 2, size);
viewport.setZoom(1.5, QPointF(300, 200), size);
viewport.translate(QSizeF(57, -30));
const LayerTransform transform{.origin = {100, 200}, .size = {100, 50}, .rotation = 90};
const TransformOverlayGeometry geometry(transform, viewport, size);
QVERIFY(near(viewport.documentPoint(geometry.handles[4], size), transform.point(QPointF(1, 1))));
QVERIFY(geometry.hit(geometry.rotationHandle).value().kind == TransformDrag::Kind::rotate);
for (size_t index = 0; index < geometry.handles.size(); ++index) {
const TransformDrag::Mode hit = geometry.hit(geometry.handles[index]).value();
QVERIFY(hit.kind == TransformDrag::Kind::resize);
QCOMPARE(hit.index, int(index));
}
QVERIFY(!geometry.hit(viewport.viewPoint(transform.center(), size)).has_value());
}
void TransformOverlayTests::resizeCursorsTurnWithTheBox()
{
CanvasViewport viewport;
const QSizeF size(400, 300);
viewport.resize(QSizeF(400, 300), 1, size);
const TransformOverlayGeometry upright({.origin = {100, 100}, .size = {100, 50}}, viewport, size);
QCOMPARE(upright.resizeCursor(3), Qt::SizeHorCursor);
QCOMPARE(upright.resizeCursor(7), Qt::SizeHorCursor);
QCOMPARE(upright.resizeCursor(1), Qt::SizeVerCursor);
QCOMPARE(upright.resizeCursor(5), Qt::SizeVerCursor);
QCOMPARE(upright.resizeCursor(0), Qt::SizeFDiagCursor);
QCOMPARE(upright.resizeCursor(4), Qt::SizeFDiagCursor);
QCOMPARE(upright.resizeCursor(2), Qt::SizeBDiagCursor);
QCOMPARE(upright.resizeCursor(6), Qt::SizeBDiagCursor);
// Ten points reach a handle; more do not.
QCOMPARE(upright.hit(upright.handles[0] + QPointF(-9.5, 0)).value().index, 0);
QVERIFY(!upright.hit(upright.handles[0] + QPointF(-10.5, 0)).has_value());
QVERIFY(!upright.hit(upright.handles[0] + QPointF(-10.5, 0.5)).has_value());
// A quarter turn swaps the edges' cursors.
const TransformOverlayGeometry turned({.origin = {100, 100}, .size = {100, 50}, .rotation = 90}, viewport, size);
QCOMPARE(turned.resizeCursor(3), Qt::SizeVerCursor);
QCOMPARE(turned.resizeCursor(1), Qt::SizeHorCursor);
QCOMPARE(turned.resizeCursor(0), Qt::SizeBDiagCursor);
}
void TransformOverlayTests::theBoxShowsForAMovableLayerAndHidesWithTheControls()
{
Shown shown;
EditorSession &session = shown.session;
const TransformOverlay overlay(session);
QVERIFY(!overlay.geometry().has_value());
session.addBlankLayer();
QVERIFY(!session.canTransform());
QVERIFY(!overlay.geometry().has_value());
session.insert(filled(20, 20, qRgba(255, 0, 0, 255), "Red"));
const QUuid red = session.activeLayerID().value();
QVERIFY(session.canTransform());
QVERIFY(overlay.geometry().has_value());
QCOMPARE(overlay.geometry().value(), TransformOverlayGeometry(layerWith(session, red).transform, session.viewport, shown.documentSize()));
session.selectTool(NavigationTool::hand);
QVERIFY(!overlay.geometry().has_value());
session.selectTool(NavigationTool::move);
// Hidden controls: only a Ctrl+T transform shows its box.
session.setShowsTransformControls(false);
QVERIFY(!overlay.geometry().has_value());
session.beginTransform(false);
QVERIFY(!overlay.geometry().has_value());
session.cancelTransform();
session.beginTransform(true);
QVERIFY(overlay.geometry().has_value());
session.cancelTransform();
session.setShowsTransformControls(true);
session.toggleLayerVisibility(red);
QVERIFY(!overlay.geometry().has_value());
session.toggleLayerVisibility(red);
// A selection or a folder: the box around them all.
session.insert(filled(10, 10, qRgba(0, 0, 255, 255), "Blue"), QPointF(85, 85));
const QUuid blue = session.activeLayerID().value();
session.selectLayers({red, blue}, red);
QCOMPARE(overlay.geometry().value(), TransformOverlayGeometry(session.groupTransformBox().value(), session.viewport, shown.documentSize()));
session.beginTransform();
LayerTransform draft = session.transformEdit().value().draft;
draft.origin += QPointF(3, 0);
session.previewTransform(draft);
QCOMPARE(overlay.geometry().value(), TransformOverlayGeometry(draft, session.viewport, shown.documentSize()));
session.cancelTransform();
session.addGroup();
const QUuid folder = session.activeLayerID().value();
QVERIFY(session.placeLayer(blue, folder));
session.selectLayer(folder);
QCOMPARE(overlay.geometry().value(), TransformOverlayGeometry(session.groupTransformBox().value(), session.viewport, shown.documentSize()));
}
void TransformOverlayTests::handlesAndGuidesArePaintedWhereTheyBelong()
{
Shown shown;
CanvasView &canvas = *shown.canvas;
EditorSession &session = shown.session;
shown.settle();
session.zoom(1);
session.insert(filled(20, 20, qRgba(255, 0, 0, 255), "Red"));
QVERIFY(canvas.synchronizeDisplay());
QCOMPARE(session.viewport.viewPoint(QPointF(0, 0), shown.documentSize()), QPointF(150, 100));
const TransformOverlay overlay(session);
QCOMPARE(overlay.drawnRect(canvas.rect()), QRect(185, 107, 30, 58));
// White squares on the handles, a disc on the grip.
QImage shot = canvas.grab().toImage();
QCOMPARE(shot.pixelColor(190, 140), QColor(Qt::white));
QCOMPARE(shot.pixelColor(210, 160), QColor(Qt::white));
QCOMPARE(shot.pixelColor(200, 112), QColor(Qt::white));
// The accent alone runs between handles, half over red.
const QColor accent = canvas.palette().color(QPalette::Highlight);
const QColor edge = shot.pixelColor(190, 145);
QVERIFY2(std::abs(edge.red() - (accent.red() + 255) / 2) <= 2 && std::abs(edge.green() - accent.green() / 2) <= 2
&& std::abs(edge.blue() - accent.blue() / 2) <= 2,
qPrintable(edge.name()));
session.setShowsTransformControls(false);
QVERIFY(overlay.drawnRect(canvas.rect()).isEmpty());
shot = canvas.grab().toImage();
QVERIFY(shot.pixelColor(190, 140) != QColor(Qt::white));
QVERIFY(shot.pixelColor(200, 112) != QColor(Qt::white));
QCOMPARE(shot.pixelColor(190, 145), QColor(Qt::red));
// A guide: an accent line across the document.
session.snapGuides = {{50}, {}};
QVERIFY(!canvas.synchronizeDisplay());
QCOMPARE(overlay.drawnRect(canvas.rect()), canvas.rect());
shot = canvas.grab().toImage();
QCOMPARE(shot.pixelColor(200, 190), canvas.palette().color(QPalette::Highlight));
QCOMPARE(shot.pixelColor(200, 101), canvas.palette().color(QPalette::Highlight));
QVERIFY(shot.pixelColor(230, 190) != canvas.palette().color(QPalette::Highlight));
session.snapGuides = {{}, {30}};
canvas.synchronizeDisplay();
shot = canvas.grab().toImage();
QCOMPARE(shot.pixelColor(170, 130), canvas.palette().color(QPalette::Highlight));
QVERIFY(shot.pixelColor(200, 190) != canvas.palette().color(QPalette::Highlight));
}
void TransformOverlayTests::anotherLayersHandlesRepaintOnlyTheirRects()
{
Shown shown;
CanvasView &canvas = *shown.canvas;
EditorSession &session = shown.session;
shown.settle();
session.zoom(1);
session.insert(filled(20, 20, qRgba(255, 0, 0, 255), "Red"), QPointF(20, 20));
const QUuid red = session.activeLayerID().value();
session.insert(filled(10, 10, qRgba(0, 0, 255, 255), "Blue"), QPointF(80, 80));
canvas.synchronizeDisplay();
QCoreApplication::processEvents();
PaintSpy spy;
canvas.installEventFilter(&spy);
// The old box and the new one, nothing more.
session.selectLayer(red);
QVERIFY(!canvas.synchronizeDisplay());
QCoreApplication::processEvents();
QCOMPARE(int(spy.rects.size()), 1);
QCOMPARE(spy.rects[0], QRect(155, 77, 85, 113));
canvas.removeEventFilter(&spy);
}
QTEST_MAIN(TransformOverlayTests)
#include "TransformOverlayTests.moc"