Modules/Sketch2D/tests/SelectionTests.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

390 lines
14 KiB
C++

#include <algorithm>
#include "Selection.hpp"
#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 SelectViewport : 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 SelectIcons : public IconProvider {
public:
[[nodiscard]] ImageId icon(const std::string& name) const override { return { (ualni) name.size() + 1 }; }
};
struct SelectScene {
Document document;
Brush brush;
SelectViewport viewport;
SelectIcons icons;
Sketch2DRoot root{ document, viewport, brush, icons };
UiDriver driver{ root, { 1280, 800 } };
SelectScene() {
root.canvas.settings().viewStiffness = 1;
brush.apply(*findBrushPreset(BrushType::Round));
brush.setSize(60);
}
Vec2F screenOf(Vec2F world) { return root.canvas.windowRect().pos + document.worldToScreen(world); }
halnf alphaAt(int4 x, int4 y) { return document.sampleColor({ (halnf) x + 0.5f, (halnf) y + 0.5f }, false).a; }
};
}
SUITE(Selection) {
TEST(RectangleCoversExactlyItsPixels) {
Selection selection;
CHECK(!selection.isActive());
CHECK_EQUAL(255, selection.coverage(-1000, 5000));
selection.addRectangle(10, 20, 30, 40, SelectionMode::Replace);
CHECK(selection.isActive());
CHECK_EQUAL(255, selection.coverage(10, 20));
CHECK_EQUAL(255, selection.coverage(29, 39));
CHECK_EQUAL(0, selection.coverage(30, 39));
CHECK_EQUAL(0, selection.coverage(9, 25));
int4 left = 0, top = 0, right = 0, bottom = 0;
REQUIRE CHECK(selection.bounds(left, top, right, bottom));
CHECK_EQUAL(10, left);
CHECK_EQUAL(20, top);
CHECK_EQUAL(30, right);
CHECK_EQUAL(40, bottom);
}
TEST(AddSubtractAndReplaceCombine) {
Selection selection;
selection.addRectangle(0, 0, 10, 10, SelectionMode::Replace);
selection.addRectangle(20, 0, 30, 10, SelectionMode::Add);
CHECK_EQUAL(255, selection.coverage(5, 5));
CHECK_EQUAL(255, selection.coverage(25, 5));
selection.addRectangle(0, 0, 5, 10, SelectionMode::Subtract);
CHECK_EQUAL(0, selection.coverage(2, 5));
CHECK_EQUAL(255, selection.coverage(7, 5));
selection.addRectangle(100, 100, 110, 110, SelectionMode::Replace);
CHECK_EQUAL(0, selection.coverage(25, 5));
CHECK_EQUAL(255, selection.coverage(105, 105));
selection.addRectangle(100, 100, 110, 110, SelectionMode::Subtract);
CHECK(!selection.isActive());
}
TEST(SubtractWithoutASelectionSelectsNothing) {
Selection selection;
selection.addRectangle(0, 0, 10, 10, SelectionMode::Subtract);
CHECK(!selection.isActive());
}
TEST(EllipseIsAntialiased) {
Selection selection;
selection.addEllipse({ 50, 50 }, { 20, 10 }, SelectionMode::Replace);
CHECK_EQUAL(255, selection.coverage(50, 50));
CHECK_EQUAL(255, selection.coverage(68, 50));
CHECK_EQUAL(0, selection.coverage(71, 50));
CHECK_EQUAL(0, selection.coverage(32, 42));
bool partial = false;
for (int4 x = 30; x < 70; x++)
for (int4 y = 40; y < 60; y++) {
const uint1 value = selection.coverage(x, y);
if (value > 0 && value < 255) partial = true;
}
CHECK(partial);
}
TEST(PolygonFollowsItsEdges) {
Selection selection;
selection.addPolygon({ { 0, 0 }, { 40, 0 }, { 0, 40 } }, SelectionMode::Replace);
CHECK_EQUAL(255, selection.coverage(5, 5));
CHECK_EQUAL(0, selection.coverage(30, 30));
CHECK_EQUAL(255, selection.coverage(18, 18));
CHECK_EQUAL(0, selection.coverage(25, 25));
}
TEST(InvertFlipsCoverageEverywhere) {
Selection selection;
selection.addRectangle(0, 0, 10, 10, SelectionMode::Replace);
selection.invert();
CHECK(selection.isInverted());
CHECK_EQUAL(0, selection.coverage(5, 5));
CHECK_EQUAL(255, selection.coverage(500, -500));
selection.addRectangle(100, 100, 110, 110, SelectionMode::Subtract);
CHECK_EQUAL(0, selection.coverage(105, 105));
CHECK_EQUAL(255, selection.coverage(120, 105));
selection.addRectangle(0, 0, 5, 5, SelectionMode::Add);
CHECK_EQUAL(255, selection.coverage(2, 2));
CHECK_EQUAL(0, selection.coverage(7, 7));
}
TEST(OutlineTracesTheBorder) {
Selection selection;
selection.addRectangle(0, 0, 3, 2, SelectionMode::Replace);
CHECK_EQUAL((size_t) 10, selection.outline().size());
}
TEST(PaintingStaysInsideTheSelection) {
SelectScene scene;
scene.document.editSelection().addRectangle(0, 0, 200, 200, SelectionMode::Replace);
scene.driver.drag(scene.screenOf({ 150, 100 }), scene.screenOf({ 260, 100 }));
scene.document.settlePaint();
CHECK(scene.alphaAt(180, 100) > 0.5f);
CHECK_CLOSE(0.0f, scene.alphaAt(230, 100), 1e-6f);
CHECK_CLOSE(0.0f, scene.alphaAt(205, 100), 1e-6f);
}
TEST(BucketAndGradientRespectTheSelection) {
SelectScene scene;
Layer& layer = *scene.document.activeLayer();
for (int4 y = 0; y < 40; y++)
for (int4 x = 0; x < 40; x++) {
const bool wall = x < 2 || y < 2 || x >= 38 || y >= 38;
if (!wall) continue;
uint1* pixel = layer.tileAt({ 0, 0 })->pixels() + ((size_t) y * kTileSize + (size_t) x) * 4;
pixel[3] = 255;
}
scene.document.editSelection().addRectangle(0, 0, 20, 40, SelectionMode::Replace);
BucketSettings bucket;
bucket.spread = 0;
CHECK(scene.document.bucketFill({ 10, 10 }, { 1, 0, 0, 1 }, bucket) == BucketResult::Filled);
CHECK_CLOSE(1.0f, scene.alphaAt(10, 10), 1e-6f);
CHECK_CLOSE(0.0f, scene.alphaAt(30, 10), 1e-6f);
Document framed;
framed.editSelection().addRectangle(0, 0, 50, 50, SelectionMode::Replace);
CHECK(
framed.drawGradient({ 0, 0 }, { 50, 0 }, { 1, 0, 0, 1 }, { 0, 0, 1, 1 }, GradientSettings()) ==
GradientResult::Drawn
);
CHECK_CLOSE(1.0f, framed.sampleColor({ 10.5f, 10.5f }, false).a, 1e-6f);
CHECK_CLOSE(0.0f, framed.sampleColor({ 60.5f, 10.5f }, false).a, 1e-6f);
}
TEST(RectangleToolSelectsAndUndoes) {
SelectScene scene;
scene.root.commands.run(Command::ToolRectangleSelect);
CHECK(scene.root.canvas.tool() == CanvasTool::Marquee);
CHECK(scene.root.toolStrip.button("rectselect").isSelected());
const size_t revision = scene.document.history().revision();
scene.driver.drag(scene.screenOf({ 50, 50 }), scene.screenOf({ 150, 120 }));
CHECK(scene.document.hasSelection());
CHECK_EQUAL(255, scene.document.selection().coverage(100, 100));
CHECK_EQUAL(0, scene.document.selection().coverage(160, 100));
CHECK_EQUAL(revision + 1, scene.document.history().revision());
scene.root.commands.run(Command::Undo);
CHECK(!scene.document.hasSelection());
scene.root.commands.run(Command::Redo);
CHECK(scene.document.hasSelection());
}
TEST(ShiftAddsAndAltSubtracts) {
SelectScene scene;
scene.root.commands.run(Command::ToolRectangleSelect);
scene.driver.drag(scene.screenOf({ 50, 50 }), scene.screenOf({ 100, 100 }));
scene.driver.moveTo(scene.screenOf({ 200, 50 }));
scene.driver.press(PointerButton::Primary, Modifiers{ true, false, false });
scene.driver.moveTo(scene.screenOf({ 250, 100 }));
scene.driver.release();
CHECK_EQUAL(255, scene.document.selection().coverage(75, 75));
CHECK_EQUAL(255, scene.document.selection().coverage(225, 75));
scene.driver.moveTo(scene.screenOf({ 40, 40 }));
scene.driver.press(PointerButton::Primary, Modifiers{ false, false, true });
scene.driver.moveTo(scene.screenOf({ 75, 110 }));
scene.driver.release();
CHECK_EQUAL(0, scene.document.selection().coverage(60, 75));
CHECK_EQUAL(255, scene.document.selection().coverage(90, 75));
}
TEST(EllipseOptionSelectsAnEllipse) {
SelectScene scene;
scene.root.canvas.settings().selection.marquee = MarqueeShape::Ellipse;
scene.root.commands.run(Command::ToolRectangleSelect);
scene.driver.drag(scene.screenOf({ 0, 0 }), scene.screenOf({ 100, 100 }));
CHECK_EQUAL(255, scene.document.selection().coverage(50, 50));
CHECK_EQUAL(0, scene.document.selection().coverage(3, 3));
}
TEST(FreehandLassoFollowsThePath) {
SelectScene scene;
scene.root.commands.run(Command::ToolLasso);
CHECK(scene.root.canvas.tool() == CanvasTool::Lasso);
scene.driver.moveTo(scene.screenOf({ 0, 0 }));
scene.driver.press();
scene.driver.moveTo(scene.screenOf({ 100, 0 }));
scene.driver.moveTo(scene.screenOf({ 0, 100 }));
scene.driver.release();
CHECK_EQUAL(255, scene.document.selection().coverage(20, 20));
CHECK_EQUAL(0, scene.document.selection().coverage(80, 80));
}
TEST(PolygonLassoClosesOnTheFirstPointOrEnter) {
SelectScene scene;
scene.root.canvas.settings().selection.lasso = LassoShape::Polygon;
scene.root.commands.run(Command::ToolLasso);
scene.driver.click(scene.screenOf({ 0, 0 }));
scene.driver.click(scene.screenOf({ 100, 0 }));
scene.driver.click(scene.screenOf({ 100, 100 }));
CHECK_EQUAL((size_t) 3, scene.root.canvas.polygonPoints().size());
CHECK(!scene.document.hasSelection());
scene.driver.click(scene.screenOf({ 2, 2 }));
CHECK(scene.root.canvas.polygonPoints().empty());
CHECK_EQUAL(255, scene.document.selection().coverage(80, 20));
CHECK_EQUAL(0, scene.document.selection().coverage(20, 80));
scene.root.commands.run(Command::Deselect);
scene.driver.click(scene.screenOf({ 200, 0 }));
scene.driver.click(scene.screenOf({ 300, 0 }));
scene.driver.click(scene.screenOf({ 300, 100 }));
scene.driver.key(InputID::ENTER);
CHECK_EQUAL(255, scene.document.selection().coverage(280, 20));
}
TEST(ClickDeselectsWhenEnabled) {
SelectScene scene;
scene.root.commands.run(Command::ToolRectangleSelect);
scene.driver.drag(scene.screenOf({ 50, 50 }), scene.screenOf({ 100, 100 }));
scene.root.canvas.settings().selection.clickToDeselect = false;
scene.driver.click(scene.screenOf({ 300, 300 }));
CHECK(scene.document.hasSelection());
scene.root.canvas.settings().selection.clickToDeselect = true;
scene.driver.click(scene.screenOf({ 300, 300 }));
CHECK(!scene.document.hasSelection());
}
TEST(SelectAllInvertDeselectAndEscape) {
SelectScene scene;
scene.driver.drag(scene.screenOf({ 100, 100 }), scene.screenOf({ 200, 100 }));
scene.document.settlePaint();
scene.root.commands.run(Command::SelectAllAsRectangle);
CHECK(scene.document.hasSelection());
CHECK_EQUAL(255, scene.document.selection().coverage(150, 100));
scene.root.commands.run(Command::InvertSelection);
CHECK(scene.document.selection().isInverted());
CHECK_EQUAL(0, scene.document.selection().coverage(150, 100));
scene.driver.key(InputID::ESCAPE);
CHECK(!scene.document.hasSelection());
scene.root.commands.run(Command::Undo);
CHECK(scene.document.selection().isInverted());
}
TEST(SelectAllHugsTheActiveLayer) {
SelectScene scene;
Layer& layer = *scene.document.activeLayer();
for (int4 y = 10; y < 30; y++)
for (int4 x = 10; x < 30; x++) {
const bool corner = x < 15 && y < 15;
uint1* pixel = layer.tileAt({ 0, 0 })->pixels() + ((size_t) y * kTileSize + (size_t) x) * 4;
pixel[3] = corner ? 0 : (x == 29 ? 128 : 255);
}
Layer& other = *scene.document.addLayer("Other");
uint1* far = other.tileAt({ 2, 2 })->pixels();
far[3] = 255;
scene.document.setActiveLayer(&layer);
scene.root.commands.run(Command::SelectAllAsRectangle);
int4 left = 0, top = 0, right = 0, bottom = 0;
REQUIRE CHECK(scene.document.selection().bounds(left, top, right, bottom));
CHECK_EQUAL(10, left);
CHECK_EQUAL(10, top);
CHECK_EQUAL(30, right);
CHECK_EQUAL(30, bottom);
CHECK_EQUAL(255, scene.document.selection().coverage(11, 11));
scene.root.commands.run(Command::SelectAllAsLasso);
CHECK_EQUAL(0, scene.document.selection().coverage(11, 11));
CHECK_EQUAL(255, scene.document.selection().coverage(20, 20));
CHECK_EQUAL(128, scene.document.selection().coverage(29, 20));
CHECK_EQUAL(0, scene.document.selection().coverage(31, 20));
scene.root.commands.run(Command::Undo);
CHECK_EQUAL(255, scene.document.selection().coverage(11, 11));
scene.document.setActiveLayer(&other);
scene.root.commands.run(Command::Deselect);
scene.document.addLayer("Empty");
scene.root.commands.run(Command::SelectAllAsLasso);
CHECK_EQUAL(std::string("Nothing to select"), scene.root.lastNotice());
}
TEST(SelectCanvasFrameUsesTheFrame) {
SelectScene scene;
scene.root.commands.run(Command::SelectCanvasFrame);
CHECK(!scene.document.hasSelection());
scene.document.setInfinite(false);
scene.document.setFrame(0, 0, 300, 200);
scene.root.commands.run(Command::SelectCanvasFrame);
int4 left = 0, top = 0, right = 0, bottom = 0;
REQUIRE CHECK(scene.document.selection().bounds(left, top, right, bottom));
CHECK_EQUAL(300, right);
CHECK_EQUAL(200, bottom);
}
TEST(MarchingAntsAreDrawn) {
SelectScene scene;
scene.document.editSelection().addRectangle(10, 10, 20, 20, SelectionMode::Replace);
scene.root.canvas.requestPaint();
scene.driver.settle();
size_t dark = 0;
size_t light = 0;
for (const PaintCommand& command : scene.driver.paint().commands()) {
if (command.kind != PaintCommand::Kind::Line) continue;
if (command.color.r < 0.1f && command.color.a > 0.8f) dark++;
if (command.color.r > 0.9f && command.color.a > 0.8f) light++;
}
CHECK_EQUAL((size_t) 40, dark);
CHECK(light > 0);
CHECK(light < dark);
}
TEST(SelectMenuAndToolbarButton) {
SelectScene scene;
scene.driver.click(scene.root.toolBar.select);
CHECK(scene.root.selectMenu.isOpen());
std::vector<std::string> texts;
for (const auto& item : scene.root.selectMenu.items())
texts.push_back(item->entry().text);
for (const char* text : { "Select all as rectangle",
"Select canvas frame",
"Invert selection",
"Deselect",
"Rectangle selection tool",
"Lasso selection tool",
"Ellipse",
"Polygon lasso" })
CHECK(std::find(texts.begin(), texts.end(), text) != texts.end());
}
TEST(TabletQuickMenuHasTheSelectMenu) {
Document document;
Brush brush;
SelectViewport viewport;
SelectIcons 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 == "Select" && item->entry().submenu == &classic.selectMenu) found = true;
CHECK(found);
}
TEST(TabletToolsMenuHasSelectionTools) {
Document document;
Brush brush;
SelectViewport viewport;
SelectIcons icons;
Sketch2DRoot classic{ document, viewport, brush, icons };
StudioRoot studio{ classic, brush, icons };
UiDriver driver{ studio, { 1280, 800 } };
studio.useTool("lasso");
CHECK(classic.canvas.tool() == CanvasTool::Lasso);
}
}