Masks painted anywhere on the canvas; mask thumbnails' ground white or black (12.12b)

This commit is contained in:
雷电芽衣 committed 2026-09-29 13:26:01 -04:00
1 parent 136ee65a48
commit f335818e53
20 files changed
+348 -68

No files matched your search

+6 -3
View File
@@ -131,8 +131,9 @@ PaintSnapshot BrushStroke::paintSnapshot() const
const QRectF placed = rect.translated(tile.rect.topLeft());
bounds = bounds ? std::optional(bounds->united(placed)) : std::optional(placed);
}
const QRectF crop = bounds.value_or(committedBounds());
const std::shared_ptr<const RasterSnapshot> raster = RasterSnapshot::replacing(m_source, sourceRect, patches(), crop, isMask);
// A mask keeps every tile touched: past its pixels, grows.
const QRectF crop = isMask ? committedBounds() : bounds.value_or(committedBounds());
const std::shared_ptr<const RasterSnapshot> raster = RasterSnapshot::replacing(m_source, sourceRect, patches(), crop, isMask, maskBackground);
return {ImportedImage(raster, raster->thumbnail(), layer.name), transform(crop), crop};
}
@@ -142,7 +143,7 @@ BrushCommit::Input BrushStroke::commitInput() const
std::vector<BrushPatch> shifted;
for (const BrushPatch &patch : patches())
shifted.push_back({patch.rect.translated(-bounds.topLeft()), patch.image});
return {int(bounds.width()), int(bounds.height()), m_source, shifted, isMask, layer.name, sourceRect.translated(-bounds.topLeft())};
return {int(bounds.width()), int(bounds.height()), m_source, shifted, isMask, layer.name, sourceRect.translated(-bounds.topLeft()), maskBackground};
}
ImportedImage BrushCommit::expandMask(const ImportedImage &asset, const Input &input, const QRectF &crop)
@@ -161,6 +162,8 @@ ImportedImage BrushCommit::expandMask(const ImportedImage &asset, const Input &i
BrushCommit::Output BrushCommit::render(const Input &input)
{
QImage context = BrushRaster::context(input.width, input.height, input.mask);
if (input.mask)
context.fill(qRound(input.fill * 255));
QPainter painter(&context);
if (input.source)
drawSource(*input.source, input.sourceRect, painter);
+18 -8
View File
@@ -64,20 +64,26 @@ double BrushStroke::spacingFraction(double hardness)
return hardness >= 1 ? 0.015 : 0.025;
}
BrushStroke::Grid BrushStroke::grid(const ImageLayer &layer, bool mask, const BrushSettings &settings, const QRectF &canvas)
BrushStroke::Grid BrushStroke::grid(const ImageLayer &layer, bool mask, const BrushSettings &settings, const QRectF &canvas, bool growsMask)
{
// A placed mask paints in its grid; else the layer's.
const bool placedMask = mask && layer.mask && layer.mask->placement;
const LayerTransform base = placedMask ? *layer.mask->placement : layer.transform;
const int originalWidth = placedMask ? layer.mask->asset.size().width()
: layer.asset ? layer.asset->size().width() : int(std::round(layer.size().width()));
const int originalHeight = placedMask ? layer.mask->asset.size().height()
: layer.asset ? layer.asset->size().height() : int(std::round(layer.size().height()));
// A solid placed mask gets a pixel per document pixel.
const bool solidPlaced = placedMask && layer.mask->asset.size().width() <= 2 && layer.mask->asset.size().height() <= 2;
const int originalWidth = solidPlaced ? std::max(1, int(std::round(base.size.width())))
: placedMask ? layer.mask->asset.size().width()
: layer.asset ? layer.asset->size().width()
: int(std::round(layer.size().width()));
const int originalHeight = solidPlaced ? std::max(1, int(std::round(base.size.height())))
: placedMask ? layer.mask->asset.size().height()
: layer.asset ? layer.asset->size().height()
: int(std::round(layer.size().height()));
if (originalWidth < 1 || originalWidth > DocumentLimits::maxSide || originalHeight < 1 || originalHeight > DocumentLimits::maxSide)
throw ProjectError(ProjectError::Kind::tooLarge);
const QTransform originalMapping = BrushRaster::pixelToDocument(base, originalWidth, originalHeight);
const QRectF originalBounds(0, 0, originalWidth, originalHeight);
const QRectF extent = mask ? originalBounds : originalBounds.united(integral(originalMapping.inverted().mapRect(canvas)));
const QRectF extent = mask && !growsMask ? originalBounds : originalBounds.united(integral(originalMapping.inverted().mapRect(canvas)));
// Judged as a double: past int the conversion is undefined.
if (extent.width() < 1 || extent.width() > 1'000'000'000 || extent.height() < 1 || extent.height() > 1'000'000'000)
throw ProjectError(ProjectError::Kind::tooLarge);
@@ -93,14 +99,15 @@ BrushStroke::Grid BrushStroke::grid(const ImageLayer &layer, bool mask, const Br
return grid;
}
BrushStroke::BrushStroke(const ImageLayer &layer, bool mask, const BrushSettings &settings, QSizeF canvas)
: BrushStroke(layer, mask, settings, QRectF(QPointF(0, 0), canvas), grid(layer, mask, settings, QRectF(QPointF(0, 0), canvas)))
BrushStroke::BrushStroke(const ImageLayer &layer, bool mask, const BrushSettings &settings, QSizeF canvas, bool growsMask)
: BrushStroke(layer, mask, settings, QRectF(QPointF(0, 0), canvas), grid(layer, mask, settings, QRectF(QPointF(0, 0), canvas), growsMask))
{
}
BrushStroke::BrushStroke(const ImageLayer &layer, bool mask, const BrushSettings &settings, const QRectF &canvas, const Grid &grid)
: layer(layer), isMask(mask), settings(settings), canvas(canvas), width(grid.width), height(grid.height), sourceRect(grid.sourceRect),
pixelToDocument(grid.pixelToDocument), paintTransform(grid.paintTransform),
maskBackground(mask && layer.mask ? LayerMask::background(layer.mask->asset.thumbnail) : 1),
m_source(mask ? (layer.mask ? std::optional(layer.mask->asset) : std::nullopt) : layer.asset),
m_paintColor(mask ? QColor::fromRgbF(settings.red, settings.red, settings.red) : QColor::fromRgbF(settings.red, settings.green, settings.blue))
{
@@ -306,6 +313,9 @@ void BrushStroke::allocateTile(qint64 key, qint64 x, qint64 y)
throw ProjectError(ProjectError::Kind::tooLarge);
m_allocatedBounds = nextBounds;
QImage context = BrushRaster::context(int(rect.width()), int(rect.height()), isMask);
// Past the mask's pixels a tile starts as its background.
if (isMask)
context.fill(qRound(maskBackground * 255));
QPainter painter(&context);
const QRectF initialRect = sourceRect.translated(-rect.topLeft());
if (m_source && m_source->raster) {
+7 -2
View File
@@ -75,6 +75,8 @@ struct Input {
bool mask;
QString name;
QRectF sourceRect;
// A grown mask where no old pixel or edit reaches.
double fill = 1;
};
struct Output {
ImportedImage asset;
@@ -87,7 +89,8 @@ Output render(const Input &input);
// Only touched 256 px tiles hold pixels; snapshots copy those.
class BrushStroke {
public:
BrushStroke(const ImageLayer &layer, bool mask, const BrushSettings &settings, QSizeF canvas);
// `growsMask`: the brush and fills reach the canvas on masks.
BrushStroke(const ImageLayer &layer, bool mask, const BrushSettings &settings, QSizeF canvas, bool growsMask = false);
const ImageLayer layer;
const bool isMask;
@@ -98,6 +101,8 @@ public:
const QRectF sourceRect;
const QTransform pixelToDocument;
const LayerTransform paintTransform;
// A mask past its pixels: 1 reveals, 0 hides.
const double maskBackground;
// Swift's Int: the budget left may run below zero.
qint64 pixelLimit = DocumentLimits::documentPixelBudget();
// Limits every edit to the document selection; nil when none.
@@ -163,7 +168,7 @@ private:
QTransform pixelToDocument;
LayerTransform paintTransform;
};
static Grid grid(const ImageLayer &layer, bool mask, const BrushSettings &settings, const QRectF &canvas);
static Grid grid(const ImageLayer &layer, bool mask, const BrushSettings &settings, const QRectF &canvas, bool growsMask);
BrushStroke(const ImageLayer &layer, bool mask, const BrushSettings &settings, const QRectF &canvas, const Grid &grid);
std::vector<BrushSegment> continuousCurve(QPointF start, QPointF end, QPointF before, QPointF after) const;
std::set<qint64> continuousKeys(const std::vector<BrushSegment> &segments) const;
+16 -5
View File
@@ -38,11 +38,11 @@ bool EditorSession::canPaint() const
}
// A tiled raster edit of pixels or mask, within budget.
std::unique_ptr<BrushStroke> EditorSession::makeRasterEdit(const ImageLayer &layer, const BrushSettings &settings) const
std::unique_ptr<BrushStroke> EditorSession::makeRasterEdit(const ImageLayer &layer, const BrushSettings &settings, bool growsMask) const
{
if (!m_document)
throw ProjectError(ProjectError::Kind::tooLarge);
auto stroke = std::make_unique<BrushStroke>(layer, m_isMaskSelected, settings, m_document->size());
auto stroke = std::make_unique<BrushStroke>(layer, m_isMaskSelected, settings, m_document->size(), growsMask);
qint64 used = 0;
for (const ImageLayer &other : m_document->layers) {
if (other.id == layer.id)
@@ -108,7 +108,7 @@ void EditorSession::beginBrush(QPointF point)
settings.green = settings.red;
settings.blue = settings.red;
}
m_brushStroke = makeRasterEdit(layer, settings);
m_brushStroke = makeRasterEdit(layer, settings, m_tool == NavigationTool::brush);
m_brushStroke->clone = clone;
m_brushStroke->isBlur = m_tool == NavigationTool::blur;
m_brushStroke->append(point);
@@ -225,6 +225,16 @@ void EditorSession::finishBrush()
finishBrushImmediately();
}
// Grown past its layer, or placed already: keeps its place.
LayerMask EditorSession::placedMask(const LayerMask &mask, const ImportedImage &asset, const QRectF &bounds, const LayerTransform &transform,
const BrushStroke &stroke)
{
LayerMask painted = mask.replacing(asset);
if (mask.placement || bounds != stroke.sourceRect)
painted.placement = transform;
return painted;
}
// Immutable tiles land at once, undo entry included.
void EditorSession::commitPaintSnapshot(const BrushStroke &stroke)
{
@@ -249,7 +259,7 @@ void EditorSession::commitPaintSnapshot(const BrushStroke &stroke)
: QStringLiteral("Brush Stroke")));
ImageLayer &layer = m_document->layers[index];
if (stroke.isMask) {
layer.mask = current.mask ? current.mask->replacing(result.asset) : LayerMask(result.asset);
layer.mask = current.mask ? placedMask(*current.mask, result.asset, result.bounds, result.transform, stroke) : LayerMask(result.asset);
} else {
layer.asset = result.asset;
layer.transform = result.transform;
@@ -327,7 +337,8 @@ void EditorSession::finishRasterCommit()
beginEdit(pending.name);
ImageLayer &layer = m_document->layers[index];
if (stroke.isMask) {
layer.mask = current.mask ? current.mask->replacing(result.asset) : LayerMask(result.asset);
const QRectF bounds = result.pixelBounds.translated(stroke.committedBounds().topLeft());
layer.mask = current.mask ? placedMask(*current.mask, result.asset, bounds, transform, stroke) : LayerMask(result.asset);
} else {
layer.asset = result.asset;
layer.transform = transform;
+4 -1
View File
@@ -1,7 +1,7 @@
// Swift's EditorSession+Brush extension: a stroke from press to commit.
public:
bool canPaint() const;
std::unique_ptr<BrushStroke> makeRasterEdit(const ImageLayer &layer, const BrushSettings &settings = BrushSettings()) const;
std::unique_ptr<BrushStroke> makeRasterEdit(const ImageLayer &layer, const BrushSettings &settings = BrushSettings(), bool growsMask = false) const;
void beginBrush(QPointF point);
void continueBrush(QPointF point);
// Where a Shift-click's line starts; nil on another target.
@@ -22,5 +22,8 @@ public:
private:
void finishRasterCommit();
// A mask painted: grown or placed, it keeps its place.
static LayerMask placedMask(const LayerMask &mask, const ImportedImage &asset, const QRectF &bounds, const LayerTransform &transform,
const BrushStroke &stroke);
// Where Smoothing lets the brush go; none while slack.
std::optional<QPointF> smoothed(QPointF point);
+1 -1
View File
@@ -48,7 +48,7 @@ void EditorSession::beginGradient(QPointF point)
return;
finishOpacityEdit();
try {
m_gradientEdit = GradientEdit{std::shared_ptr<BrushStroke>(makeRasterEdit(*layer)), point, point};
m_gradientEdit = GradientEdit{std::shared_ptr<BrushStroke>(makeRasterEdit(*layer, BrushSettings(), true)), point, point};
++m_brushRevision;
notify();
} catch (const ProjectError &error) {
+2 -1
View File
@@ -86,7 +86,8 @@ void EditorSession::applyPixelEdit(const ImageLayer &layer, const QString &name,
{
finishOpacityEdit();
try {
std::unique_ptr<BrushStroke> edit = makeRasterEdit(layer);
// A fill on a mask covers the canvas too.
std::unique_ptr<BrushStroke> edit = makeRasterEdit(layer, BrushSettings(), true);
paint(*edit);
if (edit->patches().empty()) {
if (done)
+4 -4
View File
@@ -42,8 +42,8 @@ void CanvasView::drawStroke(const BrushStroke &stroke, const ImageLayer &layer,
// A placed mask: the layer draws through its own grid.
if (!layer.asset)
return;
const std::optional<QImage> preview = stroke.placedMaskPreview(*stroke.layer.mask->placement);
if (preview && drawSurface(placedMaskSurface(layer, stroke, *stroke.layer.mask->placement, *preview)))
const std::optional<QImage> preview = stroke.placedMaskPreview(stroke.paintTransform);
if (preview && drawSurface(placedMaskSurface(layer, stroke, *preview)))
return;
LayerRenderer::Options through = options;
through.mask = preview.value_or(QImage());
@@ -95,7 +95,7 @@ LayerEffectsSurface *CanvasView::strokeSurface(const ImageLayer &layer, const Br
}
// A placed mask paints its grid; the surface, the layer's.
LayerEffectsSurface *CanvasView::placedMaskSurface(const ImageLayer &layer, const BrushStroke &stroke, const LayerTransform &placement, const QImage &preview)
LayerEffectsSurface *CanvasView::placedMaskSurface(const ImageLayer &layer, const BrushStroke &stroke, const QImage &preview)
{
const LayerEffects effects = layer.effects.value_or(LayerEffects()).visible();
if (effects.isEmpty() || !effects.isValid() || !stroke.layer.asset)
@@ -106,7 +106,7 @@ LayerEffectsSurface *CanvasView::placedMaskSurface(const ImageLayer &layer, cons
m_strokeSurface = LayerEffectsSurface::make(layer.id, effects, full.size(), full);
if (!m_strokeSurface)
return nullptr;
const QTransform toGrid = BrushRaster::pixelToDocument(placement, stroke.width, stroke.height)
const QTransform toGrid = BrushRaster::pixelToDocument(stroke.paintTransform, stroke.width, stroke.height)
* BrushRaster::pixelToDocument(stroke.layer.transform, base.width(), base.height()).inverted();
m_strokeSurface->update(stroke.layer.asset, {}, std::nullopt, LayerEffectsSurface::MaskStroke{stroke.patches(), toGrid, [preview, full](const QRectF &region) {
QImage coverage = BrushRaster::context(int(region.width()), int(region.height()), true);
+3 -2
View File
@@ -342,8 +342,9 @@ void CanvasView::drawLayers(const CanvasDocument &document, double scale, const
.mask = mask.value_or(QImage()), .clip = clip});
return;
}
// A pixel stroke draws its grid by its own transform.
const LayerTransform transform = stroke && !stroke->isMask ? stroke->paintTransform : m_session.displayedTransform(layer);
// A stroke draws its grid, a placed mask's the layer's.
const bool placedStroke = stroke && stroke->isMask && stroke->layer.mask && stroke->layer.mask->placement;
const LayerTransform transform = stroke && !placedStroke ? stroke->paintTransform : m_session.displayedTransform(layer);
const std::optional<LayerTransform> placement = m_session.displayedMaskPlacement(layer);
std::optional<QImage> mask;
if (const std::optional<QImage> distorted = m_session.maskDistortPreview(layer)) {
+1 -1
View File
@@ -156,7 +156,7 @@ private:
void drawNewText(bool &drawn, double scale, const Center &center, QPainter &target, const QImage &clip);
void handOnDraftEffects(const CanvasDocument &document);
// Swift's effects surface on the canvas (EditorCanvas+Effects.cpp).
LayerEffectsSurface *placedMaskSurface(const ImageLayer &layer, const BrushStroke &stroke, const LayerTransform &placement, const QImage &preview);
LayerEffectsSurface *placedMaskSurface(const ImageLayer &layer, const BrushStroke &stroke, const QImage &preview);
LayerEffectsSurface *strokeSurface(const ImageLayer &layer, const BrushStroke &stroke, const std::optional<QImage> &mask);
// The stroke is over: its surface stands in meanwhile.
void handOnStrokeSurface();
+6 -6
View File
@@ -48,16 +48,16 @@ std::vector<BrushPatch> without(const BrushPatch &piece, const QRectF &overlap)
}
RasterSnapshot::RasterSnapshot(int width, int height, QImage base, QRectF baseRect, std::vector<BrushPatch> patches,
bool isMask, std::optional<QPointF> alignment)
bool isMask, std::optional<QPointF> alignment, double fill)
: width(width), height(height), base(std::move(base)), baseRect(baseRect), patches(std::move(patches)),
isMask(isMask), alignment(alignment.value_or(baseRect.topLeft()))
isMask(isMask), alignment(alignment.value_or(baseRect.topLeft())), fill(fill)
{
}
std::shared_ptr<const RasterSnapshot> RasterSnapshot::replacing(const std::optional<ImportedImage> &source,
const QRectF &sourceRect,
const std::vector<BrushPatch> &added,
const QRectF &crop, bool isMask)
const QRectF &crop, bool isMask, double fill)
{
const std::shared_ptr<const RasterSnapshot> old = source ? source->raster : nullptr;
const QPointF shift = sourceRect.topLeft() - crop.topLeft();
@@ -107,7 +107,7 @@ std::shared_ptr<const RasterSnapshot> RasterSnapshot::replacing(const std::optio
const QRectF baseRect = old ? old->baseRect.translated(shift) : sourceRect.translated(-crop.topLeft());
const QPointF alignment = sourceRect.topLeft() + (old ? old->alignment : QPointF(0, 0)) - crop.topLeft();
return std::make_shared<const RasterSnapshot>(int(crop.width()), int(crop.height()), base, baseRect,
std::move(inside), isMask, alignment);
std::move(inside), isMask, alignment, fill);
}
void RasterSnapshot::draw(const QRectF &rect, QPainter &context) const
@@ -115,9 +115,9 @@ void RasterSnapshot::draw(const QRectF &rect, QPainter &context) const
context.save();
context.setClipRect(rect, Qt::IntersectClip);
context.setRenderHint(QPainter::Antialiasing, false);
// A grown mask reveals the pixels outside it.
// Past its pixels a grown mask is its background.
if (isMask)
context.fillRect(rect, Qt::white);
context.fillRect(rect, QColor::fromRgbF(float(fill), float(fill), float(fill)));
const double sx = rect.width() / width, sy = rect.height() / height;
const auto mapped = [&](const QRectF &r) {
return QRectF(rect.left() + r.left() * sx, rect.top() + r.top() * sy, r.width() * sx, r.height() * sy);
+4 -2
View File
@@ -17,14 +17,16 @@ public:
const bool isMask;
// Origin of the halving grids, kept across commits.
const QPointF alignment;
// A mask beyond its base and patches: shown or hidden.
const double fill;
RasterSnapshot(int width, int height, QImage base, QRectF baseRect, std::vector<BrushPatch> patches,
bool isMask = false, std::optional<QPointF> alignment = std::nullopt);
bool isMask = false, std::optional<QPointF> alignment = std::nullopt, double fill = 1);
static std::shared_ptr<const RasterSnapshot> replacing(const std::optional<ImportedImage> &source,
const QRectF &sourceRect,
const std::vector<BrushPatch> &patches,
const QRectF &crop, bool isMask = false);
const QRectF &crop, bool isMask = false, double fill = 1);
void draw(const QRectF &rect, QPainter &context) const;
QImage makeImage() const;
+2 -20
View File
@@ -65,27 +65,9 @@ QPixmap CanvasThumbnail::layer(const QImage &image, const LayerTransform &transf
QPixmap CanvasThumbnail::mask(const QImage &image, const LayerTransform &transform, QSizeF canvas, double box)
{
return render(canvas, box, [&](QPainter &painter, QSize size, double scale) {
const double tone = edgeTone(image);
// White or black, as the canvas treats it, never gray.
const double tone = LayerMask::background(image);
painter.fillRect(QRect(QPoint(0, 0), size), QColor::fromRgbF(tone, tone, tone));
place(image, transform, scale, painter);
});
}
double CanvasThumbnail::edgeTone(const QImage &image)
{
const QImage gray = image.convertToFormat(QImage::Format_Grayscale8);
if (gray.width() <= 0 || gray.height() <= 0)
return 1;
qint64 total = 0, count = 0;
for (int y = 0; y < gray.height(); ++y) {
const uchar *row = gray.constScanLine(y);
const bool edgeRow = y == 0 || y == gray.height() - 1;
for (int x = 0; x < gray.width(); ++x) {
if (edgeRow || x == 0 || x == gray.width() - 1) {
total += row[x];
++count;
}
}
}
return count == 0 ? 1 : double(total) / double(count) / 255;
}
+1 -3
View File
@@ -13,8 +13,6 @@ inline constexpr double backingScale = 2;
QSize fittedSize(QSizeF canvas, double box);
// The checkerboard with the pixels placed; null: empty canvas.
QPixmap layer(const QImage &image, const LayerTransform &transform, QSizeF canvas, double box);
// The mask placed; its edge tone fills the rest.
// The mask placed; its background fills the rest.
QPixmap mask(const QImage &image, const LayerTransform &transform, QSizeF canvas, double box);
// The mean gray, 0 to 1, of the outermost pixels.
double edgeTone(const QImage &image);
}