#include "CanvasContractFixtures.h" // Compositor 1.4's GPU canvas contracts on the CPU canvas: edits. namespace { QUuid insertNoise(ContractScene &scene, int width, int height, quint32 seed, QPointF centre, int alpha = 255) { const QImage image = noise(width, height, seed, alpha); scene.session.insert(ImportedImage(image, image, QStringLiteral("Noise")), centre); return scene.session.activeLayerID().value(); } QPainterPath ellipse(const QRectF &rect) { QPainterPath path; path.addEllipse(rect); return path; } bool finished(const std::function)> &start) { bool done = false; start([&done] { done = true; }); return QTest::qWaitFor([&done] { return done; }, 20000); } } class CanvasEditContractTests : public QObject { Q_OBJECT private slots: void aNewMaskRevealsTheWholeLayer_data(); void aNewMaskRevealsTheWholeLayer(); void movingPixelsShowAsTheirCommit_data(); void movingPixelsShowAsTheirCommit(); void aGradientInASelectionShowsAsItsCommit(); void aGradientThatCannotFillCancels(); void aLiveMaskFromFurtherDownShowsAsTheExport(); }; void CanvasEditContractTests::aNewMaskRevealsTheWholeLayer_data() { QTest::addColumn("rotation"); QTest::newRow("upright") << 0.0; QTest::newRow("turned") << 25.0; } // Swift's aNewMaskRevealsTheWholeLayer: one white pixel, stretched. void CanvasEditContractTests::aNewMaskRevealsTheWholeLayer() { QFETCH(double, rotation); ContractScene scene(80, 70, 80, 70); const QUuid id = insertNoise(scene, 30, 20, 2, QPointF(40, 35)); rewrite(scene.session, [&](ProjectSnapshot &snapshot) { record(snapshot, id).transform = LayerTransform{.origin = {10, 12}, .size = {38, 30}, .rotation = rotation}; }); scene.session.zoom(1); scene.session.selectLayer(id); const QImage before = scene.exported(); scene.session.addLayerMask(); QCOMPARE(scene.session.activeLayer().value().mask.value().asset.size(), QSize(1, 1)); const QImage after = scene.exported(); // Wherever the layer covered its pixel, nothing changes. int covered = 0; for (int y = 0; y < after.height(); ++y) { for (int x = 0; x < after.width(); ++x) { if (qAlpha(before.pixel(x, y)) < 255) continue; ++covered; QVERIFY2(ContractScene::gap(before.copy(x - 1, y - 1, 3, 3), after.copy(x - 1, y - 1, 3, 3)) <= 1, qPrintable(QStringLiteral("%1, %2").arg(x).arg(y))); } } QVERIFY(covered > 900); QVERIFY2(scene.gapToExport() <= 1, qPrintable(QString::number(scene.gapToExport()))); } void CanvasEditContractTests::movingPixelsShowAsTheirCommit_data() { QTest::addColumn("duplicate"); QTest::newRow("moving") << false; QTest::newRow("duplicating") << true; } // Swift's matchesWhileMovingPixels, on a masked layer at 60%. void CanvasEditContractTests::movingPixelsShowAsTheirCommit() { QFETCH(bool, duplicate); ContractScene scene(80, 60, 80, 60); const QUuid id = insertNoise(scene, 80, 60, 2, QPointF(40, 30)); rewrite(scene.session, [&](ProjectSnapshot &snapshot) { setMask(snapshot, id, rampMask(80, 60)); record(snapshot, id).opacity = 0.6; }); scene.session.zoom(1); scene.session.selectLayer(id); const QImage still = scene.documentShot(); scene.session.applySelection(ellipse(QRectF(20, 15, 30, 24)), SelectionMode::replace, QStringLiteral("Select")); QVERIFY(scene.session.beginPixelMove(duplicate)); scene.session.movePixels(QSizeF(9.4, 5.6)); const QImage moving = scene.documentShot(); QVERIFY(finished([&](std::function done) { scene.session.finishPixelMove(std::move(done)); })); const QImage moved = scene.documentShot(); // The ants run round the moved outline: left out. const QPainterPath ants = ContractScene::band(ellipse(QRectF(29, 21, 30, 24))); QVERIFY(ContractScene::gap(still, moving, ants) > 20); QVERIFY2(ContractScene::gap(moving, moved, ants) <= 1, qPrintable(QString::number(ContractScene::gap(moving, moved, ants)))); QVERIFY(scene.gapToExport(ants) <= 1); // A move leaves a hole; a duplicate leaves its pixels. const QRgb left = scene.exported().pixel(24, 27); QCOMPARE(qAlpha(left) == 0, !duplicate); } // Swift's matchesWhileDraggingAGradient: inside a selection, at 70%. void CanvasEditContractTests::aGradientInASelectionShowsAsItsCommit() { ContractScene scene(80, 60, 80, 60); insertNoise(scene, 80, 60, 2, QPointF(40, 30)); scene.session.zoom(1); const QImage still = scene.documentShot(); const QPainterPath outline = ellipse(QRectF(15, 10, 40, 30)); scene.session.applySelection(outline, SelectionMode::replace, QStringLiteral("Select")); GradientSettings settings; settings.opacity = 0.7; settings.style = GradientStyle::foregroundToBackground; scene.session.setGradientSettings(settings); scene.session.selectTool(NavigationTool::gradient); scene.session.beginGradient(QPointF(10, 30)); scene.session.moveGradient(std::nullopt, QPointF(70, 30)); scene.session.endGradientDrag(); const QImage pending = scene.documentShot(); QVERIFY(finished([&](std::function done) { scene.session.commitGradient(std::move(done)); })); const QImage committed = scene.documentShot(); // The ants, the gradient's line and its ends: overlays. QPainterPath line; line.moveTo(10, 30); line.lineTo(70, 30); const QPainterPath overlays = ContractScene::band(outline).united(ContractScene::band(line, 30)); QVERIFY(ContractScene::gap(still, pending, overlays) > 20); QVERIFY2(ContractScene::gap(pending, committed, overlays) <= 1, qPrintable(QString::number(ContractScene::gap(pending, committed, overlays)))); // Outside the selection the photo stays. QCOMPARE(scene.exported().pixel(3, 3), still.pixel(3, 3)); } // Swift's 5708ed3: a fill that fails cancels with the error. void CanvasEditContractTests::aGradientThatCannotFillCancels() { ContractScene scene(80, 60, 80, 60); insertNoise(scene, 80, 60, 2, QPointF(40, 30)); const QImage before = scene.exported(); const int count = scene.session.history.undoCount(); scene.session.selectTool(NavigationTool::gradient); scene.session.beginGradient(QPointF(10, 30)); QVERIFY(scene.session.gradientEdit()); scene.session.gradientEdit()->raster->pixelLimit = 100; scene.session.moveGradient(std::nullopt, QPointF(70, 30)); QVERIFY(!scene.session.gradientEdit() && scene.session.brushError().has_value()); QCOMPARE(scene.session.history.undoCount(), count); QCOMPARE(scene.exported(), before); } // Swift's matchesALiveMaskOutsideAClippingStack: a turned, distant source. void CanvasEditContractTests::aLiveMaskFromFurtherDownShowsAsTheExport() { ContractScene scene(80, 60, 80, 60); insertNoise(scene, 80, 60, 2, QPointF(40, 30)); const QUuid source = insertNoise(scene, 40, 30, 4, QPointF(35, 30), 140); rewrite(scene.session, [&](ProjectSnapshot &snapshot) { record(snapshot, source).transform.rotation = 25; }); insertNoise(scene, 16, 16, 5, QPointF(60, 20)); const QUuid target = insertNoise(scene, 50, 40, 9, QPointF(40, 30)); const QImage unlinked = scene.exported(); QVERIFY(scene.session.linkMask(source, target)); scene.session.zoom(1); QVERIFY(ContractScene::gap(unlinked, scene.exported()) > 20); QVERIFY2(scene.gapToExport() <= 1, qPrintable(QString::number(scene.gapToExport()))); } QTEST_MAIN(CanvasEditContractTests) #include "CanvasEditContractTests.moc"