Blur softens only the pieces the brush reaches; a clone is cut once per tile (13.16, part two)
This commit is contained in:
1 parent
cbd8612014
commit
de8f23acbe
9 files changed
+197
-37
No files matched your search
@@ -1,4 +1,4 @@
|
||||
// Swift's BlurTool extension: what a Blur stroke paints.
|
||||
public:
|
||||
// The active layer, or its mask, softened at document size.
|
||||
std::optional<QImage> blurSample(const CanvasDocument &document, bool mask = false) const;
|
||||
// The sharp layer or mask, document-sized; `render` softens parts.
|
||||
std::optional<BrushStroke::Clone> blurSample(const CanvasDocument &document, bool mask = false) const;
|
||||
@@ -5,8 +5,42 @@
|
||||
#include "Rendering/LayerRenderer.h"
|
||||
#include <cmath>
|
||||
|
||||
namespace {
|
||||
// `part` padded by `margin`: clear outside, or edges repeated.
|
||||
QImage padded(const QImage &image, const QRect &part, int margin, bool repeat)
|
||||
{
|
||||
const QRect around = part.adjusted(-margin, -margin, margin, margin);
|
||||
if (!repeat) {
|
||||
const QImage copy = image.copy(around);
|
||||
if (copy.isNull())
|
||||
throw ExportError(ExportError::Kind::render);
|
||||
return copy;
|
||||
}
|
||||
QImage copy = BrushRaster::context(around.width(), around.height(), true);
|
||||
for (int y = 0; y < around.height(); ++y) {
|
||||
const uchar *row = image.constScanLine(std::clamp(around.top() + y, 0, image.height() - 1));
|
||||
uchar *out = copy.scanLine(y);
|
||||
for (int x = 0; x < around.width(); ++x)
|
||||
out[x] = row[std::clamp(around.left() + x, 0, image.width() - 1)];
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
// A piece blurred alone equals that part of the whole.
|
||||
std::function<QImage(const QRect &)> softened(const QImage &sharp, double sigma, bool mask)
|
||||
{
|
||||
return [sharp, sigma, mask](const QRect &part) {
|
||||
const int margin = int(std::ceil(3 * sigma));
|
||||
const QImage piece = PixelAdjust::gaussianBlur(padded(sharp, part, margin, mask), sigma, mask).copy(margin, margin, part.width(), part.height());
|
||||
if (piece.isNull())
|
||||
throw ExportError(ExportError::Kind::render);
|
||||
return piece;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Swift's BlurTool extension: the layer, or its mask, softened.
|
||||
std::optional<QImage> EditorSession::blurSample(const CanvasDocument &document, bool mask) const
|
||||
std::optional<BrushStroke::Clone> EditorSession::blurSample(const CanvasDocument &document, bool mask) const
|
||||
{
|
||||
const std::optional<ImageLayer> layer = activeLayer();
|
||||
if (!layer)
|
||||
@@ -34,7 +68,7 @@ std::optional<QImage> EditorSession::blurSample(const CanvasDocument &document,
|
||||
painter.restore();
|
||||
LayerRenderer::drawCoverage(layer->mask->asset.image(), placement, painter);
|
||||
painter.end();
|
||||
return PixelAdjust::gaussianBlur(context, sigma, true);
|
||||
return BrushStroke::Clone{context, QSizeF(0, 0), softened(context, sigma, true)};
|
||||
}
|
||||
if (!layer->asset)
|
||||
return std::nullopt;
|
||||
@@ -44,7 +78,7 @@ std::optional<QImage> EditorSession::blurSample(const CanvasDocument &document,
|
||||
const LayerTransform transform = displayedTransform(*layer);
|
||||
LayerRenderer::draw(layer->asset->image(), transform, transform.center(), painter, {});
|
||||
painter.end();
|
||||
return PixelAdjust::gaussianBlur(context, sigma, false);
|
||||
return BrushStroke::Clone{context, QSizeF(0, 0), softened(context, sigma, false)};
|
||||
} catch (const ExportError &error) {
|
||||
qCWarning(lcApp) << "the Blur tool cannot sample the layer:" << error.what();
|
||||
return std::nullopt;
|
||||
|
||||
@@ -239,7 +239,7 @@ void BrushStroke::publish(const std::set<qint64> &changed)
|
||||
tile.context.detach();
|
||||
if (tile.context.isNull())
|
||||
throw ExportError(ExportError::Kind::render);
|
||||
work.push_back({&tile, &m_coverage.at(key), selectionCoverage(tile), clonePixels(tile)});
|
||||
work.push_back({&tile, &m_coverage.at(key), selectionCoverage(tile), clonePixels(key, tile)});
|
||||
const QRectF rect = pixelToDocument.mapRect(tile.rect).intersected(canvas);
|
||||
m_dirtyDocumentRect = m_dirtyDocumentRect ? std::optional(m_dirtyDocumentRect->united(rect)) : std::optional(rect);
|
||||
}
|
||||
@@ -250,9 +250,11 @@ void BrushStroke::publish(const std::set<qint64> &changed)
|
||||
const double alpha = healing ? 0.45 : settings.opacity;
|
||||
PoolMap::blocking(work, [&](const Fill &item) {
|
||||
Tile &tile = *item.tile;
|
||||
if (clone && (!isMask || isBlur))
|
||||
fillClone(item.sample, *item.coverage, settings.opacity, tile.context, item.clip, replacesWithClone);
|
||||
else
|
||||
if (m_clone && (!isMask || isBlur)) {
|
||||
// Where the sample does not reach, nothing is painted.
|
||||
if (!item.sample.isNull())
|
||||
fillClone(item.sample, *item.coverage, settings.opacity, tile.context, item.clip, replacesWithClone);
|
||||
} else
|
||||
BrushRaster::fill(colour, *item.coverage, QRectF(QPointF(0, 0), tile.rect.size()), alpha, tile.context, mode, item.clip);
|
||||
tile.image = tile.context;
|
||||
});
|
||||
@@ -267,17 +269,36 @@ QImage BrushStroke::selectionCoverage(const Tile &tile) const
|
||||
QTransform::fromTranslate(tile.rect.left(), tile.rect.top()) * pixelToDocument);
|
||||
}
|
||||
|
||||
// The clone as this tile sees it, before the fill.
|
||||
QImage BrushStroke::clonePixels(const Tile &tile) const
|
||||
void BrushStroke::setClone(std::optional<Clone> clone)
|
||||
{
|
||||
if (!clone)
|
||||
m_clone = std::move(clone);
|
||||
m_clonePieces.clear();
|
||||
}
|
||||
|
||||
// The clone as this tile sees it, before the fill.
|
||||
QImage BrushStroke::clonePixels(qint64 key, const Tile &tile)
|
||||
{
|
||||
if (!m_clone)
|
||||
return QImage();
|
||||
const int width = int(tile.rect.width()), height = int(tile.rect.height());
|
||||
// Tile pixels to clone pixels: grid, document, then offset.
|
||||
const QTransform toClone = QTransform::fromTranslate(tile.rect.left(), tile.rect.top()) * pixelToDocument
|
||||
* QTransform::fromTranslate(clone->offset.width(), clone->offset.height());
|
||||
const QTransform toWhole = QTransform::fromTranslate(tile.rect.left(), tile.rect.top()) * pixelToDocument
|
||||
* QTransform::fromTranslate(m_clone->offset.width(), m_clone->offset.height());
|
||||
// Swift's clonePiece: the part under the tile, cut once.
|
||||
if (!m_clonePieces.contains(key)) {
|
||||
const QRect part =
|
||||
toWhole.mapRect(QRectF(0, 0, width, height)).adjusted(-2, -2, 2, 2).toAlignedRect().intersected(m_clone->image.rect());
|
||||
if (part.isEmpty())
|
||||
return QImage();
|
||||
const QImage piece = m_clone->render ? m_clone->render(part) : m_clone->image.copy(part);
|
||||
if (piece.isNull())
|
||||
throw ExportError(ExportError::Kind::render);
|
||||
m_clonePieces.emplace(key, ClonePiece{piece, part.topLeft()});
|
||||
}
|
||||
const ClonePiece &piece = m_clonePieces.at(key);
|
||||
const QTransform toClone = toWhole * QTransform::fromTranslate(-piece.origin.x(), -piece.origin.y());
|
||||
// Colour even for gray clones: edges carry their coverage.
|
||||
const QImage &image = clone->image;
|
||||
const QImage &image = piece.image;
|
||||
if (toClone.type() > QTransform::TxTranslate) {
|
||||
// Swift's medium quality: bilinear under a scale or turn.
|
||||
QImage sample = BrushRaster::context(width, height, false);
|
||||
|
||||
@@ -113,8 +113,12 @@ public:
|
||||
struct Clone {
|
||||
QImage image;
|
||||
QSizeF offset;
|
||||
// Makes a part of `image`: Blur softens piece by piece.
|
||||
std::function<QImage(const QRect &)> render;
|
||||
};
|
||||
std::optional<Clone> clone;
|
||||
const std::optional<Clone> &clone() const { return m_clone; }
|
||||
// Swift's didSet: the pieces cut so far go.
|
||||
void setClone(std::optional<Clone> clone);
|
||||
// Blur: the clone is the layer softened, in place.
|
||||
bool isBlur = false;
|
||||
// The clone replaces what is under the tip, clearing too.
|
||||
@@ -179,7 +183,8 @@ private:
|
||||
void renderContinuous(const std::vector<BrushSegment> &settled, const std::vector<BrushSegment> &tail);
|
||||
void publish(const std::set<qint64> &changed);
|
||||
QImage selectionCoverage(const Tile &tile) const;
|
||||
QImage clonePixels(const Tile &tile) const;
|
||||
// Null where the clone does not reach the tile.
|
||||
QImage clonePixels(qint64 key, const Tile &tile);
|
||||
void allocateTile(qint64 key, qint64 x, qint64 y);
|
||||
// `draw` in document coordinates over every tile the canvas covers.
|
||||
void paintCanvas(bool withinSource, const std::function<void(QPainter &)> &draw);
|
||||
@@ -198,5 +203,12 @@ private:
|
||||
std::map<qint64, QImage> m_coverage;
|
||||
// Selected pixels cut from the source, in layer pixels.
|
||||
std::optional<Lifted> m_lifted;
|
||||
std::optional<Clone> m_clone;
|
||||
// Each tile's part of the clone, cut once, and where.
|
||||
struct ClonePiece {
|
||||
QImage image;
|
||||
QPoint origin;
|
||||
};
|
||||
std::map<qint64, ClonePiece> m_clonePieces;
|
||||
std::set<qint64> m_moveTiles;
|
||||
};
|
||||
@@ -88,14 +88,13 @@ void EditorSession::beginBrush(QPointF point)
|
||||
if (!image)
|
||||
return;
|
||||
m_cloneOffset = offset;
|
||||
clone = BrushStroke::Clone{*image, *offset};
|
||||
clone = BrushStroke::Clone{*image, *offset, {}};
|
||||
}
|
||||
// Blur paints a softened copy of the layer, in place.
|
||||
if (m_tool == NavigationTool::blur) {
|
||||
const std::optional<QImage> image = blurSample(m_document.value(), m_isMaskSelected);
|
||||
if (!image)
|
||||
clone = blurSample(m_document.value(), m_isMaskSelected);
|
||||
if (!clone)
|
||||
return;
|
||||
clone = BrushStroke::Clone{*image, QSizeF(0, 0)};
|
||||
}
|
||||
finishOpacityEdit();
|
||||
try {
|
||||
@@ -109,7 +108,7 @@ void EditorSession::beginBrush(QPointF point)
|
||||
settings.blue = settings.red;
|
||||
}
|
||||
m_brushStroke = makeRasterEdit(layer, settings, m_tool == NavigationTool::brush);
|
||||
m_brushStroke->clone = clone;
|
||||
m_brushStroke->setClone(clone);
|
||||
m_brushStroke->isBlur = m_tool == NavigationTool::blur;
|
||||
m_brushStroke->append(point);
|
||||
m_brushAnchor = point;
|
||||
@@ -254,7 +253,7 @@ void EditorSession::commitPaintSnapshot(const BrushStroke &stroke)
|
||||
beginEdit(stroke.editName.value_or(stroke.isMask ? QStringLiteral("Paint Mask")
|
||||
: stroke.settings.erasing ? QStringLiteral("Erase")
|
||||
: stroke.isBlur ? QStringLiteral("Blur")
|
||||
: stroke.clone ? QStringLiteral("Clone Stamp")
|
||||
: stroke.clone() ? QStringLiteral("Clone Stamp")
|
||||
: stroke.settings.healing ? QStringLiteral("Spot Healing")
|
||||
: QStringLiteral("Brush Stroke")));
|
||||
ImageLayer &layer = m_document->layers[index];
|
||||
|
||||
@@ -268,7 +268,7 @@ void EditorSession::finishWarp()
|
||||
settings.hardness = 1;
|
||||
settings.opacity = 1;
|
||||
const std::unique_ptr<BrushStroke> stroke = makeRasterEdit(current, settings);
|
||||
stroke->clone = BrushStroke::Clone{warp->image(), QSizeF(0, 0)};
|
||||
stroke->setClone(BrushStroke::Clone{warp->image(), QSizeF(0, 0), {}});
|
||||
stroke->replacesWithClone = true;
|
||||
stroke->editName = rawValue(warp->mode);
|
||||
// A point every twentieth of the tip covers every dab.
|
||||
|
||||
+10
-6
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
Reference in new issue
Block a user