Merge branch 'port-d'

# Conflicts:
#	AGENTS.md
This commit is contained in:
ZacharyZhang-NY committed 2026-10-01 23:02:47 -04:00
commit 8204e6a28b
14 files changed
+231 -101

No files matched your search

+2 -1
View File
@@ -29,8 +29,9 @@ QImage padded(const QImage &image, const QRect &part, int margin, bool repeat)
std::function<QImage(const QRect &)> softened(const QImage &sharp, double sigma, bool mask)
{
return [sharp, sigma, mask](const QRect &part) {
// The pad holds the edges; the blur's rule never reaches.
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());
const QImage piece = PixelAdjust::gaussianBlur(padded(sharp, part, margin, mask), sigma, false).copy(margin, margin, part.width(), part.height());
if (piece.isNull())
throw ExportError(ExportError::Kind::render);
return piece;
+34 -17
View File
@@ -2,9 +2,11 @@
#include "Document/EditorSession.h"
#include "IO/ImageExporter.h"
#include "IO/ProjectStore.h"
#include "Logging.h"
#include "Rendering/LayerRenderer.h"
#include <algorithm>
#include <cmath>
#include <new>
#include <stdexcept>
QString rawValue(BrushToolMode mode)
@@ -53,6 +55,20 @@ WarpStroke::WarpStroke(const ImageLayer &layer, const QImage &image, const Layer
}
}
// A point every twentieth of the tip covers every dab.
std::vector<QPointF> WarpStroke::committedPoints() const
{
const double spacing = std::max(1.0, diameter * 0.05);
std::vector<QPointF> kept;
for (size_t index = 0; index < m_points.size(); ++index) {
const QPointF step = m_points[index];
if (!kept.empty() && index + 1 < m_points.size() && std::hypot(step.x() - kept.back().x(), step.y() - kept.back().y()) < spacing)
continue;
kept.push_back(step);
}
return kept;
}
int WarpStroke::radius() const
{
return int(std::ceil(diameter / 2));
@@ -102,7 +118,12 @@ void WarpStroke::append(QPointF point)
void WarpStroke::pickUp(QPointF center)
{
const int r = radius(), side = 2 * r + 1;
m_carried.assign(size_t(side) * size_t(side) * 4, 0);
// The first dab's, inside `beginWarp`'s catch.
try {
m_carried.assign(size_t(side) * size_t(side) * 4, 0);
} catch (const std::bad_alloc &) {
throw ExportError(ExportError::Kind::render);
}
const int cx = int(std::round(center.x())), cy = int(std::round(center.y()));
const uchar *pixels = m_context.constBits();
const qsizetype stride = m_context.bytesPerLine();
@@ -166,8 +187,14 @@ void WarpStroke::push(QPointF a, QPointF b)
if (x0 > x1 || y0 > y1)
return;
const int cw = x1 - x0 + 1, ch = y1 - y0 + 1;
if (m_scratch.size() < size_t(cw) * size_t(ch) * 2)
m_scratch.assign(size_t(cw) * size_t(ch) * 2, 0);
// Swift's guard on the scratch texture: no room, no dab.
try {
if (m_scratch.size() < size_t(cw) * size_t(ch) * 2)
m_scratch.assign(size_t(cw) * size_t(ch) * 2, 0);
} catch (const std::bad_alloc &) {
qCWarning(lcRendering) << "a Liquify dab finds no memory:" << cw << "by" << ch;
return;
}
for (int y = 0; y < ch; ++y) {
const auto row = m_offsets.begin() + std::ptrdiff_t((size_t(y + y0) * size_t(width) + size_t(x0)) * 2);
std::copy(row, row + std::ptrdiff_t(size_t(cw) * 2), m_scratch.begin() + std::ptrdiff_t(size_t(y) * size_t(cw) * 2));
@@ -209,11 +236,10 @@ void WarpStroke::push(QPointF a, QPointF b)
// The untouched layer there, held to its edges, sampled once.
const float sourceX = std::clamp(float(x) + moved[0], 0.0f, float(width - 1));
const float sourceY = std::clamp(float(y) + moved[1], 0.0f, float(height - 1));
const int jx = std::max(0, std::min(int(sourceX), width - 2)), jy = std::max(0, std::min(int(sourceY), height - 2));
// `ix` above needs two columns and rows, so these exist.
const int jx = std::min(int(sourceX), width - 2), jy = std::min(int(sourceY), height - 2);
const float gx = sourceX - float(jx), gy = sourceY - float(jy);
const int kx = std::min(jx + 1, width - 1), ky = std::min(jy + 1, height - 1);
const uchar *top0 = original + jy * originalStride, *bottom0 = original + ky * originalStride;
const uchar *c00 = top0 + jx * 4, *c10 = top0 + kx * 4, *c01 = bottom0 + jx * 4, *c11 = bottom0 + kx * 4;
const uchar *c00 = original + jy * originalStride + jx * 4, *c10 = c00 + 4, *c01 = c00 + originalStride, *c11 = c01 + 4;
uchar *p = pixels + y * stride + x * 4;
for (int k = 0; k < 4; ++k) {
const float a = float(c00[k]) / 255, b = float(c10[k]) / 255, c = float(c01[k]) / 255, d = float(c11[k]) / 255;
@@ -270,17 +296,8 @@ void EditorSession::finishWarp()
stroke->setClone(BrushStroke::Clone{warp->image(), QRectF(QPointF(0, 0), warp->image().size()), false, {}});
stroke->replacesWithClone = true;
stroke->editName = rawValue(warp->mode);
// A point every twentieth of the tip covers every dab.
const double spacing = std::max(1.0, warp->diameter * 0.05);
const std::vector<QPointF> &points = warp->points();
std::optional<QPointF> kept;
for (size_t index = 0; index < points.size(); ++index) {
const QPointF step = points[index];
if (kept && index + 1 < points.size() && std::hypot(step.x() - kept->x(), step.y() - kept->y()) < spacing)
continue;
for (const QPointF step : warp->committedPoints())
stroke->append(step);
kept = step;
}
stroke->flush();
if (!stroke->patches().empty())
commitPaintSnapshot(*stroke);
+2
View File
@@ -30,6 +30,8 @@ public:
const int height;
// Every dab's centre, for painting the result into the layer.
const std::vector<QPointF> &points() const { return m_points; }
// Swift's thinning: a twentieth of the brush apart, last kept.
std::vector<QPointF> committedPoints() const;
// The layer as the stroke has reshaped it so far.
const QImage &image() const { return m_context; }
// Dabs along the way to `point`.