Quit settles a pending gradient, a pixel move and open dialogs; Dither › Scanlines (CRT) on all cores (13.17)
This commit is contained in:
1 parent
765e071c0c
commit
5bfbc6cd5d
12 files changed
+913
-32
No files matched your search
@@ -179,13 +179,13 @@ void DitherSheetTests::theControlsFollowTheStyle()
|
||||
dither.colors = DitherColors::twoColors;
|
||||
});
|
||||
QCOMPARE(shown.shown(), sorted(QStringList{"Style", "Pixel Size", "Density", "Contrast", "Colors", "Light on Dark", "Swatches"}));
|
||||
// Style's menu: ten looks in four groups.
|
||||
// Style's menu: eleven looks in four groups.
|
||||
QComboBox &style = shown.child<QComboBox>("ditherStyle");
|
||||
QStringList items;
|
||||
for (int index = 0; index < style.count(); ++index)
|
||||
items << (style.itemText(index).isEmpty() ? QStringLiteral("—") : style.itemText(index));
|
||||
QCOMPARE(items, (QStringList{"Atkinson (Classic Mac)", "Floyd–Steinberg", "—", "Bayer 2 × 2", "Bayer 4 × 4", "Bayer 8 × 8", "—", "Halftone Dots",
|
||||
"Halftone Lines", "Halftone Diamonds", "—", "Mac Patterns", "ASCII"}));
|
||||
"Halftone Lines", "Halftone Diamonds", "—", "Mac Patterns", "ASCII", "Scanlines (CRT)"}));
|
||||
QCOMPARE(style.currentText(), QString("Mac Patterns"));
|
||||
// Its quick preview never shows the busy line.
|
||||
QVERIFY(!shown.child<QProgressBar>("filterSpinner").isVisibleTo(shown.sheet.get()));
|
||||
|
||||
@@ -67,11 +67,11 @@ void DitherTests::stylesKeepSwiftsNamesOrderAndGroups()
|
||||
for (const DitherStyle each : allDitherStyles)
|
||||
names << rawValue(each);
|
||||
QCOMPARE(names, (QStringList{"Atkinson (Classic Mac)", "Floyd–Steinberg", "Bayer 2 × 2", "Bayer 4 × 4", "Bayer 8 × 8", "Halftone Dots",
|
||||
"Halftone Lines", "Halftone Diamonds", "Mac Patterns", "ASCII"}));
|
||||
"Halftone Lines", "Halftone Diamonds", "Mac Patterns", "ASCII", "Scanlines (CRT)"}));
|
||||
QString groups;
|
||||
for (const DitherStyle each : allDitherStyles)
|
||||
groups += QString::number(ditherGroup(each));
|
||||
QCOMPARE(groups, QString("0011122233"));
|
||||
QCOMPARE(groups, QString("00111222333"));
|
||||
QVERIFY(diffuses(DitherStyle::floydSteinberg) && !diffuses(DitherStyle::bayer2));
|
||||
QVERIFY(hasTones(DitherStyle::bayer8) && !hasTones(DitherStyle::dots));
|
||||
QVERIFY(isHalftone(DitherStyle::diamonds) && !isHalftone(DitherStyle::patterns));
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
#include "DialogDesk.h"
|
||||
#include "Document/ProjectWorkspace.h"
|
||||
#include "SelectionFixtures.h"
|
||||
#include "UI/ProjectWorkspaceView.h"
|
||||
#include <QtTest>
|
||||
|
||||
// Swift 1.4.4: quit applies canvas edits and cancels dialogs.
|
||||
namespace {
|
||||
ImportedImage filled(const QColor &colour)
|
||||
{
|
||||
QImage pixels(100, 20, QImage::Format_RGBA8888_Premultiplied);
|
||||
pixels.fill(colour);
|
||||
return ImportedImage(pixels, pixels, QStringLiteral("Fixture"));
|
||||
}
|
||||
|
||||
// Swift's await: true once the call's callback has run.
|
||||
bool awaited(const std::function<void(std::function<void()>)> &call)
|
||||
{
|
||||
bool done = false;
|
||||
call([&done] { done = true; });
|
||||
return QTest::qWaitFor([&done] { return done; }, 10'000);
|
||||
}
|
||||
|
||||
std::optional<bool> answer(ProjectWorkspace &workspace)
|
||||
{
|
||||
std::optional<bool> result;
|
||||
workspace.confirmQuit([&](bool value) { result = value; });
|
||||
return QTest::qWaitFor([&] { return result.has_value(); }, 10'000) ? result : std::nullopt;
|
||||
}
|
||||
|
||||
// Swift's test: a gradient waiting for Apply.
|
||||
void pendGradient(EditorSession &session)
|
||||
{
|
||||
session.createDocument(100, 20);
|
||||
session.addBlankLayer();
|
||||
session.selectTool(NavigationTool::gradient);
|
||||
session.beginGradient(QPointF(0, 10));
|
||||
session.moveGradient(std::nullopt, QPointF(100, 10));
|
||||
session.endGradientDrag();
|
||||
}
|
||||
|
||||
ImageIdentity identity(const EditorSession &session)
|
||||
{
|
||||
return session.activeLayer().value().asset.value().identity();
|
||||
}
|
||||
}
|
||||
|
||||
class QuitPendingEditsTests : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void quittingAppliesAPendingGradient();
|
||||
void quittingAppliesAPixelMove();
|
||||
void quittingCancelsAnOpenDialog();
|
||||
void quittingCancelsTheOtherDialogs();
|
||||
void quittingCancelsAnAdjustmentsEditor();
|
||||
void aWorkspaceGoneMidSettleCallsNothing();
|
||||
void closingTheWindowAppliesTheGradientThenAsks();
|
||||
void aBusyProjectHoldsTheQuit();
|
||||
void aQuitWhileManagingIsRefusedAtOnce();
|
||||
};
|
||||
|
||||
void QuitPendingEditsTests::quittingAppliesAPendingGradient()
|
||||
{
|
||||
ProjectWorkspace workspace;
|
||||
EditorSession &session = workspace.current().session;
|
||||
pendGradient(session);
|
||||
QVERIFY(!workspace.canSwitch());
|
||||
const int count = session.history.undoCount();
|
||||
QVERIFY(awaited([&](std::function<void()> done) { workspace.settlePendingEdits(std::move(done)); }));
|
||||
QVERIFY(!session.gradientEdit());
|
||||
QCOMPARE(session.history.undoCount(), count + 1);
|
||||
QCOMPARE(session.history.undoName(), QString("Gradient"));
|
||||
QVERIFY(workspace.canSwitch());
|
||||
}
|
||||
|
||||
void QuitPendingEditsTests::quittingAppliesAPixelMove()
|
||||
{
|
||||
ProjectWorkspace workspace;
|
||||
EditorSession &session = workspace.current().session;
|
||||
session.createDocument(100, 20);
|
||||
session.insert(filled(QColor(200, 100, 50)));
|
||||
session.applySelection(rectPath(QRectF(10, 5, 10, 10)), SelectionMode::replace, "Select");
|
||||
QVERIFY(session.beginPixelMove());
|
||||
session.movePixels(QSizeF(30, 0));
|
||||
QVERIFY(!workspace.canSwitch());
|
||||
const int count = session.history.undoCount();
|
||||
QVERIFY(awaited([&](std::function<void()> done) { workspace.settlePendingEdits(std::move(done)); }));
|
||||
QVERIFY(!session.pixelMove());
|
||||
QCOMPARE(session.history.undoCount(), count + 1);
|
||||
QCOMPARE(session.history.undoName(), QString("Move Pixels"));
|
||||
QCOMPARE(session.selection().value().path.boundingRect(), QRectF(40, 5, 10, 10));
|
||||
QVERIFY(workspace.canSwitch());
|
||||
}
|
||||
|
||||
void QuitPendingEditsTests::quittingCancelsAnOpenDialog()
|
||||
{
|
||||
ProjectWorkspace workspace;
|
||||
EditorSession &session = workspace.current().session;
|
||||
session.createDocument(100, 20);
|
||||
session.insert(filled(QColor(200, 100, 50)));
|
||||
const ImageIdentity original = identity(session);
|
||||
const int count = session.history.undoCount();
|
||||
const std::vector<std::function<void()>> opens{[&] { session.beginFilter(FilterKind::gaussianBlur); },
|
||||
[&] { session.beginHueSaturation(); }, [&] { session.beginLevels(); }};
|
||||
for (const std::function<void()> &open : opens) {
|
||||
open();
|
||||
QVERIFY(!workspace.canSwitch());
|
||||
QVERIFY(awaited([&](std::function<void()> done) { workspace.settlePendingEdits(std::move(done)); }));
|
||||
QVERIFY(workspace.canSwitch());
|
||||
QVERIFY(identity(session) == original);
|
||||
}
|
||||
QVERIFY(!session.filterEdit() && !session.hueSaturation() && !session.levels());
|
||||
QCOMPARE(session.history.undoCount(), count);
|
||||
}
|
||||
|
||||
void QuitPendingEditsTests::quittingCancelsTheOtherDialogs()
|
||||
{
|
||||
ProjectWorkspace workspace;
|
||||
EditorSession &session = workspace.current().session;
|
||||
session.createDocument(100, 20);
|
||||
session.insert(filled(QColor(200, 100, 50)));
|
||||
const QPainterPath before = rectPath(QRectF(10, 5, 10, 10));
|
||||
session.applySelection(before, SelectionMode::replace, "Select");
|
||||
const PaletteColor foreground = session.foregroundColor();
|
||||
const auto settle = [&] { return awaited([&](std::function<void()> done) { workspace.settlePendingEdits(std::move(done)); }); };
|
||||
// Color Range: the selection goes back as it was.
|
||||
session.beginColorRange();
|
||||
QVERIFY(session.colorRange() && !workspace.canSwitch());
|
||||
QVERIFY(settle());
|
||||
QVERIFY(!session.colorRange());
|
||||
QCOMPARE(session.selection().value().path.boundingRect(), before.boundingRect());
|
||||
// Expand, Contract or Feather: its panel goes.
|
||||
session.setSelectionAmountOperation(SelectionAmountOperation::feather);
|
||||
QVERIFY(!workspace.canSwitch());
|
||||
QVERIFY(settle());
|
||||
QVERIFY(!session.selectionAmountOperation());
|
||||
// The colour picker: closed, its colour not taken.
|
||||
session.openColorPicker(false);
|
||||
session.setColorPickerHSB(PickerHSB(120, 1, 1));
|
||||
QVERIFY(!workspace.canSwitch());
|
||||
QVERIFY(settle());
|
||||
QVERIFY(!session.colorPicker());
|
||||
QVERIFY(session.foregroundColor() == foreground);
|
||||
QVERIFY(workspace.canSwitch());
|
||||
}
|
||||
|
||||
void QuitPendingEditsTests::quittingCancelsAnAdjustmentsEditor()
|
||||
{
|
||||
ProjectWorkspace workspace;
|
||||
EditorSession &session = workspace.current().session;
|
||||
session.createDocument(100, 20);
|
||||
session.insert(filled(Qt::white));
|
||||
session.addAdjustment(AdjustmentKind::exposure);
|
||||
const QUuid id = session.adjustmentEditingID().value();
|
||||
const LayerAdjustment made = session.activeLayer().value().adjustment.value();
|
||||
QVERIFY(awaited([&](std::function<void()> done) { session.beginAdjustmentEditing(id, std::move(done)); }));
|
||||
FilterSettings settings = session.filterEdit().value().settings;
|
||||
settings.exposure.exposure = 2;
|
||||
session.updateFilter(settings, true);
|
||||
QVERIFY(!(session.activeLayer().value().adjustment.value() == made));
|
||||
QVERIFY(!workspace.canSwitch());
|
||||
QVERIFY(awaited([&](std::function<void()> done) { workspace.settlePendingEdits(std::move(done)); }));
|
||||
QVERIFY(!session.adjustmentEditingID() && !session.adjustmentOriginal() && !session.filterEdit());
|
||||
QVERIFY(session.activeLayer().value().adjustment.value() == made);
|
||||
QVERIFY(workspace.canSwitch());
|
||||
}
|
||||
|
||||
void QuitPendingEditsTests::aWorkspaceGoneMidSettleCallsNothing()
|
||||
{
|
||||
auto workspace = std::make_unique<ProjectWorkspace>();
|
||||
const std::shared_ptr<ProjectTab> tab = workspace->tabs().front();
|
||||
pendGradient(tab->session);
|
||||
bool called = false;
|
||||
workspace->settlePendingEdits([&called] { called = true; });
|
||||
workspace.reset();
|
||||
// The tab lives on and its gradient lands; no answer.
|
||||
QTRY_VERIFY(!tab->session.gradientEdit());
|
||||
QTest::qWait(100);
|
||||
QVERIFY(!called);
|
||||
QCOMPARE(tab->session.history.undoName(), QString("Gradient"));
|
||||
}
|
||||
|
||||
void QuitPendingEditsTests::closingTheWindowAppliesTheGradientThenAsks()
|
||||
{
|
||||
ProjectWorkspace workspace;
|
||||
ProjectWorkspaceView view(workspace);
|
||||
view.show();
|
||||
EditorSession &session = workspace.current().session;
|
||||
pendGradient(session);
|
||||
session.history.markSaved();
|
||||
DialogDesk desk;
|
||||
desk.replies = {"Cancel"};
|
||||
// The gradient is a step by the time it asks.
|
||||
desk.note = [&] { return QStringLiteral("pending=%1 step=%2").arg(session.gradientEdit().has_value()).arg(session.history.undoName()); };
|
||||
view.close();
|
||||
QTRY_COMPARE(desk.seen.size(), 1);
|
||||
QVERIFY2(desk.seen[0].startsWith("alert|2|Save changes to Untitled?") && desk.seen[0].endsWith("|pending=0 step=Gradient"),
|
||||
qPrintable(desk.seen[0]));
|
||||
QTRY_VERIFY(!workspace.isManaging());
|
||||
QVERIFY(view.isVisible());
|
||||
}
|
||||
|
||||
void QuitPendingEditsTests::aBusyProjectHoldsTheQuit()
|
||||
{
|
||||
ProjectWorkspace workspace;
|
||||
EditorSession &session = workspace.current().session;
|
||||
pendGradient(session);
|
||||
session.setIsProjectBusy(true);
|
||||
DialogDesk desk;
|
||||
QTest::ignoreMessage(QtWarningMsg, "Quit refused: the project is still busy");
|
||||
QCOMPARE(answer(workspace), std::optional(false));
|
||||
// Nothing asked, nothing applied: the gradient waits.
|
||||
QVERIFY(desk.seen.isEmpty());
|
||||
QVERIFY(session.gradientEdit());
|
||||
QVERIFY(!workspace.isManaging());
|
||||
session.setIsProjectBusy(false);
|
||||
}
|
||||
|
||||
void QuitPendingEditsTests::aQuitWhileManagingIsRefusedAtOnce()
|
||||
{
|
||||
ProjectWorkspace workspace;
|
||||
EditorSession &session = workspace.current().session;
|
||||
session.createDocument(100, 20);
|
||||
DialogDesk desk;
|
||||
desk.replies = {"Cancel"};
|
||||
std::optional<bool> second;
|
||||
bool settled = false;
|
||||
bool answered = false;
|
||||
// While the first quit asks, a second is refused unasked.
|
||||
desk.note = [&] {
|
||||
session.setSelectionAmountOperation(SelectionAmountOperation::feather);
|
||||
workspace.confirmQuit([&](bool value) { second = value; });
|
||||
answered = QTest::qWaitFor([&] { return second.has_value(); }, 5000);
|
||||
settled = !session.selectionAmountOperation();
|
||||
session.setSelectionAmountOperation(std::nullopt);
|
||||
return QString();
|
||||
};
|
||||
QCOMPARE(answer(workspace), std::optional(false));
|
||||
QCOMPARE(desk.seen.size(), 1);
|
||||
QVERIFY(answered);
|
||||
QCOMPARE(second, std::optional(false));
|
||||
QVERIFY(!settled);
|
||||
}
|
||||
|
||||
QTEST_MAIN(QuitPendingEditsTests)
|
||||
#include "QuitPendingEditsTests.moc"
|
||||
@@ -0,0 +1,377 @@
|
||||
#include "Document/BrushStroke.h"
|
||||
#include "Document/Dither.h"
|
||||
#include "Document/EditorSession.h"
|
||||
#include "Document/PixelAdjust.h"
|
||||
#include "UI/FilterSheet.h"
|
||||
#include <QCheckBox>
|
||||
#include <QLabel>
|
||||
#include <QLayout>
|
||||
#include <QLineEdit>
|
||||
#include <QPainter>
|
||||
#include <QSlider>
|
||||
#include <QtTest>
|
||||
#include <numeric>
|
||||
|
||||
// Swift 1.4.4's Dither › Scanlines (CRT), on every core.
|
||||
namespace {
|
||||
QImage filled(int width, int height, QColor colour)
|
||||
{
|
||||
QImage image = BrushRaster::context(width, height, false);
|
||||
image.fill(colour);
|
||||
return image;
|
||||
}
|
||||
|
||||
DitherSettings scanlines(double spacing, double glow = 0)
|
||||
{
|
||||
DitherSettings settings;
|
||||
settings.style = DitherStyle::scanlines;
|
||||
settings.lineSpacing = spacing;
|
||||
settings.glow = glow;
|
||||
return settings;
|
||||
}
|
||||
|
||||
// One channel of one column, top to bottom.
|
||||
std::vector<int> column(const QImage &image, int x, int channel = 0)
|
||||
{
|
||||
std::vector<int> values;
|
||||
for (int y = 0; y < image.height(); ++y)
|
||||
values.push_back(image.constScanLine(y)[x * 4 + channel]);
|
||||
return values;
|
||||
}
|
||||
|
||||
std::vector<int> row(const QImage &image, int y, int channel = 0)
|
||||
{
|
||||
std::vector<int> values;
|
||||
for (int x = 0; x < image.width(); ++x)
|
||||
values.push_back(image.constScanLine(y)[x * 4 + channel]);
|
||||
return values;
|
||||
}
|
||||
|
||||
// Swift's test: one column of a flat gray's render.
|
||||
std::vector<int> gray(int level, double spacing, double glow = 0)
|
||||
{
|
||||
return column(scanlines(spacing, glow).apply(filled(16, 32, QColor(level, level, level))), 5);
|
||||
}
|
||||
|
||||
std::vector<int> repeated(const std::vector<int> &band, int times)
|
||||
{
|
||||
std::vector<int> all;
|
||||
for (int each = 0; each < times; ++each)
|
||||
all.insert(all.end(), band.begin(), band.end());
|
||||
return all;
|
||||
}
|
||||
|
||||
struct Shown {
|
||||
EditorSession session;
|
||||
std::unique_ptr<FilterSheet> sheet;
|
||||
Shown()
|
||||
{
|
||||
session.createDocument(40, 30);
|
||||
const QImage image = filled(40, 30, QColor(200, 120, 40));
|
||||
session.insert(ImportedImage(image, image, QStringLiteral("Colour")));
|
||||
session.beginFilter(FilterKind::dither);
|
||||
FilterSettings settings = session.filterEdit().value().settings;
|
||||
settings.dither.style = DitherStyle::scanlines;
|
||||
session.updateFilter(settings, true);
|
||||
sheet = std::make_unique<FilterSheet>(session);
|
||||
sheet->show();
|
||||
}
|
||||
const DitherSettings &dither() const { return session.filterEdit().value().settings.dither; }
|
||||
// The shown rows in the column's order, with their help.
|
||||
QStringList rows() const
|
||||
{
|
||||
QStringList titles;
|
||||
QLayout *column = sheet->layout();
|
||||
for (int index = 0; index < column->count(); ++index) {
|
||||
QWidget *each = column->itemAt(index)->widget();
|
||||
if (!each || each->isHidden())
|
||||
continue;
|
||||
if (auto *box = qobject_cast<QCheckBox *>(each); box && box->text() != QLatin1String("Preview"))
|
||||
titles << box->text();
|
||||
for (QLabel *label : each->findChildren<QLabel *>(QString(), Qt::FindDirectChildrenOnly)) {
|
||||
if (label->buddy())
|
||||
titles << label->text() + u'|' + each->toolTip();
|
||||
}
|
||||
}
|
||||
return titles;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class ScanlinesTests : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void scanlinesAreLinesOfLightThatBloomWithBrightness();
|
||||
void glowLightsBetweenTheLines();
|
||||
void dotsBreakTheLinesIntoBeads();
|
||||
void wobbleMovesLinesSideways();
|
||||
void theStyleDrawsAtFullResolutionWithoutMarks();
|
||||
void normalizingClampsTheFourSettings();
|
||||
void eachLineIsTheAverageOfItsRows();
|
||||
void theScreenIsTheDarkColourOrBlack();
|
||||
void clearPixelsStayClear();
|
||||
void everyBandOfATallImageIsDrawn();
|
||||
void theGlowIsTheBlurredLightAtItsAmount();
|
||||
void theGlowNeverPassesAlpha();
|
||||
void theSheetShowsScanlinesRows();
|
||||
void theRowsWriteTheirSettings();
|
||||
};
|
||||
|
||||
void ScanlinesTests::scanlinesAreLinesOfLightThatBloomWithBrightness()
|
||||
{
|
||||
const std::vector<int> white = gray(255, 8), dim = gray(89, 8), black = gray(0, 8);
|
||||
for (int band = 0; band < 4; ++band) {
|
||||
const std::vector<int> rows(white.begin() + band * 8, white.begin() + band * 8 + 8);
|
||||
QVERIFY(rows[3] == 255 && rows[4] == 255);
|
||||
QVERIFY(std::any_of(rows.begin(), rows.end(), [](int value) { return value < 40; }));
|
||||
}
|
||||
QVERIFY(std::accumulate(dim.begin(), dim.end(), 0) * 2 < std::accumulate(white.begin(), white.end(), 0));
|
||||
QVERIFY(std::all_of(black.begin(), black.end(), [](int value) { return value == 0; }));
|
||||
// The beam's half height, 2.8 white, about 1.98 at 0.35.
|
||||
QCOMPARE(white, repeated({0, 255, 255, 255, 255, 255, 255, 0}, 4));
|
||||
QCOMPARE(dim, repeated({0, 0, 118, 120, 120, 118, 0, 0}, 4));
|
||||
}
|
||||
|
||||
void ScanlinesTests::glowLightsBetweenTheLines()
|
||||
{
|
||||
const std::vector<int> plain = gray(255, 8), glowing = gray(255, 8, 100);
|
||||
QVERIFY2(glowing[0] > plain[0] + 40, qPrintable(QString::number(glowing[0])));
|
||||
}
|
||||
|
||||
void ScanlinesTests::dotsBreakTheLinesIntoBeads()
|
||||
{
|
||||
const QImage white = filled(64, 16, Qt::white);
|
||||
DitherSettings settings = scanlines(8);
|
||||
QCOMPARE(row(settings.apply(white), 4), std::vector<int>(64, 255));
|
||||
settings.dots = 100;
|
||||
// Each bead lit at its middle, dark at its ends.
|
||||
const std::vector<int> lit = row(settings.apply(white), 4);
|
||||
QCOMPARE(std::vector<int>(lit.begin(), lit.begin() + 8), (std::vector<int>{0, 255, 255, 255, 255, 255, 255, 0}));
|
||||
QCOMPARE(std::vector<int>(lit.begin() + 8, lit.begin() + 16), (std::vector<int>{0, 255, 255, 255, 255, 255, 255, 0}));
|
||||
// Half dots: the bead ends dim off the middle.
|
||||
settings.dots = 50;
|
||||
QCOMPARE(row(settings.apply(white), 1)[0], 85);
|
||||
}
|
||||
|
||||
void ScanlinesTests::wobbleMovesLinesSideways()
|
||||
{
|
||||
QImage edge = filled(64, 64, Qt::black);
|
||||
QPainter(&edge).fillRect(QRect(32, 0, 32, 64), Qt::white);
|
||||
const auto edges = [&edge](double wobble) {
|
||||
DitherSettings settings = scanlines(8);
|
||||
settings.wobble = wobble;
|
||||
const QImage result = settings.apply(edge);
|
||||
std::vector<int> found;
|
||||
for (int line = 0; line < 8; ++line) {
|
||||
const std::vector<int> middle = row(result, line * 8 + 4);
|
||||
found.push_back(int(std::find_if(middle.begin(), middle.end(), [](int value) { return value > 128; }) - middle.begin()));
|
||||
}
|
||||
return found;
|
||||
};
|
||||
QCOMPARE(edges(0), std::vector<int>(8, 32));
|
||||
// Each line's shift: 12 times the two waves, rounded.
|
||||
QCOMPARE(edges(12), (std::vector<int>{35, 36, 35, 41, 44, 37, 32, 34}));
|
||||
}
|
||||
|
||||
void ScanlinesTests::theStyleDrawsAtFullResolutionWithoutMarks()
|
||||
{
|
||||
QVERIFY(!usesPixelSize(DitherStyle::scanlines) && !usesPixelSize(DitherStyle::ascii) && usesPixelSize(DitherStyle::patterns));
|
||||
QVERIFY(!drawsMarks(DitherStyle::scanlines) && drawsMarks(DitherStyle::ascii));
|
||||
QVERIFY(!hasTones(DitherStyle::scanlines) && !isHalftone(DitherStyle::scanlines) && ditherGroup(DitherStyle::scanlines) == 3);
|
||||
// A pixel size changes nothing: no chunks, no dots.
|
||||
const QImage image = filled(20, 24, QColor(180, 90, 30));
|
||||
DitherSettings settings = scanlines(4);
|
||||
const QImage plain = settings.apply(image);
|
||||
settings.pixelSize = 5;
|
||||
settings.pixelShape = DitherPixelShape::dot;
|
||||
QCOMPARE(settings.apply(image), plain);
|
||||
// The cell is the line spacing, not the cell size.
|
||||
settings.cellSize = 32;
|
||||
QCOMPARE(settings.apply(image), plain);
|
||||
}
|
||||
|
||||
void ScanlinesTests::normalizingClampsTheFourSettings()
|
||||
{
|
||||
DitherSettings settings;
|
||||
QCOMPARE(settings.lineSpacing, 4.0);
|
||||
QCOMPARE(settings.glow, 35.0);
|
||||
QCOMPARE(settings.dots, 0.0);
|
||||
QCOMPARE(settings.wobble, 0.0);
|
||||
settings.lineSpacing = 1.4;
|
||||
settings.glow = 150;
|
||||
settings.dots = -5;
|
||||
settings.wobble = 3.5;
|
||||
DitherSettings result = settings.normalized();
|
||||
QCOMPARE(result.lineSpacing, 2.0);
|
||||
QCOMPARE(result.glow, 100.0);
|
||||
QCOMPARE(result.dots, 0.0);
|
||||
QCOMPARE(result.wobble, 3.5);
|
||||
settings.lineSpacing = 40;
|
||||
settings.glow = -1;
|
||||
settings.dots = 120;
|
||||
settings.wobble = 70;
|
||||
result = settings.normalized();
|
||||
QCOMPARE(result.lineSpacing, 32.0);
|
||||
QCOMPARE(result.glow, 0.0);
|
||||
QCOMPARE(result.dots, 100.0);
|
||||
QCOMPARE(result.wobble, 64.0);
|
||||
// Rounded within; not a number takes the default.
|
||||
settings.lineSpacing = 5.5;
|
||||
settings.glow = std::nan("");
|
||||
settings.dots = std::nan("");
|
||||
settings.wobble = std::nan("");
|
||||
result = settings.normalized();
|
||||
QCOMPARE(result.lineSpacing, 6.0);
|
||||
QCOMPARE(result.glow, 35.0);
|
||||
QCOMPARE(result.dots, 0.0);
|
||||
QCOMPARE(result.wobble, 0.0);
|
||||
settings.lineSpacing = std::nan("");
|
||||
QCOMPARE(settings.normalized().lineSpacing, 4.0);
|
||||
// Glow and dots keep their fractions.
|
||||
settings.glow = 12.5;
|
||||
settings.dots = 33.3;
|
||||
QCOMPARE(settings.normalized().glow, 12.5);
|
||||
QCOMPARE(settings.normalized().dots, 33.3);
|
||||
}
|
||||
|
||||
void ScanlinesTests::eachLineIsTheAverageOfItsRows()
|
||||
{
|
||||
// Rows of white and black: each line lights at half.
|
||||
QImage stripes = filled(8, 16, Qt::black);
|
||||
for (int y = 0; y < 16; y += 2)
|
||||
QPainter(&stripes).fillRect(QRect(0, y, 8, 1), Qt::white);
|
||||
QCOMPARE(column(scanlines(8).apply(stripes), 3), repeated({0, 37, 172, 172, 172, 172, 37, 0}, 2));
|
||||
}
|
||||
|
||||
void ScanlinesTests::theScreenIsTheDarkColourOrBlack()
|
||||
{
|
||||
const QImage image = filled(8, 8, QColor(255, 128, 0));
|
||||
DitherSettings settings = scanlines(8);
|
||||
settings.colors = DitherColors::twoColors;
|
||||
settings.dark = AdjustmentColor(0, 0, 1);
|
||||
settings.light = AdjustmentColor(1, 1, 0);
|
||||
const QImage two = settings.apply(image);
|
||||
// Off the line the dark colour; on it, the mix.
|
||||
QCOMPARE(two.pixelColor(2, 0), QColor(0, 0, 255));
|
||||
QCOMPARE(two.pixelColor(2, 4), QColor(197, 197, 147));
|
||||
settings.colors = DitherColors::original;
|
||||
const QImage original = settings.apply(image);
|
||||
QCOMPARE(original.pixelColor(2, 0), QColor(0, 0, 0));
|
||||
QCOMPARE(original.pixelColor(2, 4), QColor(255, 173, 0));
|
||||
}
|
||||
|
||||
void ScanlinesTests::clearPixelsStayClear()
|
||||
{
|
||||
QImage image = filled(8, 16, Qt::white);
|
||||
QPainter painter(&image);
|
||||
painter.setCompositionMode(QPainter::CompositionMode_Source);
|
||||
painter.fillRect(QRect(0, 0, 8, 8), Qt::transparent);
|
||||
painter.end();
|
||||
const QImage result = scanlines(8, 100).apply(image);
|
||||
QCOMPARE(column(result, 2, 3), (std::vector<int>{0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255}));
|
||||
QCOMPARE(column(result, 2, 0)[4], 0);
|
||||
}
|
||||
|
||||
void ScanlinesTests::everyBandOfATallImageIsDrawn()
|
||||
{
|
||||
// 150 two-row lines: 32 bands of 5, two empty.
|
||||
const QImage image = filled(3, 300, Qt::white);
|
||||
QCOMPARE(column(scanlines(2).apply(image), 1), repeated({241, 241}, 150));
|
||||
// 70 lines of 8: bands of 3, the last one.
|
||||
const std::vector<int> tall = column(scanlines(8).apply(filled(3, 560, Qt::white)), 1);
|
||||
QCOMPARE(tall, repeated({0, 255, 255, 255, 255, 255, 255, 0}, 70));
|
||||
}
|
||||
|
||||
void ScanlinesTests::theGlowIsTheBlurredLightAtItsAmount()
|
||||
{
|
||||
QImage image = filled(64, 160, Qt::black);
|
||||
QPainter(&image).fillRect(QRect(0, 0, 32, 40), Qt::white);
|
||||
const QImage lines = scanlines(8).apply(image);
|
||||
// Swift's first glow: the lines blurred whole, sigma 27.
|
||||
const QImage blurred = PixelAdjust::gaussianBlur(lines, 8 * 3 + 3, true);
|
||||
for (const double glow : {20.0, 100.0}) {
|
||||
const QImage result = scanlines(8, glow).apply(image);
|
||||
const float amount = float(glow / 100 * 2.5);
|
||||
int worst = 0;
|
||||
for (int y = 0; y < 160; ++y) {
|
||||
for (int x = 0; x < 64; ++x) {
|
||||
const int plain = lines.constScanLine(y)[x * 4], light = blurred.constScanLine(y)[x * 4];
|
||||
const int expected = int(std::lround(std::min(255.0f, float(plain) + float(light) * amount)));
|
||||
worst = std::max(worst, std::abs(result.constScanLine(y)[x * 4] - expected));
|
||||
}
|
||||
}
|
||||
// Three levels of the shrink's blur, times 2.5.
|
||||
QVERIFY2(worst <= 8, qPrintable(QString::number(worst)));
|
||||
}
|
||||
// Below the light, at amount 0.5, it fades.
|
||||
const std::vector<int> fading = column(scanlines(8, 20).apply(image), 16);
|
||||
QVERIFY(fading[48] > 20 && fading[48] < fading[40] && fading[120] < 3);
|
||||
}
|
||||
|
||||
void ScanlinesTests::theGlowNeverPassesAlpha()
|
||||
{
|
||||
QImage image = BrushRaster::context(16, 32, false);
|
||||
image.fill(QColor(255, 255, 255, 128));
|
||||
const QImage result = scanlines(4, 100).apply(image);
|
||||
for (int y = 0; y < 32; ++y) {
|
||||
for (int x = 0; x < 16; ++x) {
|
||||
const uchar *at = result.constScanLine(y) + x * 4;
|
||||
QCOMPARE(int(at[3]), 128);
|
||||
QVERIFY(at[0] <= 128 && at[1] <= 128 && at[2] <= 128);
|
||||
}
|
||||
}
|
||||
QCOMPARE(int(result.constScanLine(0)[0]), 128);
|
||||
}
|
||||
|
||||
void ScanlinesTests::theSheetShowsScanlinesRows()
|
||||
{
|
||||
Shown shown;
|
||||
QCOMPARE(shown.rows(), (QStringList{"Style|", "Line Spacing|How far apart the screen's lines are",
|
||||
"Glow|Light blooming around the lines, like a CRT's phosphors", "Dots|Break the lines into glowing beads",
|
||||
"Wobble|Make the lines waver sideways down the screen, like a CRT losing sync",
|
||||
"Density|More ink (darker) or less before dithering", "Contrast|", "Colors|"}));
|
||||
// Each slider's travel reaches its range's ends.
|
||||
const std::tuple<const char *, double DitherSettings::*, double, double> sliders[] = {
|
||||
{"lineSpacingSlider", &DitherSettings::lineSpacing, 2, 32}, {"glowSlider", &DitherSettings::glow, 0, 100},
|
||||
{"dotsSlider", &DitherSettings::dots, 0, 100}, {"wobbleSlider", &DitherSettings::wobble, 0, 64}};
|
||||
for (const auto &[name, setting, low, high] : sliders) {
|
||||
QSlider *slider = shown.sheet->findChild<QSlider *>(QString::fromLatin1(name));
|
||||
QVERIFY2(slider, name);
|
||||
slider->setValue(slider->maximum());
|
||||
QCOMPARE(shown.dither().*setting, high);
|
||||
slider->setValue(slider->minimum());
|
||||
QCOMPARE(shown.dither().*setting, low);
|
||||
}
|
||||
// Pixel Size and Pixel Shape stay away under Scanlines.
|
||||
FilterSettings settings = shown.session.filterEdit().value().settings;
|
||||
settings.dither.pixelSize = 4;
|
||||
shown.session.updateFilter(settings, true);
|
||||
QVERIFY(!shown.rows().join(',').contains("Pixel"));
|
||||
settings.dither.style = DitherStyle::bayer4;
|
||||
shown.session.updateFilter(settings, true);
|
||||
QVERIFY(shown.rows().contains("Pixel Shape|Draw each chunky pixel as a solid square, or as a round dot like a dot-matrix screen"));
|
||||
QVERIFY(!shown.rows().join(',').contains("Glow"));
|
||||
}
|
||||
|
||||
void ScanlinesTests::theRowsWriteTheirSettings()
|
||||
{
|
||||
Shown shown;
|
||||
const std::tuple<const char *, double DitherSettings::*, const char *, double> rows[] = {
|
||||
{"lineSpacingField", &DitherSettings::lineSpacing, "12", 12}, {"glowField", &DitherSettings::glow, "60", 60},
|
||||
{"dotsField", &DitherSettings::dots, "25", 25}, {"wobbleField", &DitherSettings::wobble, "9", 9}};
|
||||
for (const auto &[name, setting, typed, expected] : rows) {
|
||||
const DitherSettings before = shown.dither();
|
||||
QLineEdit *field = shown.sheet->findChild<QLineEdit *>(QString::fromLatin1(name));
|
||||
QVERIFY2(field, name);
|
||||
field->setFocus();
|
||||
field->selectAll();
|
||||
QTest::keyClicks(field, QString::fromLatin1(typed));
|
||||
QTest::keyClick(field, Qt::Key_Return);
|
||||
DitherSettings wanted = before;
|
||||
wanted.*setting = expected;
|
||||
QVERIFY2(shown.dither() == wanted, name);
|
||||
}
|
||||
}
|
||||
|
||||
QTEST_MAIN(ScanlinesTests)
|
||||
#include "ScanlinesTests.moc"
|
||||
@@ -166,6 +166,8 @@ compositor_test(ProjectReceiveTests)
|
||||
compositor_test(ProjectReplaceTests)
|
||||
compositor_test(ProjectWorkspaceFlowTests)
|
||||
compositor_test(QuitWhileEditingTextTests)
|
||||
compositor_test(QuitPendingEditsTests)
|
||||
compositor_test(ScanlinesTests)
|
||||
compositor_test(OpenRecentTests)
|
||||
compositor_test(ColoredSliderTests)
|
||||
compositor_test(SheetTeardownTests)
|
||||
|
||||
Reference in new issue
Block a user