Masks painted anywhere on the canvas; mask thumbnails' ground white or black (12.12b)
This commit is contained in:
1 parent
136ee65a48
commit
f335818e53
20 files changed
+348
-68
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user