Blur softens only the pieces the brush reaches; a clone is cut once per tile (13.16, part two)

This commit is contained in:
ZacharyZhang-NY committed 2026-10-01 18:18:57 -04:00
1 parent cbd8612014
commit de8f23acbe
9 files changed
+197 -37

No files matched your search

+10 -6
View File
@@ -139,7 +139,9 @@ void BlurToolTests::theSampleFollowsTheRadius()
settings.blurRadius = radius;
settings.diameter = 3 * radius;
session.setBrushSettings(settings);
QCOMPARE(session.blurSample(document).value(), PixelAdjust::gaussianBlur(sharp, sigma, false));
const BrushStroke::Clone sample = session.blurSample(document).value();
QCOMPARE(sample.image, sharp);
QCOMPARE(sample.render(sharp.rect()), PixelAdjust::gaussianBlur(sharp, sigma, false));
}
// No layer, nothing to soften.
session.selectLayer(std::nullopt);
@@ -163,7 +165,8 @@ void BlurToolTests::aMaskSampleKeepsItsEdgeToneAndPlacement()
record(snapshot, id).maskPlacement = LayerTransform{.origin = {20, 10}, .size = {20, 20}};
record(snapshot, id).maskLinked = false;
});
const QImage sample = session.blurSample(session.document().value(), true).value();
const BrushStroke::Clone clone = session.blurSample(session.document().value(), true).value();
const QImage sample = clone.render(clone.image.rect());
QCOMPARE(sample.format(), QImage::Format_Grayscale8);
QImage expected(60, 40, QImage::Format_Grayscale8);
// Past its pixels the mask keeps its edge's tone: white.
@@ -187,7 +190,8 @@ void BlurToolTests::aMaskSampleKeepsItsEdgeToneAndPlacement()
record(snapshot, wideID).maskLinked = false;
});
turned.setBrushSettings(BrushSettings{.diameter = 5, .blurRadius = 1.5});
const QImage soft = turned.blurSample(turned.document().value(), true).value();
const BrushStroke::Clone sharp = turned.blurSample(turned.document().value(), true).value();
const QImage soft = sharp.render(sharp.image.rect());
// Past the true box, inside one turned the other way.
QVERIFY2(soft.constScanLine(36)[73] > 240, qPrintable(QString::number(soft.constScanLine(36)[73])));
QVERIFY2(soft.constScanLine(24)[27] > 240, qPrintable(QString::number(soft.constScanLine(24)[27])));
@@ -261,7 +265,7 @@ void BlurToolTests::aMaskBetweenPixelsTakesTheFraction()
for (int y = 0; y < 20; ++y)
for (int x = 0; x < 80; ++x)
steps.scanLine(y)[x] = x < 21 ? 0 : 200;
paint.clone = BrushStroke::Clone{steps, QSizeF(0, 0)};
paint.setClone(BrushStroke::Clone{steps, QSizeF(0, 0), {}});
paint.isBlur = true;
paint.append(QPointF(21, 10));
const QImage tile = paint.patches().at(0).image;
@@ -281,7 +285,7 @@ void BlurToolTests::aMaskPastTheCanvasEdgeKeepsItsTone()
white.fill(255);
layer.mask = LayerMask(LayerMask::assetFrom(white));
BrushStroke paint(layer, true, brush(6, 1, 1, 1, 1), QSizeF(20, 20));
paint.clone = BrushStroke::Clone{white, QSizeF(0, 0)};
paint.setClone(BrushStroke::Clone{white, QSizeF(0, 0), {}});
paint.isBlur = true;
paint.append(QPointF(0, 10));
const QImage tile = paint.patches().at(0).image;
@@ -292,7 +296,7 @@ void BlurToolTests::aMaskPastTheCanvasEdgeKeepsItsTone()
black.fill(0);
layer.mask = LayerMask(LayerMask::assetFrom(black));
BrushStroke over(layer, true, brush(6, 1, 1, 1, 1), QSizeF(20, 20));
over.clone = BrushStroke::Clone{white, QSizeF(0, 0)};
over.setClone(BrushStroke::Clone{white, QSizeF(0, 0), {}});
over.isBlur = true;
over.append(QPointF(0, 10));
QCOMPARE(int(over.patches().at(0).image.constScanLine(10)[0]), 128);
+7 -7
View File
@@ -249,7 +249,7 @@ void CloneStampTests::aScaledLayerSamplesThroughItsGrid()
ImageLayer layer(ImportedImage(clear, clear, QStringLiteral("Scaled")), QPointF(0, 0));
layer.transform.size = QSizeF(40, 20);
BrushStroke paint(layer, false, brush(30, 1, 0, 0, 0), QSizeF(80, 20));
paint.clone = BrushStroke::Clone{edged(80, 20, 31), QSizeF(10, 0)};
paint.setClone(BrushStroke::Clone{edged(80, 20, 31), QSizeF(10, 0), {}});
paint.append(QPointF(21, 10));
const QImage tile = paint.patches().at(0).image;
// Grid pixel 10 sits at document 21, sampling 31.
@@ -264,7 +264,7 @@ void CloneStampTests::aLayerBetweenPixelsSamplesTheFraction()
ImageLayer layer(QStringLiteral("Blank"), QSizeF(40, 20));
layer.transform.origin = QPointF(0.5, 0);
BrushStroke paint(layer, false, brush(20, 1, 0, 0, 0), QSizeF(80, 20));
paint.clone = BrushStroke::Clone{edged(80, 20, 31), QSizeF(3, 0)};
paint.setClone(BrushStroke::Clone{edged(80, 20, 31), QSizeF(3, 0), {}});
paint.append(QPointF(28, 10));
const QPointF centre = paint.pixelToDocument.map(QPointF(28.5, 10.5));
QCOMPARE(centre, QPointF(28, 10.5));
@@ -279,7 +279,7 @@ void CloneStampTests::aLayerBetweenPixelsSamplesTheFraction()
QImage rows = BrushRaster::context(40, 40, false);
rows.fill(QColor(255, 0, 0));
QPainter(&rows).fillRect(QRect(0, 12, 40, 28), QColor(0, 0, 255));
down.clone = BrushStroke::Clone{rows, QSizeF(0, 0)};
down.setClone(BrushStroke::Clone{rows, QSizeF(0, 0), {}});
down.append(QPointF(20, 12));
const QImage lowTile = down.patches().at(0).image;
const int row = int(std::floor(down.pixelToDocument.inverted().map(QPointF(20, 12)).y()));
@@ -297,7 +297,7 @@ void CloneStampTests::aQuarterPixelWeighsItsNeighbours()
return std::abs(pixel[0] - red) <= 2 && pixel[1] == 0 && std::abs(pixel[2] - blue) <= 2 && pixel[3] == 255;
};
BrushStroke across(layer, false, brush(20, 1, 0, 0, 0), QSizeF(80, 40));
across.clone = BrushStroke::Clone{edged(80, 40, 31), QSizeF(0, 0)};
across.setClone(BrushStroke::Clone{edged(80, 40, 31), QSizeF(0, 0), {}});
across.append(QPointF(31, 12));
QCOMPARE(across.pixelToDocument.map(QPointF(31.5, 12.5)), QPointF(30.75, 12.25));
const std::vector<int> mixed = pixel(across.patches().at(0).image, 31, 12);
@@ -307,7 +307,7 @@ void CloneStampTests::aQuarterPixelWeighsItsNeighbours()
rows.fill(QColor(255, 0, 0));
QPainter(&rows).fillRect(QRect(0, 12, 80, 28), QColor(0, 0, 255));
BrushStroke down(layer, false, brush(20, 1, 0, 0, 0), QSizeF(80, 40));
down.clone = BrushStroke::Clone{rows, QSizeF(0, 0)};
down.setClone(BrushStroke::Clone{rows, QSizeF(0, 0), {}});
down.append(QPointF(20, 12));
const std::vector<int> stacked = pixel(down.patches().at(0).image, 20, 12);
QVERIFY2(near(stacked, 64, 191), qPrintable(QString("%1 %2").arg(stacked[0]).arg(stacked[2])));
@@ -321,7 +321,7 @@ void CloneStampTests::opacityAndTheSelectionLimitTheClone()
over.fill(QColor(255, 0, 0));
BrushStroke half(ImageLayer(ImportedImage(under, under, QStringLiteral("Blue")), QPointF(0, 0)), false, brush(10, 1, 0, 0, 0, 0.5),
QSizeF(40, 20));
half.clone = BrushStroke::Clone{over, QSizeF(0, 0)};
half.setClone(BrushStroke::Clone{over, QSizeF(0, 0), {}});
half.append(QPointF(10, 10));
// Each byte is rounded once: 127.5 and 127.5 make 128.
QCOMPARE(pixel(half.patches().at(0).image, 10, 10), (std::vector<int>{128, 0, 128, 255}));
@@ -337,7 +337,7 @@ void CloneStampTests::opacityAndTheSelectionLimitTheClone()
painter.setCompositionMode(QPainter::CompositionMode_Clear);
painter.fillRect(QRect(0, 0, 40, 5), Qt::transparent);
}
bounded.clone = BrushStroke::Clone{sample, QSizeF(0, 0)};
bounded.setClone(BrushStroke::Clone{sample, QSizeF(0, 0), {}});
bounded.append(QPointF(10, 8));
const QImage tile = bounded.patches().at(0).image;
QCOMPARE(pixel(tile, 8, 8), redPixel);
+90
View File
@@ -40,6 +40,9 @@ private slots:
void largestBrushCommits_data();
void largestBrushCommits();
void aThinnedCommitLandsAllTheWarpMoved();
void blurPiecesMatchTheWhole_data();
void blurPiecesMatchTheWhole();
void piecesAreCutOnceWhereTheBrushReaches();
};
void LargeCanvasBrushTests::largestBrushCommits_data()
@@ -47,6 +50,7 @@ void LargeCanvasBrushTests::largestBrushCommits_data()
QTest::addColumn<BlurToolMode>("mode");
QTest::newRow("liquify") << BlurToolMode::liquify;
QTest::newRow("smudge") << BlurToolMode::smudge;
QTest::newRow("blur") << BlurToolMode::blur;
}
// The commit's tip runs past Size's 2000, once refused.
@@ -56,6 +60,9 @@ void LargeCanvasBrushTests::largestBrushCommits()
EditorSession session;
bands(session, 5000);
session.setBlurMode(mode);
BrushSettings settings = session.brushSettings();
settings.blurRadius = 20;
session.setBrushSettings(settings);
const ImageIdentity before = session.activeLayer().value().asset.value().identity();
stroke(session, 5000);
QCOMPARE(session.brushError(), std::optional<QString>());
@@ -97,5 +104,88 @@ void LargeCanvasBrushTests::aThinnedCommitLandsAllTheWarpMoved()
QCOMPARE(apart, 0);
}
void LargeCanvasBrushTests::blurPiecesMatchTheWhole_data()
{
QTest::addColumn<bool>("mask");
QTest::newRow("pixels") << false;
QTest::newRow("mask") << true;
}
// Swift's test: each piece is its part of the whole.
void LargeCanvasBrushTests::blurPiecesMatchTheWhole()
{
QFETCH(bool, mask);
EditorSession session;
bands(session, 600);
const QUuid id = session.activeLayerID().value();
QImage stripes(600, 600, QImage::Format_Grayscale8);
for (int y = 0; y < 600; ++y)
for (int x = 0; x < 600; ++x)
stripes.scanLine(y)[x] = (x / 37 + y / 23) % 2 ? 255 : 0;
rewrite(session, [&](ProjectSnapshot &snapshot) { setMask(snapshot, id, LayerMask::assetFrom(stripes)); });
BrushSettings settings = session.brushSettings();
settings.blurRadius = 12;
session.setBrushSettings(settings);
const BrushStroke::Clone blur = session.blurSample(session.document().value(), mask).value();
const QImage whole = blur.render(blur.image.rect());
QCOMPARE(whole.size(), QSize(600, 600));
for (const QRect part : {QRect(0, 0, 256, 256), QRect(250, 300, 260, 180), QRect(500, 530, 100, 70)}) {
const QImage piece = blur.render(part);
QCOMPARE(piece.size(), part.size());
QCOMPARE(piece, whole.copy(part));
}
}
// Only tiles the brush reaches are blurred, each once.
void LargeCanvasBrushTests::piecesAreCutOnceWhereTheBrushReaches()
{
QImage image = BrushRaster::context(1000, 1000, false);
image.fill(Qt::red);
const ImageLayer layer(ImportedImage(image, image, QStringLiteral("Red")), QPointF(0, 0));
BrushStroke stroke(layer, false, brush(20, 1, 0, 0, 0), QSizeF(1000, 1000));
std::vector<QRect> parts;
stroke.setClone(BrushStroke::Clone{image, QSizeF(0, 0), [&](const QRect &part) {
parts.push_back(part);
return image.copy(part);
}});
stroke.isBlur = true;
stroke.append(QPointF(100, 100));
stroke.append(QPointF(120, 100));
stroke.flush();
QCOMPARE(int(parts.size()), 1);
QCOMPARE(parts[0], QRect(0, 0, 258, 258));
// Over the same tile again: nothing new is cut.
stroke.append(QPointF(100, 110));
stroke.flush();
QCOMPARE(int(parts.size()), 1);
// Into the next tile: its piece, two pixels round.
stroke.append(QPointF(300, 110));
stroke.flush();
QCOMPARE(int(parts.size()), 2);
QCOMPARE(parts[1], QRect(254, 0, 260, 258));
// A new clone drops what was cut.
stroke.setClone(stroke.clone());
stroke.append(QPointF(310, 110));
stroke.flush();
QCOMPARE(int(parts.size()), 3);
QCOMPARE(parts[2], parts[1]);
// Past the clone, a tile is neither cut nor painted.
BrushStroke past(layer, false, brush(20, 1, 0, 0, 0), QSizeF(1000, 1000));
QImage blue = BrushRaster::context(200, 200, false);
blue.fill(Qt::blue);
parts.clear();
past.setClone(BrushStroke::Clone{blue, QSizeF(0, 0), [&](const QRect &part) {
parts.push_back(part);
return blue.copy(part);
}});
past.isBlur = true;
past.append(QPointF(400, 400));
past.append(QPointF(420, 400));
past.flush();
QVERIFY(parts.empty());
QVERIFY(!past.patches().empty());
QCOMPARE(pixel(preview(past, QSizeF(1000, 1000)), 410, 400), (std::vector<int>{255, 0, 0, 255}));
}
QTEST_GUILESS_MAIN(LargeCanvasBrushTests)
#include "LargeCanvasBrushTests.moc"