Files
OmaPhoto/tests/BrushIntersectionTests.cpp
雷电芽衣andClaude c864304695
Release / deb (push) Canceled after 0s
Release / rpm (push) Canceled after 0s
Release / arch (push) Canceled after 0s
Release / publish (push) Canceled after 0s
OmaPhoto 1.0.0: the Linux port of Compositor
A layered image editor for Linux and Omarchy, ported from Compositor
(github.com/robbietilton/Compositor), whose Swift source is its
specification. It reads and writes Compositor's .comp projects.

Packages for Ubuntu, Fedora and Arch come from the release workflow;
scripts/install/ holds one install script per distribution family,
and flake.nix serves NixOS.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-26 00:44:52 -04:00

113 lines
4.8 KiB
C++

#include "BrushFixtures.h"
#include "Document/EditorSession.h"
#include "IO/ImageExporter.h"
#include <QDir>
// Self-crossings, spacing by distance, the opacity cap, idempotent flush.
namespace {
std::unique_ptr<BrushStroke> stroke(double diameter = 120, double opacity = 1)
{
return std::make_unique<BrushStroke>(blankLayer(800, 800), false, brush(diameter, 0, 1, 1, 1, opacity), QSizeF(800, 800));
}
void trace(BrushStroke &stroke, const std::vector<QPointF> &points, double step = 12)
{
stroke.append(points[0]);
for (size_t i = 1; i < points.size(); ++i) {
const QPointF a = points[i - 1], b = points[i];
const int count = std::max(1, int(std::ceil(std::hypot(b.x() - a.x(), b.y() - a.y()) / step)));
for (int index = 1; index <= count; ++index) {
const double t = double(index) / count;
stroke.append(QPointF(a.x() + (b.x() - a.x()) * t, a.y() + (b.y() - a.y()) * t));
}
}
stroke.flush();
}
QImage raster(const BrushStroke &stroke)
{
return preview(stroke, QSizeF(800, 800));
}
}
class BrushIntersectionTests : public QObject {
Q_OBJECT
private slots:
void selfCrossingsBlendInsteadOfTakingTheStrongestEdge();
void accumulationDependsOnDistanceNotEventCount();
void softCrossingsRespectStrokeOpacityAndFlushIsIdempotent();
void exportCrossingExample();
};
void BrushIntersectionTests::selfCrossingsBlendInsteadOfTakingTheStrongestEdge()
{
const auto vertical = stroke(), horizontal = stroke(), crossing = stroke();
trace(*vertical, {QPointF(400, 100), QPointF(400, 700)});
trace(*horizontal, {QPointF(700, 400), QPointF(100, 400)});
trace(*crossing, {QPointF(400, 100), QPointF(400, 700), QPointF(700, 700), QPointF(700, 400), QPointF(100, 400)});
const QImage v = raster(*vertical), h = raster(*horizontal), c = raster(*crossing);
for (int offset : {45, 48, 51}) {
const int a = alpha(v, 400 + offset, 400 + offset), b = alpha(h, 400 + offset, 400 + offset);
const int actual = alpha(c, 400 + offset, 400 + offset);
const int expected = 255 - (255 - a) * (255 - b) / 255;
QVERIFY2(actual > std::max(a, b) + 10, qPrintable(QStringLiteral("offset %1: %2 over %3 and %4").arg(offset).arg(actual).arg(a).arg(b)));
QVERIFY2(std::abs(actual - expected) <= 3, qPrintable(QStringLiteral("offset %1: %2 vs %3").arg(offset).arg(actual).arg(expected)));
}
}
void BrushIntersectionTests::accumulationDependsOnDistanceNotEventCount()
{
for (double diameter : {12.0, 120.0, 520.0}) {
const auto sparse = stroke(diameter), dense = stroke(diameter);
const std::vector<QPointF> points{QPointF(60, 400), QPointF(740, 400)};
trace(*sparse, points, 1000);
trace(*dense, points, 5);
const QImage a = raster(*sparse), b = raster(*dense);
for (int y = 400; y < std::min(800, 400 + int(diameter / 2)); ++y)
QVERIFY2(std::abs(alpha(a, 400, y) - alpha(b, 400, y)) <= 2, qPrintable(QStringLiteral("diameter %1, row %2").arg(diameter).arg(y)));
}
}
void BrushIntersectionTests::softCrossingsRespectStrokeOpacityAndFlushIsIdempotent()
{
const auto paint = stroke(120, 0.4);
trace(*paint, {QPointF(400, 100), QPointF(400, 700), QPointF(700, 700), QPointF(700, 400), QPointF(100, 400)});
const QImage first = raster(*paint);
QCOMPARE(alpha(first, 400, 400), 102);
for (int y = 0; y < 800; ++y) {
const uchar *row = first.constScanLine(y);
for (int x = 0; x < 800; ++x)
QVERIFY(row[x * 4 + 3] <= 102);
}
paint->flush();
const QImage second = raster(*paint);
QCOMPARE(second, first);
}
// Swift's picture of a 520 px crossing, with BRUSH_BENCHMARK=1.
void BrushIntersectionTests::exportCrossingExample()
{
if (qEnvironmentVariable("BRUSH_BENCHMARK") != QStringLiteral("1"))
QSKIP("BRUSH_BENCHMARK=1 exports the crossing");
EditorSession session;
session.createDocument(4000, 4000);
QImage black = BrushRaster::context(4000, 4000, false);
black.fill(Qt::black);
session.insert(ImportedImage(black, black, "Crossing"));
session.selectTool(NavigationTool::brush);
session.setBrushSettings(brush(520, 0, 1, 1, 1));
const std::vector<QPointF> path = {{1600, 700}, {1750, 3300}, {2900, 2400}, {3150, 1800}, {1600, 1700}, {450, 2000}};
session.beginBrush(path[0]);
for (size_t i = 1; i < path.size(); ++i) {
for (int step = 1; step <= 60; ++step) {
const double t = step / 60.0;
session.continueBrush(path[i - 1] + (path[i] - path[i - 1]) * t);
}
}
QVERIFY(session.finishBrushImmediately());
ImageExporter::exportPNG(session.projectSnapshot().value(), QDir::temp().filePath(QStringLiteral("compositor-brush-crossing.png")));
}
QTEST_GUILESS_MAIN(BrushIntersectionTests)
#include "BrushIntersectionTests.moc"