#include "BrushFixtures.h" #include "SelectionFixtures.h" #include "SessionFixtures.h" #include "Rendering/EditorCanvas.h" #include "Rendering/RasterSnapshot.h" #include // Swift 1.2.11: a mask painted anywhere on the canvas grows. namespace { // A blue square on a wide canvas, its mask chosen. std::unique_ptr masked(bool reveal, QSize canvas = QSize(100, 60)) { auto session = std::make_unique(); session->createDocument(canvas.width(), canvas.height(), true); QImage blue = BrushRaster::context(20, 20, false); blue.fill(Qt::blue); session->addPixelLayer(blue, QPointF(20, 20), QStringLiteral("Blue"), QStringLiteral("Add")); session->addLayerMask(reveal); session->selectLayerTarget(session->activeLayerID().value(), true); session->selectTool(NavigationTool::brush); session->setBrushSettings(brush(6, 1, 0, 0, 0)); return session; } // The mask's value at a document point, through its placement. int maskAt(const EditorSession &session, QPointF point) { const LayerMask mask = session.activeLayer().value().mask.value(); const LayerTransform placement = mask.placement.value(); const QImage image = mask.asset.image(); const QPointF pixel = BrushRaster::pixelToDocument(placement, image.width(), image.height()).inverted().map(point); return qGray(image.pixel(int(pixel.x()), int(pixel.y()))); } } class MaskPaintAnywhereTests : public QObject { Q_OBJECT private slots: void theBrushGrowsAMaskPastItsLayer(); void newAreaStartsAsTheMasksBackground(); void gradientsAndFillsGrowAMaskToo(); void otherToolsStayWithinTheMask(); void aSolidPlacedMaskGetsAPixelPerDocumentPixel(); void aSnapshotAndACommitFillWithTheBackground(); void theCanvasDrawsAGrownStrokeInPlace(); void uncoveredCornersKeepTheBackground(); void aGrownPlacedMaskMapsItsTilesUnderEffects(); }; void MaskPaintAnywhereTests::theBrushGrowsAMaskPastItsLayer() { const auto session = masked(true); const QUuid id = session->activeLayerID().value(); session->beginBrush(QPointF(70, 30)); session->continueBrush(QPointF(80, 30)); // The stroke's grid reaches the canvas, not just the layer. QVERIFY(session->brushStroke()->width >= 100); session->finishBrush(); const LayerMask mask = layerWith(*session, id).mask.value(); // Grown, it keeps its place on the document, still linked. QVERIFY(mask.placement); QVERIFY(mask.isLinked); QVERIFY(mask.asset.size().width() > 20); QCOMPARE(maskAt(*session, QPointF(75, 30)), 0); QCOMPARE(maskAt(*session, QPointF(30, 30)), 255); // Untouched in a touched tile: the background, white. QCOMPARE(maskAt(*session, QPointF(90, 50)), 255); // A linked mask still moves with its layer. const QPointF before = mask.placement.value().origin; session->selectTool(NavigationTool::move); session->nudgeLayer(5, 0); QCOMPARE(layerWith(*session, id).mask.value().placement.value().origin, before + QPointF(5, 0)); } void MaskPaintAnywhereTests::newAreaStartsAsTheMasksBackground() { // Hidden everywhere: new area hides, only the paint shows. const auto session = masked(false); session->setMaskPaintWhite(true); session->setBrushSettings(brush(6, 1, 1, 1, 1)); session->beginBrush(QPointF(70, 30)); session->continueBrush(QPointF(80, 30)); session->finishBrush(); QCOMPARE(maskAt(*session, QPointF(75, 30)), 255); QCOMPARE(maskAt(*session, QPointF(90, 50)), 0); QCOMPARE(maskAt(*session, QPointF(30, 30)), 0); } void MaskPaintAnywhereTests::gradientsAndFillsGrowAMaskToo() { const auto gradient = masked(true); gradient->selectTool(NavigationTool::gradient); gradient->beginGradient(QPointF(0, 30)); gradient->moveGradient(std::nullopt, QPointF(100, 30)); bool done = false; gradient->commitGradient([&] { done = true; }); QVERIFY(QTest::qWaitFor([&] { return done; }, 5000)); QVERIFY(gradient->activeLayer().value().mask.value().placement); QVERIFY(gradient->activeLayer().value().mask.value().asset.size().width() >= 100); // The Smear paints within the mask's own grid. const auto fill = masked(true); fill->applySelection(rectPath(QRectF(60, 10, 30, 30)), SelectionMode::replace, QStringLiteral("Select")); fill->selectLayerTarget(fill->activeLayerID().value(), true); bool filled = false; fill->fillSelection(EditorSession::FillSource::foreground, [&] { filled = true; }); QVERIFY(QTest::qWaitFor([&] { return filled; }, 5000)); QCOMPARE(maskAt(*fill, QPointF(75, 25)), 0); QCOMPARE(maskAt(*fill, QPointF(30, 30)), 255); } void MaskPaintAnywhereTests::otherToolsStayWithinTheMask() { // The Smear's blur paints within the mask's own grid. const auto session = masked(true); session->selectTool(NavigationTool::blur); session->setBlurMode(BlurToolMode::blur); session->beginBrush(QPointF(30, 30)); QVERIFY(session->brushStroke() && session->brushStroke()->isBlur); QCOMPARE(session->brushStroke()->width, 20); session->cancelBrush(); } void MaskPaintAnywhereTests::aSolidPlacedMaskGetsAPixelPerDocumentPixel() { // Two-pixel masks are solid; this placement is 20 by 10. const auto session = masked(true); const QUuid id = session->activeLayerID().value(); rewrite(*session, [&](ProjectSnapshot &snapshot) { ProjectLayerRecord &layer = record(snapshot, id); QImage white = BrushRaster::context(2, 2, true); white.fill(255); snapshot.masks.insert_or_assign(id, ImportedImage(white, white, QStringLiteral("Mask"))); layer.maskPlacement = LayerTransform{.origin = QPointF(20, 20), .size = QSizeF(20, 10)}; layer.maskLinked = false; }); session->selectLayerTarget(id, true); QCOMPARE(layerWith(*session, id).mask.value().asset.size(), QSize(2, 2)); // Without growth the grid is the box, a pixel each. session->selectTool(NavigationTool::blur); session->setBlurMode(BlurToolMode::blur); session->beginBrush(QPointF(25, 25)); QCOMPARE(session->brushStroke()->width, 20); QCOMPARE(session->brushStroke()->height, 10); session->cancelBrush(); // The brush grows it, still a pixel per document pixel. session->selectTool(NavigationTool::brush); session->beginBrush(QPointF(30, 25)); QCOMPARE(session->brushStroke()->width, 100); QCOMPARE(session->brushStroke()->height, 60); session->finishBrush(); QCOMPARE(maskAt(*session, QPointF(30, 25)), 0); QCOMPARE(maskAt(*session, QPointF(22, 22)), 255); } void MaskPaintAnywhereTests::aSnapshotAndACommitFillWithTheBackground() { // Past its base a mask snapshot is its fill. QImage base = BrushRaster::context(4, 4, true); base.fill(255); const auto snapshot = RasterSnapshot::replacing(ImportedImage(base, base, QStringLiteral("Mask")), QRectF(2, 2, 4, 4), {}, QRectF(0, 0, 8, 8), true, 0); const QImage image = snapshot->makeImage(); QCOMPARE(qGray(image.pixel(0, 0)), 0); QCOMPARE(qGray(image.pixel(3, 3)), 255); const BrushCommit::Output output = BrushCommit::render(BrushCommit::Input{8, 8, ImportedImage(base, base, QStringLiteral("Mask")), {}, true, QStringLiteral("Mask"), QRectF(2, 2, 4, 4), 0}); QCOMPARE(qGray(output.asset.image().pixel(0, 0)), 0); QCOMPARE(qGray(output.asset.image().pixel(3, 3)), 255); } void MaskPaintAnywhereTests::theCanvasDrawsAGrownStrokeInPlace() { const auto session = masked(true); CanvasView canvas(*session); canvas.resize(100, 60); session->viewport.resize(QSizeF(100, 60), 1, QSizeF(100, 60)); session->zoom(1); session->beginBrush(QPointF(70, 30)); session->continueBrush(QPointF(80, 30)); const QImage shot = canvas.grab().toImage(); const auto at = [&](QPointF point) { return shot.pixelColor(session->viewport.viewPoint(point, QSizeF(100, 60)).toPoint()); }; // Mid-stroke the layer stays where it is, whole. QCOMPARE(at(QPointF(21.5, 21.5)), QColor(Qt::blue)); QCOMPARE(at(QPointF(38.5, 38.5)), QColor(Qt::blue)); QVERIFY(at(QPointF(45.5, 30.5)) != QColor(Qt::blue)); } void MaskPaintAnywhereTests::uncoveredCornersKeepTheBackground() { // Tiles apart: the grown box holds corners no tile covers. const auto brushed = masked(false, QSize(600, 400)); brushed->setMaskPaintWhite(true); brushed->setBrushSettings(brush(6, 1, 1, 1, 1)); brushed->beginBrush(QPointF(500, 350)); brushed->continueBrush(QPointF(510, 350)); brushed->finishBrush(); QCOMPARE(maskAt(*brushed, QPointF(505, 350)), 255); QCOMPARE(maskAt(*brushed, QPointF(300, 100)), 0); // A fill, committed off the thread, keeps it too. const auto filled = masked(false, QSize(600, 400)); filled->applySelection(rectPath(QRectF(500, 340, 20, 20)), SelectionMode::replace, QStringLiteral("Select")); filled->selectLayerTarget(filled->activeLayerID().value(), true); filled->setMaskPaintWhite(true); bool done = false; filled->fillSelection(EditorSession::FillSource::foreground, [&] { done = true; }); QVERIFY(QTest::qWaitFor([&] { return done; }, 5000)); QCOMPARE(maskAt(*filled, QPointF(510, 350)), 255); QCOMPARE(maskAt(*filled, QPointF(300, 100)), 0); // Revealing everywhere, the corner stays white. const auto revealed = masked(true, QSize(600, 400)); revealed->applySelection(rectPath(QRectF(500, 340, 20, 20)), SelectionMode::replace, QStringLiteral("Select")); revealed->selectLayerTarget(revealed->activeLayerID().value(), true); done = false; revealed->fillSelection(EditorSession::FillSource::foreground, [&] { done = true; }); QVERIFY(QTest::qWaitFor([&] { return done; }, 5000)); QCOMPARE(maskAt(*revealed, QPointF(510, 350)), 0); QCOMPARE(maskAt(*revealed, QPointF(300, 100)), 255); } void MaskPaintAnywhereTests::aGrownPlacedMaskMapsItsTilesUnderEffects() { // A wide layer; its solid placed mask grows wide. auto session = std::make_unique(); session->createDocument(1200, 100, true); QImage blue = BrushRaster::context(200, 40, false); blue.fill(Qt::blue); session->addPixelLayer(blue, QPointF(100, 30), QStringLiteral("Blue"), QStringLiteral("Add")); const QUuid id = session->activeLayerID().value(); LayerEffects effects; effects.stroke = StrokeEffect{.size = 2, .red = 1, .green = 1, .blue = 0, .opacity = 1}; session->setEffects(effects); session->addLayerMask(true); rewrite(*session, [&](ProjectSnapshot &snapshot) { ProjectLayerRecord &layer = record(snapshot, id); layer.maskPlacement = layer.transform; layer.maskLinked = false; }); session->selectLayerTarget(id, true); session->selectTool(NavigationTool::brush); session->setBrushSettings(brush(10, 1, 0, 0, 0)); CanvasView canvas(*session); canvas.resize(1200, 100); session->viewport.resize(QSizeF(1200, 100), 1, QSizeF(1200, 100)); session->zoom(1); // A first pass at the left end, then the right. session->beginBrush(QPointF(110, 50)); canvas.grab(); session->continueBrush(QPointF(110, 51)); canvas.grab(); session->continueBrush(QPointF(280, 50)); const QImage shot = canvas.grab().toImage(); const auto at = [&](QPointF point) { return shot.pixelColor(session->viewport.viewPoint(point, QSizeF(1200, 100)).toPoint()); }; QVERIFY2(at(QPointF(280.5, 50.5)) != QColor(Qt::blue), qPrintable(at(QPointF(280.5, 50.5)).name())); QCOMPARE(at(QPointF(200.5, 35.5)), QColor(Qt::blue)); } QTEST_MAIN(MaskPaintAnywhereTests) #include "MaskPaintAnywhereTests.moc"