Modules/Sketch2D/tests/FilterTests.cpp
Шурупов Илья Викторович ebc22c5f38 Use one menu for the menu bar and the tablet menu pages
The actions and settings split is reverted. Sketch2DRoot::mainMenu is
the single menu structure: on PC it fills the menu bar and the studio
gear is hidden, on the tablet the gear opens it as pages.
2026-10-20 02:31:43 +03:00

159 lines
5.1 KiB
C++

#include "Sketch2DUI.hpp"
#include "StudioUI.hpp"
#include "Ui.hpp"
#include "UiDriver.hpp"
#include "UnitTest++/UnitTest++.h"
using namespace tp;
using namespace tp::sketch2d;
using namespace tp::ui;
using namespace tp::ui::testing;
namespace {
class FilterViewport : public ViewportRenderer {
public:
void render(Document&, const ViewSettings&, Vec2F) override {}
[[nodiscard]] ImageId image() const override { return { 1 }; }
void setDebugOverlay(bool) override {}
[[nodiscard]] bool debugOverlay() const override { return false; }
};
class FilterIcons : public IconProvider {
public:
[[nodiscard]] ImageId icon(const std::string& name) const override { return { (ualni) name.size() + 1 }; }
};
void put(Document& document, int4 x, int4 y, const RGBA& color) {
uint1* pixel = document.activeLayer()->tileAt({ 0, 0 })->pixels() + ((size_t) y * kTileSize + (size_t) x) * 4;
pixel[0] = (uint1) std::lround(color.r * color.a * 255);
pixel[1] = (uint1) std::lround(color.g * color.a * 255);
pixel[2] = (uint1) std::lround(color.b * color.a * 255);
pixel[3] = (uint1) std::lround(color.a * 255);
}
struct FilterScene {
Document document;
Brush brush;
FilterViewport viewport;
FilterIcons icons;
Sketch2DRoot root{ document, viewport, brush, icons };
UiDriver driver{ root, { 1280, 800 } };
FilterScene() {
put(document, 1, 1, { 1, 0, 0, 1 });
put(document, 2, 1, { 0.2f, 0.4f, 0.6f, 0.5f });
put(document, 3, 1, { 1, 1, 1, 1 });
put(document, 4, 1, { 0, 0, 0, 1 });
document.commit("Pixels");
}
RGBA at(int4 x, int4 y) { return document.sampleColor({ (halnf) x + 0.5f, (halnf) y + 0.5f }, false); }
};
void checkRgba(const RGBA& expected, const RGBA& actual) {
CHECK_CLOSE(expected.r, actual.r, 0.02f);
CHECK_CLOSE(expected.g, actual.g, 0.02f);
CHECK_CLOSE(expected.b, actual.b, 0.02f);
CHECK_CLOSE(expected.a, actual.a, 0.02f);
}
}
SUITE(PixelFilters) {
TEST(InvertFlipsColorsAndKeepsAlpha) {
FilterScene scene;
scene.root.commands.run(Command::Invert);
checkRgba({ 0, 1, 1, 1 }, scene.at(1, 1));
checkRgba({ 0.8f, 0.6f, 0.4f, 0.5f }, scene.at(2, 1));
CHECK_CLOSE(0.0f, scene.at(10, 10).a, 1e-6f);
scene.root.commands.run(Command::Undo);
checkRgba({ 1, 0, 0, 1 }, scene.at(1, 1));
}
TEST(DesaturateUsesLuma) {
FilterScene scene;
scene.root.commands.run(Command::Desaturate);
checkRgba({ 0.299f, 0.299f, 0.299f, 1 }, scene.at(1, 1));
const halnf value = 0.299f * 0.2f + 0.587f * 0.4f + 0.114f * 0.6f;
checkRgba({ value, value, value, 0.5f }, scene.at(2, 1));
}
TEST(ValueToAlphaTurnsWhiteTransparent) {
FilterScene scene;
scene.root.commands.run(Command::ValueToAlpha);
CHECK(scene.at(3, 1).a < 0.01f);
checkRgba({ 0, 0, 0, 1 }, scene.at(4, 1));
checkRgba({ 0, 0, 0, 0.701f }, scene.at(1, 1));
}
TEST(ValueToAlphaInvertedTurnsBlackTransparent) {
FilterScene scene;
scene.root.commands.run(Command::ValueToAlphaInverted);
CHECK(scene.at(4, 1).a < 0.01f);
checkRgba({ 1, 1, 1, 1 }, scene.at(3, 1));
checkRgba({ 1, 1, 1, 0.299f }, scene.at(1, 1));
}
TEST(FiltersRespectTheSelection) {
FilterScene scene;
scene.document.editSelection().addRectangle(0, 0, 2, 2, SelectionMode::Replace);
scene.root.commands.run(Command::Invert);
checkRgba({ 0, 1, 1, 1 }, scene.at(1, 1));
checkRgba({ 1, 1, 1, 1 }, scene.at(3, 1));
}
TEST(AlphaLockRefusesValueToAlphaButAllowsInvert) {
FilterScene scene;
scene.document.toggleAlphaLock();
scene.root.commands.run(Command::ValueToAlpha);
CHECK_EQUAL(std::string("You can't change alpha on a alpha locked layer"), scene.root.lastNotice());
checkRgba({ 1, 1, 1, 1 }, scene.at(3, 1));
scene.root.commands.run(Command::Invert);
checkRgba({ 0, 0, 0, 1 }, scene.at(3, 1));
}
TEST(EmptyLayerExplains) {
FilterScene scene;
scene.document.addLayer("Empty");
const size_t revision = scene.document.history().revision();
scene.root.commands.run(Command::Desaturate);
CHECK_EQUAL(std::string("The layer is empty"), scene.root.lastNotice());
CHECK_EQUAL(revision, scene.document.history().revision());
}
TEST(EditMenuListsFiltersAndPixelEdits) {
FilterScene scene;
std::vector<std::string> texts;
for (const auto& item : scene.root.editMenu.items())
texts.push_back(item->entry().text);
for (const char* text : { "Fill color",
"Clear",
"Crop",
"Free Transform",
"Flip horizontal",
"Flip vertical",
"Invert",
"Desaturate",
"Copy value to alpha",
"Copy value to alpha inverted" }) {
bool found = false;
for (const std::string& existing : texts)
found = found || existing == text;
CHECK(found);
}
}
TEST(TabletQuickMenuHasTheEditMenu) {
Document document;
Brush brush;
FilterViewport viewport;
FilterIcons icons;
Sketch2DRoot classic{ document, viewport, brush, icons };
StudioRoot studio{ classic, brush, icons };
UiDriver driver{ studio, { 1280, 800 } };
bool found = false;
for (const auto& item : classic.mainMenu.items())
if (item->entry().text == "Edit" && item->entry().submenu == &classic.editMenu) found = true;
CHECK(found);
}
}