Magic tool Object mode (Compositor 1.1.8)
This commit is contained in:
1 parent
78bfa2445d
commit
3d56688ce4
39 files changed
+1193
-134
No files matched your search
@@ -20,7 +20,7 @@ void CompositorUITestsLaunchTests::testLaunch()
|
||||
// The welcome is up: Swift's launch test waits for it.
|
||||
QVERIFY(window.findChild<QWidget *>("createCanvas")->isVisible());
|
||||
QCoreApplication::processEvents();
|
||||
QVERIFY(window.grab().save(QStringLiteral("LaunchScreen.png")));
|
||||
QVERIFY(window.grab().save(QCoreApplication::applicationDirPath() + QStringLiteral("/LaunchScreen.png")));
|
||||
}
|
||||
|
||||
void CompositorUITestsLaunchTests::testWindowKeepsItsMinimumSize()
|
||||
|
||||
@@ -141,7 +141,7 @@ void ContentViewStatusTests::everyToolHasItsHint()
|
||||
{NavigationTool::move, "Drag to move · Handles to resize · Circle to rotate · 1–0 layer opacity · Space to pan"},
|
||||
{NavigationTool::marquee, "Drag a rectangle · Shift add · Alt subtract · Shift again mid-drag square · Drag inside to move · Ctrl-drag moves pixels · Delete clears · Ctrl+D deselect"},
|
||||
{NavigationTool::lasso, "Drag to select · Drag inside to move · Shift add · Alt subtract · Delete clears · Alt+Backspace/Ctrl+Backspace fill · Ctrl+D deselect"},
|
||||
{NavigationTool::wand, "Click to select similar colors · Shift add · Alt subtract · Drag inside to move · Ctrl-drag moves pixels · Delete clears · Ctrl+D deselect"},
|
||||
{NavigationTool::wand, "Click to select similar colors · Tab for Object · Shift add · Alt subtract · Drag inside to move · Ctrl-drag moves pixels · Delete clears · Ctrl+D deselect"},
|
||||
{NavigationTool::crop, "Drag to crop · Enter apply · Escape cancel · Space to pan"},
|
||||
{NavigationTool::brush, "Drag to paint · [ ] size · Shift-[ ] hardness · 1–0 opacity · Escape cancel · Space to pan"},
|
||||
{NavigationTool::spotHealing, "Drag over blemishes to heal · [ ] size · Shift-[ ] hardness · Escape cancel · Space to pan"},
|
||||
@@ -159,6 +159,7 @@ void ContentViewStatusTests::everyToolHasItsHint()
|
||||
QCOMPARE(ContentView::hint(NavigationTool::move, ToolIconKind::eraser), ContentView::hint(NavigationTool::move));
|
||||
QCOMPARE(ContentView::hint(NavigationTool::lasso, ToolIconKind::ellipse), ContentView::hint(NavigationTool::lasso));
|
||||
QCOMPARE(ContentView::hint(NavigationTool::marquee, ToolIconKind::polygonal), ContentView::hint(NavigationTool::marquee));
|
||||
QCOMPARE(ContentView::hint(NavigationTool::lasso, ToolIconKind::object), ContentView::hint(NavigationTool::lasso));
|
||||
EditorSession session;
|
||||
session.createDocument(8, 8);
|
||||
ContentView view(session);
|
||||
@@ -166,6 +167,11 @@ void ContentViewStatusTests::everyToolHasItsHint()
|
||||
session.selectTool(tool);
|
||||
QCOMPARE(find<QLabel>(view, "activityStatus").text(), QString::fromUtf8(words));
|
||||
}
|
||||
// Magic's words follow its mode.
|
||||
session.selectTool(NavigationTool::wand);
|
||||
session.setWandMode(WandMode::object);
|
||||
QCOMPARE(find<QLabel>(view, "activityStatus").text(),
|
||||
QString("Click an object to select its outline · Tab for Wand · Shift add · Alt subtract · Drag inside to move · Ctrl-drag moves pixels · Delete clears · Ctrl+D deselect"));
|
||||
// The Shape's Shift follows its kind.
|
||||
session.selectTool(NavigationTool::shape);
|
||||
session.setShapeKind(ShapeKind::ellipse);
|
||||
|
||||
@@ -32,7 +32,7 @@ const std::pair<NavigationTool, const char *> labelled[] = {
|
||||
{NavigationTool::move, "Move / Transform (V)"},
|
||||
{NavigationTool::marquee, "Marquee (M)"},
|
||||
{NavigationTool::lasso, "Lasso (L)"},
|
||||
{NavigationTool::wand, "Magic Wand (W)"},
|
||||
{NavigationTool::wand, "Magic (W) · Tab switches Wand and Object"},
|
||||
{NavigationTool::crop, "Crop (C)"},
|
||||
{NavigationTool::brush, "Brush (B) · Eraser (E)"},
|
||||
{NavigationTool::spotHealing, "Spot Healing Brush (J)"},
|
||||
@@ -167,6 +167,13 @@ void ContentViewTests::theRailHoldsEveryToolInSwiftsOrder()
|
||||
QVERIFY(brush->grab().toImage() != painting);
|
||||
session.setBrushMode(BrushToolMode::paint);
|
||||
QCOMPARE(brush->grab().toImage(), painting);
|
||||
// Magic's shows the object icon in Object mode.
|
||||
QToolButton *const magic = buttons[3];
|
||||
const QImage colours = magic->grab().toImage();
|
||||
session.setWandMode(WandMode::object);
|
||||
QVERIFY(magic->grab().toImage() != colours);
|
||||
session.setWandMode(WandMode::wand);
|
||||
QCOMPARE(magic->grab().toImage(), colours);
|
||||
}
|
||||
|
||||
void ContentViewTests::theBarFollowsTheTool()
|
||||
@@ -236,7 +243,7 @@ void ContentViewTests::theBarFollowsTheTool()
|
||||
QCOMPARE(bar().title->text(), QString("Lasso"));
|
||||
session.selectTool(NavigationTool::wand);
|
||||
QVERIFY(bar().property("kept").toBool());
|
||||
QCOMPARE(bar().title->text(), QString("Magic Wand"));
|
||||
QCOMPARE(bar().title->text(), QString("Magic"));
|
||||
QCOMPARE(bar().height(), 42);
|
||||
QCOMPARE(bar().height(), 42);
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ private slots:
|
||||
void expandContractAndTheirAmountsNeedASelection();
|
||||
void deselectTheEmptyWordsAndAntialiasFollowTheSession();
|
||||
void theWandsControlsWriteItsSettings();
|
||||
void magicsModesAndObjectControlsWriteTheirSettings();
|
||||
};
|
||||
|
||||
void LassoControlsTests::theBarShowsTheToolsKindsAndSwitchesThem()
|
||||
@@ -63,7 +64,7 @@ void LassoControlsTests::theBarShowsTheToolsKindsAndSwitchesThem()
|
||||
QVERIFY(bar.button("marqueeEllipse").isChecked() && !bar.button("marqueeRectangle").isChecked());
|
||||
QVERIFY(find<QCheckBox>(bar.controls, "selectionAntialiased").isVisible());
|
||||
session.selectTool(NavigationTool::wand);
|
||||
QCOMPARE(bar.controls.title->text(), QString("Magic Wand"));
|
||||
QCOMPARE(bar.controls.title->text(), QString("Magic"));
|
||||
QVERIFY(!bar.button("marqueeRectangle").isVisible() && !bar.button("lassoFreehand").isVisible());
|
||||
QVERIFY(find<QCheckBox>(bar.controls, "selectionAntialiased").isVisible());
|
||||
QCOMPARE(bar.controls.height(), 42);
|
||||
@@ -254,6 +255,10 @@ void LassoControlsTests::theWandsControlsWriteItsSettings()
|
||||
QTest::keyClicks(&tolerance, "0");
|
||||
QTest::keyClick(&tolerance, Qt::Key_Down);
|
||||
QCOMPARE(session.wandSettings().tolerance, 0);
|
||||
// A field that stays above zero takes no sign.
|
||||
tolerance.selectAll();
|
||||
QTest::keyClicks(&tolerance, "-");
|
||||
QCOMPARE(tolerance.text(), QString("0"));
|
||||
QTest::keyClick(&tolerance, Qt::Key_Return);
|
||||
// The other three write their fields and read them back.
|
||||
size.setCurrentIndex(2);
|
||||
@@ -271,5 +276,97 @@ void LassoControlsTests::theWandsControlsWriteItsSettings()
|
||||
QVERIFY(bar.button("wandThisLayer").isChecked() && contiguous.isChecked());
|
||||
}
|
||||
|
||||
void LassoControlsTests::magicsModesAndObjectControlsWriteTheirSettings()
|
||||
{
|
||||
Bar bar;
|
||||
EditorSession &session = *bar.session;
|
||||
auto &edge = find<QLineEdit>(bar.controls, "objectEdge");
|
||||
auto &antialias = find<QCheckBox>(bar.controls, "selectionAntialiased");
|
||||
QToolButton &wand = bar.button("magicWandMode"), &object = bar.button("magicObjectMode");
|
||||
// The modes show with Magic alone, labelled as Swift's.
|
||||
QVERIFY(!wand.isVisible() && !object.isVisible());
|
||||
session.selectTool(NavigationTool::wand);
|
||||
QVERIFY(wand.isVisible() && object.isVisible() && wand.isChecked() && !object.isChecked());
|
||||
QCOMPARE(wand.text(), QString("Wand"));
|
||||
QCOMPARE(object.text(), QString("Object"));
|
||||
QCOMPARE(object.toolTip(), QString("Press Tab to switch between Wand and Object"));
|
||||
QCOMPARE(antialias.toolTip(), QString("Smooth selection edges; turn off for hard pixel edges"));
|
||||
QVERIFY(!edge.isVisible() && !bar.button("objectThisLayer").isVisible());
|
||||
// Magic takes no outline, so a choice only switches.
|
||||
session.beginLasso(QPointF(5, 5), SelectionMode::replace);
|
||||
QVERIFY(!session.lassoDraft());
|
||||
QTest::mouseClick(&object, Qt::LeftButton);
|
||||
QCOMPARE(session.wandMode(), WandMode::object);
|
||||
QVERIFY(object.isChecked() && !wand.isChecked());
|
||||
// Object shows its own controls and hides the wand's.
|
||||
QVERIFY(edge.isVisible() && bar.button("objectThisLayer").isVisible() && bar.button("objectAllLayers").isVisible());
|
||||
QVERIFY(find<QLabel>(bar.controls, "objectEdgeLabel").isVisible());
|
||||
QVERIFY(!find<QLineEdit>(bar.controls, "wandTolerance").isVisible() && !find<QCheckBox>(bar.controls, "wandContiguous").isVisible());
|
||||
QVERIFY(!bar.button("wandThisLayer").isVisible());
|
||||
QCOMPARE(antialias.toolTip(), QString("Smooth the detected object outline; turn off for the raw pixel mask"));
|
||||
QCOMPARE(bar.button("objectAllLayers").toolTip(), QString("Analyze the active layer only, or every visible layer as shown"));
|
||||
QCOMPARE(edge.toolTip(), QString("Positive values tighten the detected mask inward; negative values expand it outward"));
|
||||
QVERIFY(bar.button("objectAllLayers").isChecked() && !bar.button("objectThisLayer").isChecked());
|
||||
QCOMPARE(edge.text(), QString("0"));
|
||||
// The source writes its field alone.
|
||||
QTest::mouseClick(&bar.button("objectThisLayer"), Qt::LeftButton);
|
||||
QVERIFY(!session.objectSelectionSettings().sampleAllLayers);
|
||||
QTest::mouseClick(&bar.button("objectAllLayers"), Qt::LeftButton);
|
||||
QVERIFY(session.objectSelectionSettings().sampleAllLayers);
|
||||
// The edge takes a sign, clamped to ten either way.
|
||||
edge.setFocus();
|
||||
QTRY_VERIFY(edge.hasFocus());
|
||||
edge.selectAll();
|
||||
QTest::keyClicks(&edge, "-");
|
||||
QCOMPARE(session.objectSelectionSettings().edgeOffset, 0);
|
||||
// A sign leads the digits, never follows them.
|
||||
QTest::keyClicks(&edge, "-");
|
||||
QCOMPARE(edge.text(), QString("-"));
|
||||
QTest::keyClicks(&edge, "4");
|
||||
QCOMPARE(session.objectSelectionSettings().edgeOffset, -4);
|
||||
QTest::keyClicks(&edge, "0");
|
||||
QCOMPARE(session.objectSelectionSettings().edgeOffset, -10);
|
||||
edge.selectAll();
|
||||
QTest::keyClicks(&edge, "12");
|
||||
QCOMPARE(session.objectSelectionSettings().edgeOffset, 10);
|
||||
edge.selectAll();
|
||||
edge.insert(QStringLiteral("-99999999999999999999"));
|
||||
QCOMPARE(session.objectSelectionSettings().edgeOffset, -10);
|
||||
edge.selectAll();
|
||||
edge.insert(QStringLiteral("99999999999999999999"));
|
||||
QCOMPARE(session.objectSelectionSettings().edgeOffset, 10);
|
||||
edge.selectAll();
|
||||
QTest::keyClick(&edge, Qt::Key_Backspace);
|
||||
QTest::keyClicks(&edge, "a+");
|
||||
QCOMPARE(edge.text(), QString());
|
||||
QCOMPARE(session.objectSelectionSettings().edgeOffset, 10);
|
||||
QTest::keyClicks(&edge, "3");
|
||||
QTest::keyClick(&edge, Qt::Key_Down, Qt::ShiftModifier);
|
||||
QCOMPARE(session.objectSelectionSettings().edgeOffset, -7);
|
||||
QTest::keyClick(&edge, Qt::Key_Down, Qt::ShiftModifier);
|
||||
QCOMPARE(session.objectSelectionSettings().edgeOffset, -10);
|
||||
QTest::keyClick(&edge, Qt::Key_Up);
|
||||
QCOMPARE(session.objectSelectionSettings().edgeOffset, -9);
|
||||
QVERIFY(session.objectSelectionSettings().sampleAllLayers);
|
||||
QTest::keyClick(&edge, Qt::Key_Return);
|
||||
// The session's own changes show; Wand brings its controls back.
|
||||
ObjectSelectionSettings other;
|
||||
other.sampleAllLayers = false;
|
||||
other.edgeOffset = 6;
|
||||
session.setObjectSelectionSettings(other);
|
||||
QCOMPARE(edge.text(), QString("6"));
|
||||
QVERIFY(bar.button("objectThisLayer").isChecked());
|
||||
session.cycleToolMode();
|
||||
QVERIFY(wand.isChecked() && !edge.isVisible() && find<QLineEdit>(bar.controls, "wandTolerance").isVisible());
|
||||
session.setWandMode(WandMode::object);
|
||||
QVERIFY(object.isChecked() && !wand.isChecked());
|
||||
QTest::mouseClick(&object, Qt::LeftButton);
|
||||
QTest::mouseClick(&wand, Qt::LeftButton);
|
||||
QCOMPARE(session.wandMode(), WandMode::wand);
|
||||
// The Lasso shows neither.
|
||||
session.selectTool(NavigationTool::lasso);
|
||||
QVERIFY(!wand.isVisible() && !edge.isVisible());
|
||||
}
|
||||
|
||||
QTEST_MAIN(LassoControlsTests)
|
||||
#include "LassoControlsTests.moc"
|
||||
@@ -0,0 +1,475 @@
|
||||
#include "SelectionCanvasFixtures.h"
|
||||
#include "Document/BrushStroke.h"
|
||||
#include "Document/MagicWand.h"
|
||||
#include "Document/ObjectSelection.h"
|
||||
#include "Rendering/SelectionIcons.h"
|
||||
#include "UI/NativeLayerList.h"
|
||||
#include <QPainter>
|
||||
|
||||
// The Magic tool's Object mode: regions, edges, outlines, clicks.
|
||||
namespace {
|
||||
// Red discs on white, or flat white.
|
||||
QImage discs(const std::vector<std::pair<QPointF, double>> &shapes, QSize size = QSize(400, 300))
|
||||
{
|
||||
QImage image = BrushRaster::context(size.width(), size.height(), false);
|
||||
image.fill(Qt::white);
|
||||
QPainter painter(&image);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.setBrush(QColor(217, 26, 26));
|
||||
for (const auto &[centre, radius] : shapes)
|
||||
painter.drawEllipse(centre, radius, radius);
|
||||
return image;
|
||||
}
|
||||
|
||||
// Pixels an outline covers at half coverage or more.
|
||||
int covered(const std::optional<QPainterPath> &path, QSize size)
|
||||
{
|
||||
const QImage coverage = DocumentSelection{path.value(), false}.coverage(size.width(), size.height());
|
||||
int count = 0;
|
||||
for (int y = 0; y < size.height(); ++y) {
|
||||
for (int x = 0; x < size.width(); ++x)
|
||||
count += coverage.constScanLine(y)[x] >= 128;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
bool inside(const std::optional<QPainterPath> &path, QPointF point)
|
||||
{
|
||||
return path.value().contains(point);
|
||||
}
|
||||
|
||||
std::unique_ptr<EditorSession> withDisc()
|
||||
{
|
||||
auto session = std::make_unique<EditorSession>();
|
||||
session->createDocument(400, 300);
|
||||
const QImage image = discs({{QPointF(200, 150), 80}});
|
||||
session->insert(ImportedImage(image, image, QStringLiteral("Disc")));
|
||||
session->selectTool(NavigationTool::wand);
|
||||
session->setWandMode(WandMode::object);
|
||||
return session;
|
||||
}
|
||||
|
||||
void selectObject(EditorSession &session, QPointF point, SelectionMode mode)
|
||||
{
|
||||
bool done = false;
|
||||
session.selectObject(point, mode, [&done] { done = true; });
|
||||
QVERIFY(QTest::qWaitFor([&done] { return done; }, 20000));
|
||||
}
|
||||
|
||||
// Points of the path, a subpath's close included.
|
||||
int points(const QPainterPath &path)
|
||||
{
|
||||
return path.elementCount();
|
||||
}
|
||||
}
|
||||
|
||||
class ObjectSelectionTests : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void aRegionTakesTheEightConnectedCellsUnderTheClick();
|
||||
void edgesStepARingAPixelAndStopAtTen();
|
||||
void smoothingRoundsStairsAndKeepsSubpathsAndTheirRule();
|
||||
void theObjectUnderTheClickIsTracedAndTheBackgroundIsNone();
|
||||
void theEdgeOffsetTightensAndLoosensTheOutline();
|
||||
void theEdgeFollowsTheImageNotTheModelsGrid();
|
||||
void theSessionSelectsCombinesAndNamesTheStep();
|
||||
void theSessionRefusesAndReadsTheLayerItIsTold();
|
||||
void theModeSwitchesByTabAndWKeepsIt();
|
||||
void theCanvasClicksPicksAgainInsideAndShowsTheObjectCursor();
|
||||
};
|
||||
|
||||
void ObjectSelectionTests::aRegionTakesTheEightConnectedCellsUnderTheClick()
|
||||
{
|
||||
const std::vector<uchar> mask{
|
||||
1, 1, 0, 0, 1,
|
||||
0, 1, 0, 0, 1,
|
||||
0, 0, 1, 0, 0,
|
||||
1, 0, 0, 0, 1,
|
||||
};
|
||||
const std::vector<uchar> first = ObjectSelection::region(mask, 5, 4, 0, 0).value();
|
||||
QCOMPARE(first, (std::vector<uchar>{
|
||||
1, 1, 0, 0, 0,
|
||||
0, 1, 0, 0, 0,
|
||||
0, 0, 1, 0, 0,
|
||||
0, 0, 0, 0, 0,
|
||||
}));
|
||||
// One region from any cell, the diagonal included.
|
||||
QCOMPARE(ObjectSelection::region(mask, 5, 4, 2, 2).value(), first);
|
||||
QCOMPARE(ObjectSelection::region(mask, 5, 4, 4, 1).value(), (std::vector<uchar>{
|
||||
0, 0, 0, 0, 1,
|
||||
0, 0, 0, 0, 1,
|
||||
0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0,
|
||||
}));
|
||||
QCOMPARE(ObjectSelection::region(mask, 5, 4, 4, 3).value(), (std::vector<uchar>{
|
||||
0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 1,
|
||||
}));
|
||||
// Background holds no region.
|
||||
QVERIFY(!ObjectSelection::region(mask, 5, 4, 3, 0));
|
||||
}
|
||||
|
||||
void ObjectSelectionTests::edgesStepARingAPixelAndStopAtTen()
|
||||
{
|
||||
std::vector<uchar> block(49);
|
||||
for (int y = 2; y <= 4; ++y) {
|
||||
for (int x = 2; x <= 4; ++x)
|
||||
block[size_t(y * 7 + x)] = 255;
|
||||
}
|
||||
QCOMPARE(ObjectSelection::adjusted(block, 7, 7, 0), block);
|
||||
std::vector<uchar> centre(49);
|
||||
centre[24] = 255;
|
||||
QCOMPARE(ObjectSelection::adjusted(block, 7, 7, 1), centre);
|
||||
QCOMPARE(ObjectSelection::adjusted(block, 7, 7, 2), std::vector<uchar>(49));
|
||||
std::vector<uchar> grown(49);
|
||||
for (int y = 1; y <= 5; ++y) {
|
||||
for (int x = 1; x <= 5; ++x)
|
||||
grown[size_t(y * 7 + x)] = 255;
|
||||
}
|
||||
QCOMPARE(ObjectSelection::adjusted(block, 7, 7, -1), grown);
|
||||
// Past ten steps either way, ten.
|
||||
std::vector<uchar> dot(25 * 25);
|
||||
dot[12 * 25 + 12] = 255;
|
||||
const std::vector<uchar> far = ObjectSelection::adjusted(dot, 25, 25, -20);
|
||||
QCOMPARE(far, ObjectSelection::adjusted(dot, 25, 25, -10));
|
||||
QCOMPARE(std::count(far.begin(), far.end(), uchar(255)), 21 * 21);
|
||||
QCOMPARE(far[2 * 25 + 2], uchar(255));
|
||||
QCOMPARE(far[1 * 25 + 12], uchar(0));
|
||||
std::vector<uchar> full(25 * 25, 255);
|
||||
QCOMPARE(ObjectSelection::adjusted(far, 25, 25, 20), ObjectSelection::adjusted(far, 25, 25, 10));
|
||||
// The image's edge counts as inside: a full mask keeps.
|
||||
QCOMPARE(ObjectSelection::adjusted(full, 25, 25, 3), full);
|
||||
}
|
||||
|
||||
void ObjectSelectionTests::smoothingRoundsStairsAndKeepsSubpathsAndTheirRule()
|
||||
{
|
||||
// A staircase triangle and a holed square, traced.
|
||||
std::vector<uchar> mask(40 * 40);
|
||||
for (int y = 0; y < 40; ++y) {
|
||||
for (int x = 0; x < 40; ++x)
|
||||
mask[size_t(y * 40 + x)] = (x <= y && y < 20) || (x >= 24 && x < 36 && y >= 24 && y < 36 && !(x >= 28 && x < 32 && y >= 28 && y < 32)) ? 255 : 0;
|
||||
}
|
||||
const QPainterPath traced = MagicWand::outline(mask, 40, 40).value();
|
||||
QVERIFY(points(traced) > 40);
|
||||
const QPainterPath smooth = ObjectSelection::smoothed(traced);
|
||||
QCOMPARE(smooth.fillRule(), traced.fillRule());
|
||||
// Three subpaths, each simplified, then cut three times.
|
||||
const QList<QPolygonF> parts = smooth.toSubpathPolygons();
|
||||
QCOMPARE(parts.size(), 3);
|
||||
QCOMPARE(parts[0].size(), 3 * 8 + 1);
|
||||
QCOMPARE(parts[1].size(), 4 * 8 + 1);
|
||||
QCOMPARE(parts[2].size(), 4 * 8 + 1);
|
||||
// The stair is gone: the hypotenuse is straight.
|
||||
QVERIFY(smooth.contains(QPointF(8, 12)) && !smooth.contains(QPointF(12, 8)));
|
||||
// Corners round off; the hole stays a hole.
|
||||
QVERIFY(smooth.contains(QPointF(30, 25)) && !smooth.contains(QPointF(30, 30)));
|
||||
QVERIFY(!smooth.contains(QPointF(24.3, 24.3)));
|
||||
QVERIFY(traced.contains(QPointF(24.3, 24.3)));
|
||||
// By hand: the extreme breaks the loop; curves keep ends.
|
||||
QPainterPath open;
|
||||
open.moveTo(0, 0);
|
||||
for (const QPointF point : {QPointF(5, -1.3), QPointF(10, 0), QPointF(10, 10), QPointF(0, 10), QPointF(0, 5)})
|
||||
open.lineTo(point);
|
||||
open.moveTo(20, 0);
|
||||
open.lineTo(30, 0);
|
||||
open.cubicTo(30, 3, 30, 7, 30, 10);
|
||||
open.lineTo(20, 10);
|
||||
const QList<QPolygonF> built = ObjectSelection::smoothed(open).toSubpathPolygons();
|
||||
QCOMPARE(built.size(), 2);
|
||||
QCOMPARE(built[0].size(), 33);
|
||||
QCOMPARE(built[0].front(), QPointF(4.375, 0));
|
||||
QCOMPARE(built[1].size(), 33);
|
||||
QCOMPARE(built[1].front(), QPointF(24.375, 0));
|
||||
QCOMPARE(built[1][8], QPointF(30, 4.375));
|
||||
// A flat loop keeps its points; a closed pair goes.
|
||||
QPainterPath flat;
|
||||
flat.moveTo(40, 0);
|
||||
for (const QPointF point : {QPointF(45, 0), QPointF(50, 0), QPointF(45, 0.5)})
|
||||
flat.lineTo(point);
|
||||
flat.closeSubpath();
|
||||
flat.moveTo(60, 0);
|
||||
flat.lineTo(70, 0);
|
||||
flat.closeSubpath();
|
||||
const QList<QPolygonF> kept = ObjectSelection::smoothed(flat).toSubpathPolygons();
|
||||
QCOMPARE(kept.size(), 1);
|
||||
QCOMPARE(kept[0].size(), 33);
|
||||
// A subpath under three points is dropped.
|
||||
QPainterPath thin;
|
||||
thin.moveTo(0, 0);
|
||||
thin.lineTo(5, 5);
|
||||
QVERIFY(ObjectSelection::smoothed(thin).isEmpty());
|
||||
}
|
||||
|
||||
void ObjectSelectionTests::theObjectUnderTheClickIsTracedAndTheBackgroundIsNone()
|
||||
{
|
||||
const QImage pair = discs({{QPointF(110, 150), 60}, {QPointF(290, 150), 60}});
|
||||
const std::optional<QPainterPath> left = ObjectSelection::select(pair, QPointF(110, 150), 0, false);
|
||||
QVERIFY(inside(left, QPointF(110, 150)) && inside(left, QPointF(160, 150)));
|
||||
QVERIFY(!inside(left, QPointF(290, 150)) && !inside(left, QPointF(10, 10)) && !inside(left, QPointF(178, 150)));
|
||||
const std::optional<QPainterPath> right = ObjectSelection::select(pair, QPointF(290.7, 150.2), 0, false);
|
||||
QVERIFY(inside(right, QPointF(290, 150)) && !inside(right, QPointF(110, 150)));
|
||||
// The raw mask hugs the disc, within two percent.
|
||||
const int area = covered(left, pair.size());
|
||||
QVERIFY2(std::abs(area - M_PI * 60 * 60) < 0.02 * M_PI * 60 * 60, qPrintable(QString::number(area)));
|
||||
// Background, off the image, no number, or no subject: none.
|
||||
QVERIFY(!ObjectSelection::select(pair, QPointF(10, 10), 0, false));
|
||||
QVERIFY(!ObjectSelection::select(pair, QPointF(200, 150), 0, false));
|
||||
for (const QPointF point : {QPointF(-0.5, 10), QPointF(400, 10), QPointF(10, 300), QPointF(std::nan(""), 10), QPointF(10, INFINITY)})
|
||||
QVERIFY(!ObjectSelection::select(pair, point, 0, false));
|
||||
QVERIFY(!ObjectSelection::select(discs({}), QPointF(200, 150), 0, false));
|
||||
// The point floors: the last half pixel is inside.
|
||||
const QImage edges = discs({{QPointF(20, 150), 60}, {QPointF(380, 150), 60}});
|
||||
QVERIFY(inside(ObjectSelection::select(edges, QPointF(399.5, 150), 0, false), QPointF(390, 150)));
|
||||
QVERIFY(inside(ObjectSelection::select(edges, QPointF(0, 150), 0, false), QPointF(10, 150)));
|
||||
QVERIFY(!ObjectSelection::select(edges, QPointF(-0.5, 150), 0, false));
|
||||
// The cell row comes from the height.
|
||||
const QImage low = discs({{QPointF(800, 1000), 150}}, QSize(1600, 1200));
|
||||
QVERIFY(inside(ObjectSelection::select(low, QPointF(800, 1000), 0, false), QPointF(800, 1000)));
|
||||
// Smoothing keeps the object and drops the stairs.
|
||||
const std::optional<QPainterPath> smooth = ObjectSelection::select(pair, QPointF(110, 150), 0, true);
|
||||
QVERIFY(inside(smooth, QPointF(110, 150)) && !inside(smooth, QPointF(290, 150)));
|
||||
QVERIFY(points(smooth.value()) < points(left.value()));
|
||||
// Cutting corners shaves a convex outline a little.
|
||||
const int rounded = covered(smooth, pair.size());
|
||||
QVERIFY2(rounded < area && rounded > 0.97 * area, qPrintable(QString::number(rounded)));
|
||||
}
|
||||
|
||||
void ObjectSelectionTests::theEdgeOffsetTightensAndLoosensTheOutline()
|
||||
{
|
||||
const QImage disc = discs({{QPointF(200, 150), 80}});
|
||||
const int plain = covered(ObjectSelection::select(disc, QPointF(200, 150), 0, false), disc.size());
|
||||
const int tight = covered(ObjectSelection::select(disc, QPointF(200, 150), 4, false), disc.size());
|
||||
const int loose = covered(ObjectSelection::select(disc, QPointF(200, 150), -4, false), disc.size());
|
||||
// Square steps widen a round rim: 8 r k.
|
||||
const double ring = 8 * 80 * 4;
|
||||
QVERIFY2(std::abs(plain - tight - ring) < 0.05 * ring, qPrintable(QString::number(plain - tight)));
|
||||
QVERIFY2(std::abs(loose - plain - ring - 4 * 4 * 4) < 0.05 * ring, qPrintable(QString::number(loose - plain)));
|
||||
}
|
||||
|
||||
void ObjectSelectionTests::theEdgeFollowsTheImageNotTheModelsGrid()
|
||||
{
|
||||
// Grid cells five pixels wide; the image decides the edge.
|
||||
const QSize size(1600, 1200);
|
||||
const QImage disc = discs({{QPointF(800, 600), 320}}, size);
|
||||
const QImage coverage = DocumentSelection{ObjectSelection::select(disc, QPointF(800, 600), 0, false).value(), false}.coverage(size.width(), size.height());
|
||||
int wrong = 0, outside = 0;
|
||||
for (int y = 0; y < size.height(); ++y) {
|
||||
for (int x = 0; x < size.width(); ++x) {
|
||||
const bool truth = std::hypot(x + 0.5 - 800, y + 0.5 - 600) < 320;
|
||||
const bool chosen = coverage.constScanLine(y)[x] >= 128;
|
||||
wrong += truth != chosen;
|
||||
outside += chosen && !truth;
|
||||
}
|
||||
}
|
||||
// Measured: 190 wrong, 141 outside; the grid alone, 1682.
|
||||
QVERIFY2(wrong < 230 && outside > 70 && wrong - outside < 100, qPrintable(QStringLiteral("%1 %2").arg(wrong).arg(outside)));
|
||||
}
|
||||
|
||||
void ObjectSelectionTests::theSessionSelectsCombinesAndNamesTheStep()
|
||||
{
|
||||
const auto session = withDisc();
|
||||
bool done = false;
|
||||
session->selectObject(QPointF(200, 150), SelectionMode::replace, [&done] { done = true; });
|
||||
QVERIFY(session->isProjectBusy());
|
||||
QVERIFY(QTest::qWaitFor([&done] { return done; }, 20000));
|
||||
QVERIFY(!session->isProjectBusy());
|
||||
QCOMPARE(session->history.undoName(), QString("Object Selection"));
|
||||
QCOMPARE(coverage(*session, 200, 150), 255);
|
||||
QCOMPARE(coverage(*session, 10, 10), 0);
|
||||
// A traced outline is kept as traced, antialiased as set.
|
||||
QVERIFY(session->selection().value().antialiased);
|
||||
// Add and Subtract combine with what is there.
|
||||
session->applySelection(rectPath(QRectF(10, 10, 30, 30)), SelectionMode::replace, "Select");
|
||||
selectObject(*session, QPointF(200, 150), SelectionMode::add);
|
||||
QCOMPARE(session->history.undoName(), QString("Object Selection"));
|
||||
QCOMPARE(coverage(*session, 20, 20), 255);
|
||||
QCOMPARE(coverage(*session, 200, 150), 255);
|
||||
selectObject(*session, QPointF(200, 150), SelectionMode::subtract);
|
||||
QCOMPARE(coverage(*session, 20, 20), 255);
|
||||
QCOMPARE(coverage(*session, 200, 150), 0);
|
||||
// Background adds nothing, and New there deselects.
|
||||
const int steps = session->history.undoCount();
|
||||
selectObject(*session, QPointF(10, 290), SelectionMode::add);
|
||||
QCOMPARE(session->history.undoCount(), steps);
|
||||
QCOMPARE(coverage(*session, 20, 20), 255);
|
||||
selectObject(*session, QPointF(10, 290), SelectionMode::replace);
|
||||
QVERIFY(!session->selection());
|
||||
// The anti-alias setting reaches the selection.
|
||||
const auto whole = [](const QPainterPath &path) {
|
||||
for (int index = 0; index < path.elementCount(); ++index) {
|
||||
const QPainterPath::Element element = path.elementAt(index);
|
||||
if (element.x != std::round(element.x) || element.y != std::round(element.y))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
selectObject(*session, QPointF(200, 150), SelectionMode::replace);
|
||||
QVERIFY(!whole(session->selection().value().path));
|
||||
session->setSelectionAntialiased(false);
|
||||
selectObject(*session, QPointF(200, 150), SelectionMode::replace);
|
||||
QVERIFY(!session->selection().value().antialiased);
|
||||
// Without anti-alias the traced pixel edges stay.
|
||||
QVERIFY(whole(session->selection().value().path));
|
||||
// The document gone mid-run: the outline is dropped.
|
||||
done = false;
|
||||
session->selectObject(QPointF(200, 150), SelectionMode::replace, [&done] { done = true; });
|
||||
session->clearProject();
|
||||
QVERIFY(QTest::qWaitFor([&done] { return done; }, 20000));
|
||||
QVERIFY(!session->document() && !session->isProjectBusy());
|
||||
}
|
||||
|
||||
void ObjectSelectionTests::theSessionRefusesAndReadsTheLayerItIsTold()
|
||||
{
|
||||
const auto session = withDisc();
|
||||
session->selectAll();
|
||||
const int steps = session->history.undoCount();
|
||||
for (const QPointF point : {QPointF(-1, 10), QPointF(400, 10), QPointF(10, 300), QPointF(std::nan(""), 10)}) {
|
||||
selectObject(*session, point, SelectionMode::replace);
|
||||
QCOMPARE(coverage(*session, 10, 10), 255);
|
||||
}
|
||||
session->setIsProjectBusy(true);
|
||||
selectObject(*session, QPointF(200, 150), SelectionMode::replace);
|
||||
session->setIsProjectBusy(false);
|
||||
QVERIFY(session->beginSelectionMove());
|
||||
selectObject(*session, QPointF(200, 150), SelectionMode::replace);
|
||||
session->endSelectionMove();
|
||||
QCOMPARE(coverage(*session, 10, 10), 255);
|
||||
QCOMPARE(session->history.undoCount(), steps);
|
||||
// This Layer reads a blank layer: nothing there.
|
||||
session->addBlankLayer();
|
||||
ObjectSelectionSettings own = session->objectSelectionSettings();
|
||||
own.sampleAllLayers = false;
|
||||
session->setObjectSelectionSettings(own);
|
||||
selectObject(*session, QPointF(200, 150), SelectionMode::replace);
|
||||
QVERIFY(!session->selection());
|
||||
own.sampleAllLayers = true;
|
||||
session->setObjectSelectionSettings(own);
|
||||
selectObject(*session, QPointF(200, 150), SelectionMode::replace);
|
||||
QCOMPARE(coverage(*session, 200, 150), 255);
|
||||
// The edge offset reaches the trace, clamped to ten.
|
||||
const int plain = covered(session->selection().value().path, QSize(400, 300));
|
||||
own.edgeOffset = 50;
|
||||
session->setObjectSelectionSettings(own);
|
||||
selectObject(*session, QPointF(200, 150), SelectionMode::replace);
|
||||
const int clamped = covered(session->selection().value().path, QSize(400, 300));
|
||||
QCOMPARE(session->objectSelectionSettings().edgeOffset, 50);
|
||||
const QImage disc = discs({{QPointF(200, 150), 80}});
|
||||
QCOMPARE(clamped, covered(ObjectSelection::select(disc, QPointF(200, 150), 10, true), QSize(400, 300)));
|
||||
QVERIFY(clamped < plain);
|
||||
own.edgeOffset = -50;
|
||||
session->setObjectSelectionSettings(own);
|
||||
selectObject(*session, QPointF(200, 150), SelectionMode::replace);
|
||||
QCOMPARE(covered(session->selection().value().path, QSize(400, 300)), covered(ObjectSelection::select(disc, QPointF(200, 150), -10, true), QSize(400, 300)));
|
||||
}
|
||||
|
||||
void ObjectSelectionTests::theModeSwitchesByTabAndWKeepsIt()
|
||||
{
|
||||
EditorSession session;
|
||||
session.createDocument(20, 20);
|
||||
QCOMPARE(session.wandMode(), WandMode::wand);
|
||||
QCOMPARE(rawValue(WandMode::wand), QString("Wand"));
|
||||
QCOMPARE(rawValue(WandMode::object), QString("Object"));
|
||||
QSignalSpy changed(&session, &EditorSession::changed);
|
||||
session.setWandMode(WandMode::object);
|
||||
QCOMPARE(changed.count(), 1);
|
||||
ObjectSelectionSettings settings;
|
||||
QVERIFY(settings.sampleAllLayers && settings.edgeOffset == 0);
|
||||
settings.edgeOffset = -3;
|
||||
session.setObjectSelectionSettings(settings);
|
||||
QCOMPARE(changed.count(), 2);
|
||||
QCOMPARE(session.objectSelectionSettings(), settings);
|
||||
// Tab cycles the mode only under the Magic tool.
|
||||
session.selectTool(NavigationTool::lasso);
|
||||
session.cycleToolMode();
|
||||
QCOMPARE(session.wandMode(), WandMode::object);
|
||||
session.pressWandKey();
|
||||
QCOMPARE(session.tool(), NavigationTool::wand);
|
||||
QCOMPARE(session.wandMode(), WandMode::object);
|
||||
session.cycleToolMode();
|
||||
QCOMPARE(session.wandMode(), WandMode::wand);
|
||||
session.cycleToolMode();
|
||||
QCOMPARE(session.wandMode(), WandMode::object);
|
||||
QCOMPARE(label(NavigationTool::wand), QString("Magic (W) · Tab switches Wand and Object"));
|
||||
// The list's W picks the tool; a repeat changes nothing.
|
||||
NativeLayerList list(session);
|
||||
list.show();
|
||||
QVERIFY(QTest::qWaitForWindowExposed(&list));
|
||||
list.setFocus();
|
||||
QTRY_VERIFY(list.hasFocus());
|
||||
session.selectTool(NavigationTool::brush);
|
||||
QKeyEvent repeat(QEvent::KeyPress, Qt::Key_W, Qt::NoModifier, QStringLiteral("w"), true);
|
||||
QApplication::sendEvent(&list, &repeat);
|
||||
QCOMPARE(session.tool(), NavigationTool::brush);
|
||||
QTest::keyClick(&list, Qt::Key_W);
|
||||
QCOMPARE(session.tool(), NavigationTool::wand);
|
||||
QCOMPARE(session.wandMode(), WandMode::object);
|
||||
}
|
||||
|
||||
void ObjectSelectionTests::theCanvasClicksPicksAgainInsideAndShowsTheObjectCursor()
|
||||
{
|
||||
Canvas shown;
|
||||
EditorSession &session = shown.session;
|
||||
const QImage image = discs({{QPointF(200, 150), 80}});
|
||||
session.insert(ImportedImage(image, image, QStringLiteral("Disc")));
|
||||
session.selectTool(NavigationTool::wand);
|
||||
shown.canvas->synchronizeDisplay();
|
||||
const double ratio = shown.canvas->devicePixelRatio();
|
||||
shown.hover(QPointF(20, 20));
|
||||
QVERIFY(shown.shows(CanvasView::wandCursor(SelectionMode::replace, ratio)));
|
||||
// Tab on the canvas switches to Object and its cursor.
|
||||
shown.canvas->setFocus();
|
||||
QTRY_VERIFY(shown.canvas->hasFocus());
|
||||
QTest::keyClick(shown.canvas, Qt::Key_Tab);
|
||||
QCOMPARE(session.wandMode(), WandMode::object);
|
||||
shown.hover(QPointF(20, 20));
|
||||
QVERIFY(shown.shows(CanvasView::selectionCursor(SelectionIcon::objectSelection, SelectionMode::replace, ratio)));
|
||||
shown.hover(QPointF(20, 20), Qt::ShiftModifier);
|
||||
QVERIFY(shown.shows(CanvasView::selectionCursor(SelectionIcon::objectSelection, SelectionMode::add, ratio)));
|
||||
// A click traces the object under the document point.
|
||||
session.zoom(2);
|
||||
shown.canvas->synchronizeDisplay();
|
||||
shown.click(session.viewport.viewPoint(QPointF(260, 150), shown.documentSize()));
|
||||
QTRY_VERIFY_WITH_TIMEOUT(!session.isProjectBusy() && session.selection().has_value(), 20000);
|
||||
QCOMPARE(session.history.undoName(), QString("Object Selection"));
|
||||
QCOMPARE(shown.coverage(200, 150), 255);
|
||||
QCOMPARE(shown.coverage(20, 20), 0);
|
||||
session.zoom(1);
|
||||
shown.canvas->synchronizeDisplay();
|
||||
// A click inside a selection picks afresh, never deselects.
|
||||
session.selectAll();
|
||||
shown.canvas->synchronizeDisplay();
|
||||
shown.click(QPointF(200, 150));
|
||||
QTRY_VERIFY_WITH_TIMEOUT(!session.isProjectBusy() && shown.coverage(20, 20) == 0, 20000);
|
||||
QCOMPARE(shown.coverage(200, 150), 255);
|
||||
QCOMPARE(session.history.undoName(), QString("Object Selection"));
|
||||
// Shift adds the object to a box elsewhere.
|
||||
session.applySelection(rectPath(QRectF(10, 10, 20, 20)), SelectionMode::replace, "Select");
|
||||
shown.canvas->synchronizeDisplay();
|
||||
shown.click(QPointF(200, 150), Qt::ShiftModifier);
|
||||
QTRY_VERIFY_WITH_TIMEOUT(!session.isProjectBusy() && shown.coverage(200, 150) == 255, 20000);
|
||||
QCOMPARE(shown.coverage(20, 20), 255);
|
||||
// Wand mode's click matches colour again.
|
||||
session.setWandMode(WandMode::wand);
|
||||
session.deselect();
|
||||
shown.canvas->synchronizeDisplay();
|
||||
shown.click(QPointF(20, 20));
|
||||
QTRY_VERIFY_WITH_TIMEOUT(!session.isProjectBusy() && session.selection().has_value(), 20000);
|
||||
QCOMPARE(session.history.undoName(), QString("Magic Wand"));
|
||||
QCOMPARE(shown.coverage(20, 20), 255);
|
||||
QCOMPARE(shown.coverage(200, 150), 0);
|
||||
// W picks Magic, keeps the mode, and ignores a repeat.
|
||||
session.setWandMode(WandMode::object);
|
||||
session.selectTool(NavigationTool::brush);
|
||||
QKeyEvent repeat(QEvent::KeyPress, Qt::Key_W, Qt::NoModifier, QStringLiteral("w"), true);
|
||||
QApplication::sendEvent(shown.canvas, &repeat);
|
||||
QCOMPARE(session.tool(), NavigationTool::brush);
|
||||
QTest::keyClick(shown.canvas, Qt::Key_W);
|
||||
QCOMPARE(session.tool(), NavigationTool::wand);
|
||||
QCOMPARE(session.wandMode(), WandMode::object);
|
||||
}
|
||||
|
||||
QTEST_MAIN(ObjectSelectionTests)
|
||||
#include "ObjectSelectionTests.moc"
|
||||
@@ -79,6 +79,8 @@ inline QStringList described(const EditorSession &session)
|
||||
.arg(int(session.selectionAntialiased())).arg(session.selectionExpandAmount()).arg(session.selectionContractAmount());
|
||||
result << QStringLiteral("wand %1 %2 %3 %4").arg(session.wandSettings().tolerance).arg(int(session.wandSettings().sampleSize))
|
||||
.arg(int(session.wandSettings().contiguous)).arg(int(session.wandSettings().sampleAllLayers));
|
||||
result << QStringLiteral("magic %1 object %2 %3").arg(int(session.wandMode())).arg(int(session.objectSelectionSettings().sampleAllLayers))
|
||||
.arg(session.objectSelectionSettings().edgeOffset);
|
||||
if (session.lassoDraft().has_value()) {
|
||||
const LassoDraft &draft = session.lassoDraft().value();
|
||||
result << QStringLiteral("lasso %1 %2 points %3 cursor %4").arg(int(draft.kind)).arg(int(draft.mode)).arg(draft.points.size())
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "AddressSpaceLimit.h"
|
||||
#include "Document/ObjectSelection.h"
|
||||
#include "Document/BrushStroke.h"
|
||||
#include "Document/SubjectRemoval.h"
|
||||
#include "IO/ImageExporter.h"
|
||||
@@ -14,6 +15,7 @@ private slots:
|
||||
void initTestCase() { mallopt(M_MMAP_THRESHOLD, 64 * 1024); }
|
||||
void aPreviewThatFindsNoMemoryIsARenderError();
|
||||
void aModelRunThatFindsNoMemoryIsARenderError();
|
||||
void anObjectSelectionThatFindsNoMemoryIsARenderError();
|
||||
};
|
||||
|
||||
namespace {
|
||||
@@ -51,5 +53,22 @@ void SubjectRemovalFailureTests::aModelRunThatFindsNoMemoryIsARenderError()
|
||||
QVERIFY_THROWS_EXCEPTION(ExportError, SubjectRemoval::run(fresh, FilterSettings()));
|
||||
}
|
||||
|
||||
void SubjectRemovalFailureTests::anObjectSelectionThatFindsNoMemoryIsARenderError()
|
||||
{
|
||||
const QImage large = disc(6000, 4000);
|
||||
// The model's mask is kept, so only the selection allocates.
|
||||
QVERIFY(ObjectSelection::select(large, QPointF(3000, 2000), 0, false).has_value());
|
||||
malloc_trim(0);
|
||||
{
|
||||
// The model's grid finds no room.
|
||||
const AddressSpaceLimit limit(16ll * 1024);
|
||||
QVERIFY_THROWS_EXCEPTION(ExportError, ObjectSelection::select(large, QPointF(3000, 2000), 0, false));
|
||||
}
|
||||
malloc_trim(0);
|
||||
// The grid fits; the image-sized masks do not.
|
||||
const AddressSpaceLimit limit(8ll * 1024 * 1024);
|
||||
QVERIFY_THROWS_EXCEPTION(ExportError, ObjectSelection::select(large, QPointF(3000, 2000), 0, false));
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(SubjectRemovalFailureTests)
|
||||
#include "SubjectRemovalFailureTests.moc"
|
||||
@@ -1,4 +1,6 @@
|
||||
#include "Document/BrushStroke.h"
|
||||
#include "Document/EditorSession.h"
|
||||
#include "Document/ObjectSelection.h"
|
||||
#include "Document/SubjectRemoval.h"
|
||||
#include <QPainter>
|
||||
#include <QTemporaryDir>
|
||||
@@ -48,6 +50,23 @@ void SubjectRemovalModelTests::aModelThatFailsIsNamedAndTheNextUseLoadsAgain()
|
||||
said = failure(image);
|
||||
QVERIFY2(said.startsWith(QStringLiteral("Remove Background could not use its model: omaphoto/u2net.onnx is in no data folder (")), qPrintable(said));
|
||||
QVERIFY2(said.contains(empty.path()), qPrintable(said));
|
||||
// Object mode passes the model's failure on unchanged.
|
||||
try {
|
||||
ObjectSelection::select(image, QPointF(100, 75), 0, true);
|
||||
QFAIL("the object selection ran without a model");
|
||||
} catch (const SubjectRemovalError &error) {
|
||||
QVERIFY(error.kind == SubjectRemovalError::Kind::model);
|
||||
}
|
||||
// The session reports it and keeps the selection.
|
||||
EditorSession session;
|
||||
session.createDocument(200, 150);
|
||||
session.insert(ImportedImage(image, image, QStringLiteral("Disc")));
|
||||
session.selectAll();
|
||||
bool done = false;
|
||||
session.selectObject(QPointF(100, 75), SelectionMode::replace, [&done] { done = true; });
|
||||
QVERIFY(QTest::qWaitFor([&done] { return done; }, 20000));
|
||||
QVERIFY(session.brushError().value().startsWith(QStringLiteral("Remove Background could not use its model: ")));
|
||||
QCOMPARE(session.selection().value().path.boundingRect(), QRectF(0, 0, 200, 150));
|
||||
// Back in its folder, the next use loads it.
|
||||
if (folders.isEmpty())
|
||||
qunsetenv("XDG_DATA_DIRS");
|
||||
|
||||
@@ -67,9 +67,9 @@ void ToolIconTests::everyToolHasItsOwnIcon()
|
||||
icons << icon;
|
||||
}
|
||||
QCOMPARE(icons.size(), 15);
|
||||
// The Marquee's, the Lasso's and the brush's kinds are icons.
|
||||
// Marquee, Lasso, brush and Magic kinds are icons.
|
||||
for (const auto &[tool, kind] : {std::pair(NavigationTool::marquee, ToolIconKind::ellipse), std::pair(NavigationTool::lasso, ToolIconKind::polygonal),
|
||||
std::pair(NavigationTool::brush, ToolIconKind::eraser)}) {
|
||||
std::pair(NavigationTool::brush, ToolIconKind::eraser), std::pair(NavigationTool::wand, ToolIconKind::object)}) {
|
||||
const QImage icon = drawn(tool, Qt::white, 36, {0, 0}, kind);
|
||||
QVERIFY(inked(icon) > 60 && !icons.contains(icon));
|
||||
icons << icon;
|
||||
@@ -77,6 +77,7 @@ void ToolIconTests::everyToolHasItsOwnIcon()
|
||||
// A kind another tool lacks leaves its icon alone.
|
||||
QCOMPARE(drawn(NavigationTool::brush, Qt::white, 36, {0, 0}, ToolIconKind::ellipse), drawn(NavigationTool::brush, Qt::white));
|
||||
QCOMPARE(drawn(NavigationTool::move, Qt::white, 36, {0, 0}, ToolIconKind::eraser), drawn(NavigationTool::move, Qt::white));
|
||||
QCOMPARE(drawn(NavigationTool::lasso, Qt::white, 36, {0, 0}, ToolIconKind::object), drawn(NavigationTool::lasso, Qt::white));
|
||||
}
|
||||
|
||||
void ToolIconTests::anIconTakesTheColourItIsGivenAndNoOther()
|
||||
@@ -312,22 +313,22 @@ void ToolIconTests::anIconKeepsToTheCallersClip()
|
||||
void ToolIconTests::theDrawingsMatchTheirReference()
|
||||
{
|
||||
// Every stroke is pinned by the picture in the fixtures.
|
||||
QImage sheet(18 * 44 + 8, 2 * 52, QImage::Format_ARGB32_Premultiplied);
|
||||
QImage sheet(19 * 44 + 8, 2 * 52, QImage::Format_ARGB32_Premultiplied);
|
||||
sheet.fill(QColor(36, 36, 36));
|
||||
QPainter painter(&sheet);
|
||||
for (int index = 0; index < 15; ++index) {
|
||||
ToolIcons::paint(painter, tools[index], QPointF(8 + index * 44, 8), 36, QColor(235, 235, 235));
|
||||
ToolIcons::paint(painter, tools[index], QPointF(17 + index * 44, 66), 18, QColor(235, 235, 235));
|
||||
}
|
||||
// Three more: the ellipse Marquee, polygonal Lasso, the eraser.
|
||||
// Four more: ellipse Marquee, polygonal Lasso, eraser, Object mode.
|
||||
for (const auto &[index, tool, kind] : {std::tuple(15, NavigationTool::marquee, ToolIconKind::ellipse), std::tuple(16, NavigationTool::lasso, ToolIconKind::polygonal),
|
||||
std::tuple(17, NavigationTool::brush, ToolIconKind::eraser)}) {
|
||||
std::tuple(17, NavigationTool::brush, ToolIconKind::eraser), std::tuple(18, NavigationTool::wand, ToolIconKind::object)}) {
|
||||
ToolIcons::paint(painter, tool, QPointF(8 + index * 44, 8), 36, QColor(235, 235, 235), kind);
|
||||
ToolIcons::paint(painter, tool, QPointF(17 + index * 44, 66), 18, QColor(235, 235, 235), kind);
|
||||
}
|
||||
painter.end();
|
||||
// After a wanted change, look at this file; commit it.
|
||||
QVERIFY(sheet.save(QStringLiteral("ToolIcons.png")));
|
||||
QVERIFY(sheet.save(QCoreApplication::applicationDirPath() + QStringLiteral("/ToolIcons.png")));
|
||||
const QImage reference = QImage(QFINDTESTDATA("fixtures/ToolIcons.png")).convertToFormat(QImage::Format_ARGB32_Premultiplied);
|
||||
QCOMPARE(reference.size(), sheet.size());
|
||||
// Type's letters take the system's sans; the sheet's is DejaVu.
|
||||
|
||||
Vendored
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 21 KiB |
Reference in new issue
Block a user