189 lines
8.3 KiB
C++
189 lines
8.3 KiB
C++
#include "SelectionFixtures.h"
|
|
|
|
// Feather softens the selection itself, so everything clipped fades.
|
|
namespace {
|
|
// Values strictly between clear and full along one row.
|
|
int fading(const QImage &image, int row, int channel, int stride)
|
|
{
|
|
int count = 0;
|
|
for (int x = 0; x < image.width(); ++x) {
|
|
const int value = image.constScanLine(row)[x * stride + channel];
|
|
count += value > 8 && value < 247;
|
|
}
|
|
return count;
|
|
}
|
|
}
|
|
|
|
class SelectionFeatherTests : public QObject {
|
|
Q_OBJECT
|
|
private slots:
|
|
void featherSoftensTheSelectionAndWhatItClips();
|
|
void feathersAddAsBlursAndStopAtTheirLimit();
|
|
void featherTravelsWithTheOutline();
|
|
void theClipGrowsByTheFalloff();
|
|
void theAmountSheetAsksThenApplies();
|
|
void aMaskFromAFeatheredSelectionFades();
|
|
};
|
|
|
|
void SelectionFeatherTests::featherSoftensTheSelectionAndWhatItClips()
|
|
{
|
|
EditorSession session;
|
|
session.createDocument(60, 20, true);
|
|
session.addBlankLayer();
|
|
session.selectAll();
|
|
session.setSelectionFeatherAmount(6);
|
|
session.featherSelection(6);
|
|
QVERIFY(session.canModifySelection());
|
|
QCOMPARE(session.selection().value().feather, 6.0);
|
|
QCOMPARE(session.history.undoName(), QString("Feather Selection"));
|
|
session.setSelection(DocumentSelection{rectPath(QRectF(20, 0, 20, 20)), true, 6}, "probe");
|
|
const SelectionClip clip = session.selection().value().clip(QSizeF(60, 20));
|
|
const QImage coverage = clip.coverage.value();
|
|
QVERIFY2(fading(coverage, coverage.height() / 2, 0, 1) >= 4, "the clip has no soft edge");
|
|
// A copy takes the fade too, within the canvas.
|
|
QCOMPARE(session.selectionCopyRegion().value(), QRectF(8, 0, 44, 20));
|
|
session.setForegroundColor(PaletteColor{1, 0, 0});
|
|
bool done = false;
|
|
session.fillSelection(EditorSession::FillSource::foreground, [&] { done = true; });
|
|
QTRY_VERIFY(done);
|
|
const QImage filled = session.activeLayer().value().asset.value().image().convertToFormat(QImage::Format_RGBA8888_Premultiplied);
|
|
QVERIFY2(fading(filled, filled.height() / 2, 3, 4) >= 4, "the fill has a hard edge");
|
|
// Trimmed to the fade: whole inside, faint at the ends.
|
|
QCOMPARE(session.activeLayer().value().transform.origin, QPointF(12, 0));
|
|
QCOMPARE(filled.width(), 36);
|
|
QCOMPARE(int(filled.constScanLine(10)[18 * 4 + 3]), 255);
|
|
QCOMPARE(int(filled.constScanLine(10)[3]), 1);
|
|
}
|
|
|
|
void SelectionFeatherTests::feathersAddAsBlursAndStopAtTheirLimit()
|
|
{
|
|
const auto session = selectionSession();
|
|
QSignalSpy changes(session.get(), &EditorSession::changed);
|
|
session->featherSelection(4);
|
|
QVERIFY(!session->selection());
|
|
QCOMPARE(int(changes.count()), 0);
|
|
lasso(*session, square(10, 10, 40));
|
|
session->featherSelection(3);
|
|
session->featherSelection(4);
|
|
QCOMPARE(session->selection().value().feather, 5.0);
|
|
session->featherSelection(0);
|
|
session->featherSelection(-3);
|
|
QCOMPARE(session->selection().value().feather, 5.0);
|
|
session->featherSelection(250);
|
|
QCOMPARE(session->selection().value().feather, 250.0);
|
|
session->undo();
|
|
QCOMPARE(session->selection().value().feather, 5.0);
|
|
// A hard edge stays hard: clear or full, nothing between.
|
|
session->setSelectionAntialiased(false);
|
|
lasso(*session, {QPointF(10, 10), QPointF(60, 20), QPointF(20, 70)});
|
|
const QImage hard = session->selection().value().coverage(100, 100);
|
|
for (int y = 0; y < 100; ++y)
|
|
QCOMPARE(fading(hard, y, 0, 1), 0);
|
|
}
|
|
|
|
void SelectionFeatherTests::featherTravelsWithTheOutline()
|
|
{
|
|
const auto session = selectionSession();
|
|
lasso(*session, square(20, 20, 40));
|
|
session->featherSelection(4);
|
|
session->expandSelection(2);
|
|
QCOMPARE(session->selection().value().feather, 4.0);
|
|
session->contractSelection(1);
|
|
QCOMPARE(session->selection().value().feather, 4.0);
|
|
session->invertSelection();
|
|
QCOMPARE(session->selection().value().feather, 4.0);
|
|
session->invertSelection();
|
|
QVERIFY(session->beginSelectionMove());
|
|
session->moveSelection(QSizeF(5, 0));
|
|
QCOMPARE(session->selection().value().feather, 4.0);
|
|
QCOMPARE(bounds(*session).left(), 24.0);
|
|
}
|
|
|
|
void SelectionFeatherTests::theClipGrowsByTheFalloff()
|
|
{
|
|
const DocumentSelection soft{rectPath(QRectF(10, 10, 20, 20)), true, 3};
|
|
QCOMPARE(soft.coverageBounds(), QRectF(4, 4, 32, 32));
|
|
QCOMPARE(soft.clip(QSizeF(100, 100)).rect, QRectF(3, 3, 34, 34));
|
|
QCOMPARE(soft.clip(QSizeF(20, 20)).rect, QRectF(3, 3, 17, 17));
|
|
// A feathered edge is antialiased first, whatever the choice.
|
|
QPainterPath triangle;
|
|
triangle.addPolygon(QPolygonF{QPointF(5, 5), QPointF(35, 12), QPointF(12, 33)});
|
|
QCOMPARE((DocumentSelection{triangle, false, 1}.coverage(40, 40)), (DocumentSelection{triangle, true, 1}.coverage(40, 40)));
|
|
// The canvas edge repeats: a whole canvas stays whole there.
|
|
const QImage whole = DocumentSelection{rectPath(QRectF(0, 0, 40, 40)), true, 6}.coverage(40, 40);
|
|
QCOMPARE(int(whole.constScanLine(0)[0]), 255);
|
|
QCOMPARE(int(whole.constScanLine(39)[20]), 255);
|
|
const DocumentSelection hard{rectPath(QRectF(10, 10, 20, 20)), true, 0};
|
|
QCOMPARE(hard.coverageBounds(), QRectF(10, 10, 20, 20));
|
|
// Past the outline it fades, then nothing.
|
|
const QImage image = soft.coverage(40, 40);
|
|
QVERIFY(image.constScanLine(20)[8] > 0);
|
|
QCOMPARE(int(image.constScanLine(20)[2]), 0);
|
|
QCOMPARE(int(image.constScanLine(20)[20]), 255);
|
|
// Either side of the outline mirrors the other.
|
|
QCOMPARE(image.constScanLine(20)[9] + image.constScanLine(20)[10], 255);
|
|
}
|
|
|
|
void SelectionFeatherTests::theAmountSheetAsksThenApplies()
|
|
{
|
|
const auto session = selectionSession();
|
|
QSignalSpy changes(session.get(), &EditorSession::changed);
|
|
session->promptSelectionAmount(SelectionAmountOperation::feather);
|
|
QVERIFY(!session->selectionAmountOperation());
|
|
QCOMPARE(int(changes.count()), 0);
|
|
lasso(*session, square(20, 20, 40));
|
|
session->promptSelectionAmount(SelectionAmountOperation::feather);
|
|
QCOMPARE(session->selectionAmountOperation(), std::optional(SelectionAmountOperation::feather));
|
|
QVERIFY(!session->canEditLayers());
|
|
QVERIFY(!session->canUseHistory());
|
|
QVERIFY(!session->canStartProjectOperation());
|
|
session->confirmSelectionAmount(0);
|
|
session->confirmSelectionAmount(251);
|
|
QVERIFY(session->selectionAmountOperation().has_value());
|
|
QCOMPARE(session->selection().value().feather, 0.0);
|
|
session->confirmSelectionAmount(250);
|
|
QVERIFY(!session->selectionAmountOperation());
|
|
QCOMPARE(session->selectionFeatherAmount(), 250);
|
|
QCOMPARE(session->selection().value().feather, 250.0);
|
|
QVERIFY(session->canEditLayers());
|
|
// A file request waits for the question, then runs.
|
|
session->promptSelectionAmount(SelectionAmountOperation::expand);
|
|
bool ran = false;
|
|
session->waitForFileRequest([&ran] { ran = true; });
|
|
QTest::qWait(10);
|
|
QVERIFY(!ran);
|
|
session->setSelectionAmountOperation(std::nullopt);
|
|
QTRY_VERIFY(ran);
|
|
session->promptSelectionAmount(SelectionAmountOperation::expand);
|
|
session->confirmSelectionAmount(501);
|
|
QVERIFY(session->selectionAmountOperation().has_value());
|
|
session->confirmSelectionAmount(3);
|
|
QCOMPARE(session->selectionExpandAmount(), 3);
|
|
QCOMPARE(session->history.undoName(), QString("Expand Selection"));
|
|
QCOMPARE(bounds(*session), QRectF(17, 17, 46, 46));
|
|
session->promptSelectionAmount(SelectionAmountOperation::contract);
|
|
session->confirmSelectionAmount(500);
|
|
QCOMPARE(session->selectionContractAmount(), 500);
|
|
QVERIFY(session->selection().value().isEmpty());
|
|
// No question open: an amount does nothing.
|
|
session->undo();
|
|
session->confirmSelectionAmount(5);
|
|
QCOMPARE(session->selectionContractAmount(), 500);
|
|
}
|
|
|
|
void SelectionFeatherTests::aMaskFromAFeatheredSelectionFades()
|
|
{
|
|
EditorSession session;
|
|
session.createDocument(60, 20, true);
|
|
session.setSelection(DocumentSelection{rectPath(QRectF(20, 0, 20, 20)), true, 6}, "probe");
|
|
session.addMask(false);
|
|
const QImage mask = session.activeLayer().value().mask.value().asset.image();
|
|
QCOMPARE(mask, session.document().value().layers.back().mask.value().asset.image());
|
|
QVERIFY2(fading(mask, 10, 0, 1) >= 4, "the mask has a hard edge");
|
|
QCOMPARE(int(mask.constScanLine(10)[30]), 255);
|
|
QCOMPARE(int(mask.constScanLine(10)[2]), 0);
|
|
}
|
|
|
|
QTEST_GUILESS_MAIN(SelectionFeatherTests)
|
|
#include "SelectionFeatherTests.moc"
|