Files
OmaPhoto/tests/DistortCanvasTests.cpp
T
雷电芽衣andClaude c864304695
Release / deb (push) Canceled after 0s
Release / rpm (push) Canceled after 0s
Release / arch (push) Canceled after 0s
Release / publish (push) Canceled after 0s
OmaPhoto 1.0.0: the Linux port of Compositor
A layered image editor for Linux and Omarchy, ported from Compositor
(github.com/robbietilton/Compositor), whose Swift source is its
specification. It reads and writes Compositor's .comp projects.

Packages for Ubuntu, Fedora and Arch come from the release workflow;
scripts/install/ holds one install script per distribution family,
and flake.nix serves NixOS.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-26 00:44:52 -04:00

236 lines
11 KiB
C++

#include "CanvasFixtures.h"
#include "Rendering/TransformOverlay.h"
#include "UI/TransformInspector.h"
// A distortion on the canvas: box, drags, cursor, preview.
namespace {
QImage image(const QCursor &cursor)
{
return cursor.pixmap().toImage();
}
void hover(QWidget &widget, QPointF at, Qt::KeyboardModifiers modifiers = Qt::NoModifier)
{
QMouseEvent event(QEvent::MouseMove, at, at, widget.mapToGlobal(at.toPoint()), Qt::NoButton, Qt::NoButton, modifiers);
QApplication::sendEvent(&widget, &event);
}
// The document fills the canvas: view points are pixels.
struct Canvas : Shown {
QUuid red;
Canvas() : Shown(QSize(400, 300), QSize(400, 300))
{
settle();
session.zoom(1);
session.insert(filled(100, 100, qRgba(255, 0, 0, 255), "Red"));
red = session.activeLayerID().value();
session.selectTool(NavigationTool::move);
canvas->synchronizeDisplay();
}
void press(QPointF at, Qt::KeyboardModifiers modifiers = Qt::NoModifier) { QTest::mousePress(canvas, Qt::LeftButton, modifiers, at.toPoint()); }
void move(QPointF to, Qt::KeyboardModifiers modifiers = Qt::NoModifier)
{
QMouseEvent event(QEvent::MouseMove, to, to, canvas->mapToGlobal(to.toPoint()), Qt::NoButton, Qt::LeftButton, modifiers);
QApplication::sendEvent(canvas, &event);
}
void release(QPointF at, Qt::KeyboardModifiers modifiers = Qt::NoModifier) { QTest::mouseRelease(canvas, Qt::LeftButton, modifiers, at.toPoint()); }
bool shows(const QCursor &cursor) const { return image(canvas->cursor()) == image(cursor); }
};
}
class DistortCanvasTests : public QObject {
Q_OBJECT
private slots:
void aDistortionsGeometryHasNoGripAndHitsItsCornersAndEdges();
void ctrlDraggingAHandleDistortsAndKeepsDistorting();
void theDistortCursorShowsWithCtrlOrOnceDistorted();
void thePreviewIsDrawnAndTheNumbersRest();
void aDistortedLayerShowsThroughItsMask();
};
void DistortCanvasTests::aDistortionsGeometryHasNoGripAndHitsItsCornersAndEdges()
{
CanvasViewport viewport;
const QSizeF size(400, 300);
viewport.resize(QSizeF(400, 300), 1, size);
viewport.setZoom(1, QPointF(200, 150), size);
QCOMPARE(viewport.viewPoint(QPointF(0, 0), size), QPointF(0, 0));
const Corners corners = {QPointF(100, 100), QPointF(200, 100), QPointF(180, 160), QPointF(100, 160)};
const TransformOverlayGeometry geometry(corners, viewport, size);
QVERIFY(!geometry.showsRotation);
QCOMPARE(geometry.handles[0], QPointF(100, 100));
QCOMPARE(geometry.handles[1], QPointF(150, 100));
QCOMPARE(geometry.handles[3], QPointF(190, 130));
QCOMPARE(geometry.handles[6], QPointF(100, 160));
QCOMPARE(geometry.handles[7], QPointF(100, 130));
QCOMPARE(geometry.rotationHandle, geometry.handles[1]);
// The top's middle is a handle, not the grip.
const TransformDrag::Mode top = geometry.hit(QPointF(150, 100)).value();
QVERIFY(top.kind == TransformDrag::Kind::resize);
QCOMPARE(top.index, 1);
QCOMPARE(geometry.hit(QPointF(185, 145)).value().index, 3);
QVERIFY(!geometry.hit(QPointF(150, 130)).has_value());
// A box's geometry keeps its grip; the two differ.
const TransformOverlayGeometry box({.origin = {100, 100}, .size = {100, 60}}, viewport, size);
QVERIFY(box.showsRotation);
QVERIFY(!(box == geometry));
}
void DistortCanvasTests::ctrlDraggingAHandleDistortsAndKeepsDistorting()
{
Canvas shown;
EditorSession &session = shown.session;
const double ratio = shown.canvas->devicePixelRatio();
// Ctrl on the bottom right corner: that corner alone follows.
shown.press(QPointF(250, 200), Qt::ControlModifier);
QVERIFY(shown.shows(CanvasView::distortCursor(ratio)));
shown.move(QPointF(260, 210), Qt::ControlModifier);
shown.move(QPointF(270, 230), Qt::ControlModifier);
const TransformEdit edit = session.transformEdit().value();
QVERIFY(edit.persistent);
QCOMPARE(edit.corners.value(), (Corners{QPointF(150, 100), QPointF(250, 100), QPointF(270, 230), QPointF(150, 200)}));
QCOMPARE(edit.draft, layerWith(session, shown.red).transform);
shown.release(QPointF(270, 230), Qt::ControlModifier);
// The edit waits for Apply; its box has no grip.
QVERIFY(session.transformEdit().has_value());
const TransformOverlay overlay(session);
QVERIFY(!overlay.geometry().value().showsRotation);
QCOMPARE(overlay.geometry().value().handles[4], QPointF(270, 230));
// Once distorted, plain drags distort too; Shift keeps an axis.
shown.press(QPointF(150, 100));
QVERIFY(shown.shows(CanvasView::distortCursor(ratio)));
shown.move(QPointF(140, 105), Qt::ShiftModifier);
shown.release(QPointF(140, 105), Qt::ShiftModifier);
QCOMPARE(session.transformEdit().value().corners.value()[0], QPointF(140, 100));
// An edge handle moves both its corners.
shown.press(QPointF(198, 100));
shown.move(QPointF(198, 90));
shown.release(QPointF(198, 90));
QCOMPARE(session.transformEdit().value().corners.value()[0], QPointF(140, 90));
QCOMPARE(session.transformEdit().value().corners.value()[1], QPointF(250, 90));
// Return applies: the pixels are resampled into the shape's bounds.
QTRY_VERIFY(shown.canvas->hasFocus());
QTest::keyClick(shown.canvas, Qt::Key_Return);
QVERIFY(!session.transformEdit().has_value());
QCOMPARE(session.history.undoName(), QString("Distort"));
QCOMPARE(layerWith(session, shown.red).transform, (LayerTransform{.origin = {140, 90}, .size = {130, 140}}));
QCOMPARE(layerWith(session, shown.red).asset.value().size(), QSize(130, 140));
session.undo();
QCOMPARE(layerWith(session, shown.red).transform.size, QSizeF(100, 100));
}
void DistortCanvasTests::theDistortCursorShowsWithCtrlOrOnceDistorted()
{
Canvas shown;
CanvasView &canvas = *shown.canvas;
const double ratio = canvas.devicePixelRatio();
hover(canvas, QPointF(150, 100));
QCOMPARE(canvas.cursor().shape(), Qt::SizeFDiagCursor);
hover(canvas, QPointF(150, 100), Qt::ControlModifier);
QVERIFY(shown.shows(CanvasView::distortCursor(ratio)));
// Ctrl off a handle picks: the move pointer stays.
hover(canvas, QPointF(200, 150), Qt::ControlModifier);
QVERIFY(shown.shows(CanvasView::moveCursor(ratio)));
hover(canvas, QPointF(150, 100));
QCOMPARE(canvas.cursor().shape(), Qt::SizeFDiagCursor);
shown.session.beginTransform();
shown.session.beginDistort();
canvas.synchronizeDisplay();
QVERIFY(shown.shows(CanvasView::distortCursor(ratio)));
// The white arrow: light where the duplicate pointer is dark.
QVERIFY(image(CanvasView::distortCursor(1)).pixelColor(8, 12).lightness() > 128);
QVERIFY(image(CanvasView::duplicateCursor(1)).pixelColor(8, 12).lightness() < 128);
shown.session.cancelTransform();
canvas.synchronizeDisplay();
QCOMPARE(canvas.cursor().shape(), Qt::SizeFDiagCursor);
}
void DistortCanvasTests::thePreviewIsDrawnAndTheNumbersRest()
{
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(50, 50));
QCOMPARE(session.viewport.viewPoint(QPointF(0, 0), shown.documentSize()), QPointF(150, 100));
QVERIFY(canvas.synchronizeDisplay());
TransformInspector inspector(session);
QWidget &numbers = *inspector.findChild<QWidget *>("transformFields");
QVERIFY(numbers.isEnabled());
session.beginTransform();
session.beginDistort();
QVERIFY(!numbers.isEnabled());
// The top right corner pulled out: the preview follows.
session.previewCorners({QPointF(40, 40), QPointF(80, 40), QPointF(60, 60), QPointF(40, 60)});
QVERIFY(canvas.synchronizeDisplay());
QImage shot = canvas.grab().toImage();
QCOMPARE(shot.pixelColor(215, 145), QColor(Qt::red));
QVERIFY(shot.pixelColor(225, 155) != QColor(Qt::red));
QCOMPARE(shot.pixelColor(197, 147), QColor(Qt::red));
// The same corners again change nothing.
QVERIFY(!canvas.synchronizeDisplay());
session.cancelTransform();
QVERIFY(canvas.synchronizeDisplay());
QVERIFY(numbers.isEnabled());
shot = canvas.grab().toImage();
QVERIFY(shot.pixelColor(215, 145) != QColor(Qt::red));
// An unlinked mask distorted alone shows in the layer's grid.
const QUuid red = session.activeLayerID().value();
QImage block(20, 20, QImage::Format_Grayscale8);
block.fill(0);
for (int y = 2; y < 18; ++y)
std::fill_n(block.scanLine(y) + 2, 8, uchar(255));
rewrite(session, [&](ProjectSnapshot &snapshot) {
setMask(snapshot, red, LayerMask::assetFrom(block));
record(snapshot, red).maskLinked = false;
});
// Installing the snapshot fits the view: back to 100%.
session.zoom(1);
QCOMPARE(session.viewport.viewPoint(QPointF(0, 0), shown.documentSize()), QPointF(150, 100));
session.selectTool(NavigationTool::move);
session.selectLayerTarget(red, true);
session.beginTransform();
session.beginDistort();
// The bottom left corner pulled out: the block leans left.
session.previewCorners({QPointF(40, 40), QPointF(60, 40), QPointF(60, 60), QPointF(30, 60)});
QVERIFY(session.transformEdit().value().mask);
const std::optional<QImage> preview = session.maskDistortPreview(layerWith(session, red));
QCOMPARE(int(preview.value().constScanLine(5)[5]), 255);
QCOMPARE(int(preview.value().constScanLine(16)[16]), 0);
canvas.synchronizeDisplay();
shot = canvas.grab().toImage();
QCOMPARE(shot.pixelColor(195, 145), QColor(Qt::red));
QVERIFY(shot.pixelColor(199, 154) != QColor(Qt::red));
}
void DistortCanvasTests::aDistortedLayerShowsThroughItsMask()
{
Shown shown;
CanvasView &canvas = *shown.canvas;
EditorSession &session = shown.session;
shown.settle();
session.insert(filled(20, 20, qRgba(255, 0, 0, 255), "Red"), QPointF(50, 50));
const QUuid red = session.activeLayerID().value();
// A linked mask hides the left half.
QImage half(20, 20, QImage::Format_Grayscale8);
half.fill(255);
for (int y = 0; y < 20; ++y)
std::fill_n(half.scanLine(y), 10, uchar(0));
rewrite(session, [&](ProjectSnapshot &snapshot) { setMask(snapshot, red, LayerMask::assetFrom(half)); });
session.zoom(1);
session.selectTool(NavigationTool::move);
session.selectLayer(red);
session.beginTransform();
session.beginDistort();
session.previewCorners({QPointF(40, 40), QPointF(62, 40), QPointF(60, 60), QPointF(40, 60)});
canvas.synchronizeDisplay();
// The document sits at (150, 100); probes miss the handles.
const QImage shot = canvas.grab().toImage();
QVERIFY(shot.pixelColor(196, 154) != QColor(Qt::red));
QCOMPARE(shot.pixelColor(205, 146), QColor(Qt::red));
}
QTEST_MAIN(DistortCanvasTests)
#include "DistortCanvasTests.moc"