Modules/Sketch2D/tests/AdjustmentTests.cpp
Шурупов Илья Викторович 41a8263524 Add adjustments with live preview
Edit > Adjust opens brightness & exposure, hue & saturation, color
balance, colorize and alpha, also through the reference shortcuts
Ctrl+L, Ctrl+B and Ctrl+U. The dialog's sliders re-render the active
layer from its original pixels while they move, limited by the
selection. Apply records one undo step and Cancel or Esc restores
the layer. Exposure works in stops, hue, saturation, lightness and
colorize in HSL, and the alpha adjustment leaves alpha locked layers
untouched. LabeledSlider can now be renamed so one dialog serves all
five adjustments.
2026-10-18 07:43:37 +03:00

225 lines
7.1 KiB
C++

#include "Sketch2DUI.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 AdjustViewport : 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 AdjustIcons : public IconProvider {
public:
[[nodiscard]] ImageId icon(const std::string& name) const override { return { (ualni) name.size() + 1 }; }
};
struct AdjustScene {
Document document;
Brush brush;
AdjustViewport viewport;
AdjustIcons icons;
Sketch2DRoot root{ document, viewport, brush, icons };
UiDriver driver{ root, { 1280, 800 } };
AdjustScene() {
uint1* pixels = document.activeLayer()->tileAt({ 0, 0 })->pixels();
const auto put = [pixels](int4 x, int4 r, int4 g, int4 b, int4 a) {
uint1* pixel = pixels + ((size_t) kTileSize + (size_t) x) * 4;
pixel[0] = (uint1) (r * a / 255);
pixel[1] = (uint1) (g * a / 255);
pixel[2] = (uint1) (b * a / 255);
pixel[3] = (uint1) a;
};
put(1, 255, 0, 0, 255);
put(2, 128, 128, 128, 255);
put(3, 51, 102, 153, 128);
document.commit("Pixels");
}
RGBA at(int4 x) { return document.sampleColor({ (halnf) x + 0.5f, 1.5f }, false); }
void setSlider(LabeledSlider& slider, halnf value) {
driver.layout();
driver.click(slider.slider().windowPointForValue(value));
}
};
void checkRgba(const RGBA& expected, const RGBA& actual, halnf tolerance = 0.02f) {
CHECK_CLOSE(expected.r, actual.r, tolerance);
CHECK_CLOSE(expected.g, actual.g, tolerance);
CHECK_CLOSE(expected.b, actual.b, tolerance);
CHECK_CLOSE(expected.a, actual.a, tolerance);
}
void run(Adjustment adjustment, halnf rgba[4]) { adjustment.apply(rgba); }
}
SUITE(Adjustments) {
TEST(BrightnessAddsAndExposureMultiplies) {
halnf pixel[4] = { 0.25f, 0.5f, 0.1f, 1 };
Adjustment adjustment;
adjustment.brightness = 0.1f;
adjustment.exposure = 2;
run(adjustment, pixel);
CHECK_CLOSE(1.1f, pixel[0], 1e-5f);
CHECK_CLOSE(2.1f, pixel[1], 1e-5f);
CHECK_CLOSE(0.5f, pixel[2], 1e-5f);
}
TEST(HueRotatesAndSaturationScales) {
halnf red[4] = { 1, 0, 0, 1 };
Adjustment hue;
hue.kind = AdjustmentKind::HueSaturation;
hue.hueDegrees = 120;
run(hue, red);
CHECK_CLOSE(0.0f, red[0], 1e-4f);
CHECK_CLOSE(1.0f, red[1], 1e-4f);
CHECK_CLOSE(0.0f, red[2], 1e-4f);
halnf color[4] = { 0.8f, 0.4f, 0.4f, 1 };
Adjustment gray;
gray.kind = AdjustmentKind::HueSaturation;
gray.saturation = -1;
run(gray, color);
CHECK_CLOSE(0.6f, color[0], 1e-4f);
CHECK_CLOSE(0.6f, color[1], 1e-4f);
CHECK_CLOSE(0.6f, color[2], 1e-4f);
halnf dark[4] = { 0.5f, 0.5f, 0.5f, 1 };
Adjustment light;
light.kind = AdjustmentKind::HueSaturation;
light.lightness = 0.5f;
run(light, dark);
CHECK_CLOSE(0.75f, dark[0], 1e-4f);
}
TEST(ColorBalanceShiftsEachChannelTowardsItsEnd) {
halnf pixel[4] = { 0.5f, 0.5f, 0.5f, 1 };
Adjustment balance;
balance.kind = AdjustmentKind::ColorBalance;
balance.red = 0.5f;
balance.blue = -0.5f;
run(balance, pixel);
CHECK_CLOSE(0.75f, pixel[0], 1e-5f);
CHECK_CLOSE(0.5f, pixel[1], 1e-5f);
CHECK_CLOSE(0.25f, pixel[2], 1e-5f);
}
TEST(ColorizeKeepsLightness) {
halnf pixel[4] = { 0.2f, 0.6f, 0.4f, 1 };
Adjustment colorize;
colorize.kind = AdjustmentKind::Colorize;
colorize.hueDegrees = 0;
colorize.saturation = 1;
run(colorize, pixel);
CHECK_CLOSE(0.8f, pixel[0], 1e-4f);
CHECK_CLOSE(0.0f, pixel[1], 1e-4f);
CHECK_CLOSE(0.0f, pixel[2], 1e-4f);
CHECK_CLOSE(0.4f, (pixel[0] + std::min(pixel[1], pixel[2])) / 2, 1e-4f);
CHECK(!colorize.isNeutral());
}
TEST(AlphaFadesOrStrengthens) {
halnf pixel[4] = { 1, 1, 1, 0.5f };
Adjustment alpha;
alpha.kind = AdjustmentKind::Alpha;
alpha.alpha = -0.5f;
run(alpha, pixel);
CHECK_CLOSE(0.25f, pixel[3], 1e-5f);
alpha.alpha = 0.5f;
run(alpha, pixel);
CHECK_CLOSE(0.625f, pixel[3], 1e-5f);
}
TEST(DialogPreviewsLiveAndApplyCommits) {
AdjustScene scene;
const size_t revision = scene.document.history().revision();
scene.driver.key(InputID::U, Modifiers{ false, true, false });
CHECK(scene.root.adjustments.isOpen());
CHECK(scene.root.adjustments.kind() == AdjustmentKind::HueSaturation);
CHECK_EQUAL(std::string("Hue"), scene.root.adjustments.first.name());
scene.setSlider(scene.root.adjustments.first, 120);
checkRgba({ 0, 1, 0, 1 }, scene.at(1));
CHECK_EQUAL(revision, scene.document.history().revision());
scene.setSlider(scene.root.adjustments.first, 0);
checkRgba({ 1, 0, 0, 1 }, scene.at(1));
scene.setSlider(scene.root.adjustments.first, 120);
scene.root.adjustments.accept();
CHECK(!scene.root.adjustments.isOpen());
CHECK_EQUAL(revision + 1, scene.document.history().revision());
checkRgba({ 0, 1, 0, 1 }, scene.at(1));
scene.root.commands.run(Command::Undo);
checkRgba({ 1, 0, 0, 1 }, scene.at(1));
}
TEST(CancelRestoresThePixels) {
AdjustScene scene;
scene.root.commands.run(Command::AdjustValue);
scene.setSlider(scene.root.adjustments.first, 0.5f);
CHECK(scene.at(2).r > 0.9f);
const size_t revision = scene.document.history().revision();
scene.root.adjustments.cancel();
CHECK_CLOSE(128.0f / 255.0f, scene.at(2).r, 0.01f);
CHECK_EQUAL(revision, scene.document.history().revision());
CHECK(!scene.document.isAdjusting());
}
TEST(AdjustmentsRespectTheSelectionAndAlpha) {
AdjustScene scene;
scene.document.editSelection().addRectangle(0, 0, 2, 4, SelectionMode::Replace);
scene.root.commands.run(Command::AdjustValue);
scene.setSlider(scene.root.adjustments.first, -1);
scene.root.adjustments.accept();
checkRgba({ 0, 0, 0, 1 }, scene.at(1));
CHECK_CLOSE(128.0f / 255.0f, scene.at(2).r, 0.01f);
CHECK_CLOSE(128.0f / 255.0f, scene.at(3).a, 0.01f);
}
TEST(AlphaAdjustmentChangesTransparency) {
AdjustScene scene;
scene.root.commands.run(Command::AdjustAlpha);
CHECK(!scene.root.adjustments.second.isVisible());
scene.setSlider(scene.root.adjustments.first, -0.5f);
scene.root.adjustments.accept();
CHECK_CLOSE(0.5f, scene.at(1).a, 0.01f);
CHECK_CLOSE(1.0f, scene.at(1).r, 0.02f);
}
TEST(AlphaAdjustmentLeavesAlphaLockedLayersAlone) {
AdjustScene scene;
scene.document.toggleAlphaLock();
scene.root.commands.run(Command::AdjustAlpha);
scene.setSlider(scene.root.adjustments.first, -0.5f);
scene.root.adjustments.accept();
CHECK_CLOSE(1.0f, scene.at(1).a, 0.01f);
}
TEST(EmptyLayerExplains) {
AdjustScene scene;
scene.document.addLayer("Empty");
scene.root.commands.run(Command::AdjustColorize);
CHECK(!scene.root.adjustments.isOpen());
CHECK_EQUAL(std::string("The layer is empty"), scene.root.lastNotice());
}
TEST(EditAdjustMenuListsEveryAdjustment) {
AdjustScene scene;
std::vector<std::string> texts;
for (const auto& item : scene.root.adjustImageMenu.items())
texts.push_back(item->entry().text);
CHECK(
texts ==
std::vector<std::string>({ "Brightness & exposure", "Hue & saturation", "Color balance", "Colorize", "Alpha" })
);
}
}