Merge branch 'port-c'
This commit is contained in:
commit
e243751f64
54 files changed
+1428
-233
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 stroke's pixels or mask softened, in its own grid.
|
||||
std::optional<BrushStroke::Clone> blurSample(const BrushStroke &stroke) const;
|
||||
+25
-35
@@ -2,49 +2,39 @@
|
||||
#include "Document/PixelAdjust.h"
|
||||
#include "IO/ImageExporter.h"
|
||||
#include "Logging.h"
|
||||
#include "Rendering/LayerRenderer.h"
|
||||
#include <cmath>
|
||||
|
||||
// Swift's BlurTool extension: the layer, or its mask, softened.
|
||||
std::optional<QImage> EditorSession::blurSample(const CanvasDocument &document, bool mask) const
|
||||
// Swift's BlurTool extension: the layer's pixels, or mask, softened.
|
||||
std::optional<BrushStroke::Clone> EditorSession::blurSample(const BrushStroke &stroke) const
|
||||
{
|
||||
const std::optional<ImageLayer> layer = activeLayer();
|
||||
if (!layer)
|
||||
const ImageLayer &layer = stroke.layer;
|
||||
const std::optional<ImportedImage> &asset = stroke.isMask ? (layer.mask ? std::optional(layer.mask->asset) : std::nullopt) : layer.asset;
|
||||
if (!asset)
|
||||
return std::nullopt;
|
||||
// The softening follows the brush's size.
|
||||
const double sigma = std::min(30.0, std::max(1.5, m_brushSettings.diameter / 10));
|
||||
// The canvas's softening carried into the layer's pixels.
|
||||
const QTransform &map = stroke.pixelToDocument;
|
||||
const double perPixel = std::max(1e-6, std::sqrt(std::abs(map.m11() * map.m22() - map.m12() * map.m21())));
|
||||
const double sidePixels = std::max(stroke.sourceRect.width(), stroke.sourceRect.height());
|
||||
const double sigma = std::min(std::min(30.0, std::max(1.5, m_brushSettings.diameter / 10)) / perPixel, sidePixels / 2);
|
||||
// Room for the blur to spread past the pixels' edges.
|
||||
const double margin = std::ceil(3 * sigma);
|
||||
const QRectF region = stroke.sourceRect.adjusted(-margin, -margin, margin, margin);
|
||||
// A huge layer's sample is made coarser, within several canvases.
|
||||
const qint64 canvas = m_document ? qint64(m_document->width) * m_document->height : 0;
|
||||
const double budget = double(std::min(DocumentLimits::maxSurfacePixels, std::max<qint64>(16'000'000, 4 * canvas)));
|
||||
const double fit = std::min(1.0, std::sqrt(budget / (region.width() * region.height())));
|
||||
const int width = std::max(1, int(std::ceil(region.width() * fit))), height = std::max(1, int(std::ceil(region.height() * fit)));
|
||||
const QRectF placed(margin * fit, margin * fit, stroke.sourceRect.width() * fit, stroke.sourceRect.height() * fit);
|
||||
try {
|
||||
if (mask) {
|
||||
if (!layer->mask)
|
||||
return std::nullopt;
|
||||
QImage context = BrushRaster::context(document.width, document.height, true);
|
||||
// Past its pixels a mask keeps its edge tone.
|
||||
const int tone = int(std::round(LayerMask::background(layer->mask->asset.thumbnail) * 255));
|
||||
context.fill(QColor(tone, tone, tone));
|
||||
QPainter painter(&context);
|
||||
// Coverage only adds white: the mask's area starts black.
|
||||
const LayerTransform placement = layer->maskTransform();
|
||||
painter.save();
|
||||
QTransform box;
|
||||
box.translate(placement.center().x(), placement.center().y());
|
||||
box.rotateRadians(placement.radians());
|
||||
painter.setTransform(box, true);
|
||||
painter.fillRect(QRectF(-placement.size.width() / 2, -placement.size.height() / 2, placement.size.width(), placement.size.height()),
|
||||
Qt::black);
|
||||
painter.restore();
|
||||
LayerRenderer::drawCoverage(layer->mask->asset.image(), placement, painter);
|
||||
painter.end();
|
||||
return PixelAdjust::gaussianBlur(context, sigma, true);
|
||||
}
|
||||
if (!layer->asset)
|
||||
return std::nullopt;
|
||||
QImage context = BrushRaster::context(document.width, document.height, false);
|
||||
QImage context = BrushRaster::context(width, height, stroke.isMask);
|
||||
// Past its pixels a mask keeps its edge tone.
|
||||
if (stroke.isMask && layer.mask)
|
||||
context.fill(int(std::round(LayerMask::background(layer.mask->asset.thumbnail) * 255)));
|
||||
QPainter painter(&context);
|
||||
// A painted asset flattens here; the catch covers it too.
|
||||
const LayerTransform transform = displayedTransform(*layer);
|
||||
LayerRenderer::draw(layer->asset->image(), transform, transform.center(), painter, {});
|
||||
BrushRaster::draw(asset->image(), placed, painter);
|
||||
painter.end();
|
||||
return PixelAdjust::gaussianBlur(context, sigma, false);
|
||||
return BrushStroke::Clone{PixelAdjust::gaussianBlur(context, sigma * fit, stroke.isMask), region, true};
|
||||
} catch (const ExportError &error) {
|
||||
qCWarning(lcApp) << "the Blur tool cannot sample the layer:" << error.what();
|
||||
return std::nullopt;
|
||||
|
||||
@@ -58,6 +58,14 @@ LayerTransform BrushStroke::transform(const QRectF &bounds) const
|
||||
return result;
|
||||
}
|
||||
|
||||
// Offset into grid pixels, turned and scaled as the layer.
|
||||
QRectF BrushStroke::gridRect(const QRectF &rect, QSizeF offset) const
|
||||
{
|
||||
const QTransform toGrid = pixelToDocument.inverted();
|
||||
const QPointF shift = toGrid.map(QPointF(offset.width(), offset.height())) - toGrid.map(QPointF(0, 0));
|
||||
return rect.translated(-shift);
|
||||
}
|
||||
|
||||
// Swift's heal: the painted spot rebuilt from nearby texture.
|
||||
void BrushStroke::heal()
|
||||
{
|
||||
|
||||
@@ -273,11 +273,12 @@ QImage BrushStroke::clonePixels(const Tile &tile) const
|
||||
if (!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());
|
||||
// Colour even for gray clones: edges carry their coverage.
|
||||
const QImage &image = clone->image;
|
||||
// Tile pixels to clone pixels: grid, maybe document, then placed.
|
||||
const QRectF &placed = clone->placed;
|
||||
const QTransform toClone = QTransform::fromTranslate(tile.rect.left(), tile.rect.top()) * (clone->inGrid ? QTransform() : pixelToDocument)
|
||||
* QTransform::fromTranslate(-placed.left(), -placed.top()) * QTransform::fromScale(image.width() / placed.width(), image.height() / placed.height());
|
||||
if (toClone.type() > QTransform::TxTranslate) {
|
||||
// Swift's medium quality: bilinear under a scale or turn.
|
||||
QImage sample = BrushRaster::context(width, height, false);
|
||||
|
||||
@@ -111,10 +111,11 @@ public:
|
||||
qint64 pixelLimit = DocumentLimits::documentPixelBudget();
|
||||
// Limits every edit to the document selection; nil when none.
|
||||
std::optional<SelectionClip> selectionClip;
|
||||
// Clone Stamp: a document-sized image, and each point's offset.
|
||||
// Painted through the tip, placed: document or `inGrid` pixels.
|
||||
struct Clone {
|
||||
QImage image;
|
||||
QSizeF offset;
|
||||
QRectF placed;
|
||||
bool inGrid;
|
||||
};
|
||||
std::optional<Clone> clone;
|
||||
// Blur: the clone is the layer softened, in place.
|
||||
@@ -148,6 +149,8 @@ public:
|
||||
QRectF committedBounds() const;
|
||||
LayerTransform committedTransform() const;
|
||||
LayerTransform transform(const QRectF &bounds) const;
|
||||
// `rect` shifted to copy from `offset` document pixels away.
|
||||
QRectF gridRect(const QRectF &rect, QSizeF offset) const;
|
||||
// Spot Healing's end: the painted spot rebuilt from around it.
|
||||
void heal();
|
||||
PaintSnapshot paintSnapshot() const;
|
||||
|
||||
@@ -6,5 +6,5 @@ public:
|
||||
std::optional<QSizeF> cloneStrokeOffset(QPointF point) const;
|
||||
// The source for a brush at `point`: the canvas's crosshair.
|
||||
std::optional<QPointF> cloneSamplePoint(QPointF point) const;
|
||||
// What a stroke copies from, document-sized, taken as it starts.
|
||||
std::optional<QImage> cloneSample(const CanvasDocument &document) const;
|
||||
// What `stroke` copies, `offset` away: its pixels or the canvas.
|
||||
std::optional<BrushStroke::Clone> cloneSample(const CanvasDocument &document, const BrushStroke &stroke, QSizeF offset) const;
|
||||
@@ -1,7 +1,6 @@
|
||||
#include "Document/EditorSession.h"
|
||||
#include "IO/ImageExporter.h"
|
||||
#include "Logging.h"
|
||||
#include "Rendering/LayerRenderer.h"
|
||||
#include <cmath>
|
||||
|
||||
// Swift's CloneStamp extension: the source, its offset, the sample.
|
||||
@@ -40,20 +39,20 @@ std::optional<QPointF> EditorSession::cloneSamplePoint(QPointF point) const
|
||||
return point + QPointF(m_cloneOffset->width(), m_cloneOffset->height());
|
||||
}
|
||||
|
||||
std::optional<QImage> EditorSession::cloneSample(const CanvasDocument &document) const
|
||||
std::optional<BrushStroke::Clone> EditorSession::cloneSample(const CanvasDocument &document, const BrushStroke &stroke, QSizeF offset) const
|
||||
{
|
||||
try {
|
||||
// A painted asset flattens here; the catch covers it too.
|
||||
if (!m_cloneSettings.sampleAllLayers) {
|
||||
if (!stroke.layer.asset)
|
||||
return std::nullopt;
|
||||
return BrushStroke::Clone{stroke.layer.asset->image(), stroke.gridRect(stroke.sourceRect, offset), true};
|
||||
}
|
||||
QImage context = BrushRaster::context(document.width, document.height, false);
|
||||
QPainter painter(&context);
|
||||
// A painted asset flattens here; the catch covers it too.
|
||||
if (m_cloneSettings.sampleAllLayers) {
|
||||
drawLiveComposite(document, painter);
|
||||
} else if (const std::optional<ImageLayer> layer = activeLayer(); layer && layer->asset) {
|
||||
const LayerTransform transform = displayedTransform(*layer);
|
||||
LayerRenderer::draw(layer->asset->image(), transform, transform.center(), painter, {});
|
||||
}
|
||||
drawLiveComposite(document, painter);
|
||||
painter.end();
|
||||
return context;
|
||||
return BrushStroke::Clone{context, QRectF(-offset.width(), -offset.height(), context.width(), context.height()), false};
|
||||
} catch (const ExportError &error) {
|
||||
qCWarning(lcApp) << "Clone Stamp cannot sample the canvas:" << error.what();
|
||||
return std::nullopt;
|
||||
|
||||
@@ -37,6 +37,27 @@ bool EditorSession::canPaint() const
|
||||
&& (m_isMaskSelected || !layer->adjustment);
|
||||
}
|
||||
|
||||
std::optional<QString> EditorSession::paintRefusal() const
|
||||
{
|
||||
const std::optional<ImageLayer> layer = activeLayer();
|
||||
if (!canEditLayers() || !layer || canPaint())
|
||||
return std::nullopt;
|
||||
if (m_selectedLayerIDs.size() > 1)
|
||||
return QStringLiteral("Several layers are selected. Select just one to paint on it.");
|
||||
if (layer->isGroup && !m_isMaskSelected)
|
||||
return QStringLiteral("“%1” is a folder, which has no pixels of its own. Paint on a layer inside it, or on the folder’s mask.").arg(layer->name);
|
||||
if (!m_document->effectiveVisibleIDs().contains(layer->id))
|
||||
return QStringLiteral("“%1” is hidden, or inside a hidden folder. Show it to paint on it.").arg(layer->name);
|
||||
if (m_isMaskSelected && !(layer->mask && layer->mask->isEnabled))
|
||||
return QStringLiteral("The layer mask is turned off. Shift-click its thumbnail to turn it on, then paint.");
|
||||
if (!m_isMaskSelected && layer->adjustment)
|
||||
return QStringLiteral("“%1” is an adjustment layer, with no pixels to paint. Paint on its mask instead.").arg(layer->name);
|
||||
const std::optional<DocumentSelection> current = selection();
|
||||
if (current && current->isEmpty())
|
||||
return QStringLiteral("Nothing is selected, so there’s nowhere to paint. Choose Select › Deselect (Ctrl+D) to paint anywhere.");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// A tiled raster edit of pixels or mask, within budget.
|
||||
std::unique_ptr<BrushStroke> EditorSession::makeRasterEdit(const ImageLayer &layer, const BrushSettings &settings, bool growsMask) const
|
||||
{
|
||||
@@ -74,28 +95,20 @@ void EditorSession::beginBrush(QPointF point)
|
||||
}
|
||||
// Spot Healing and Clone Stamp rework pixels, never a mask.
|
||||
const bool paintsMasks = m_tool == NavigationTool::brush || m_tool == NavigationTool::blur;
|
||||
if ((!paintsMasks && (!isBrushTool(m_tool) || m_isMaskSelected)) || !canPaint())
|
||||
if (!paintsMasks && (!isBrushTool(m_tool) || m_isMaskSelected))
|
||||
return;
|
||||
if (!canPaint()) {
|
||||
setBrushError(paintRefusal());
|
||||
return;
|
||||
}
|
||||
const ImageLayer layer = activeLayer().value();
|
||||
std::optional<BrushStroke::Clone> clone;
|
||||
std::optional<QSizeF> sourceOffset;
|
||||
if (m_tool == NavigationTool::cloneStamp) {
|
||||
const std::optional<QSizeF> offset = cloneStrokeOffset(point);
|
||||
if (!offset) {
|
||||
sourceOffset = cloneStrokeOffset(point);
|
||||
if (!sourceOffset) {
|
||||
setBrushError(QStringLiteral("Alt-click where Clone Stamp should copy from first."));
|
||||
return;
|
||||
}
|
||||
const std::optional<QImage> image = cloneSample(m_document.value());
|
||||
if (!image)
|
||||
return;
|
||||
m_cloneOffset = 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)
|
||||
return;
|
||||
clone = BrushStroke::Clone{*image, QSizeF(0, 0)};
|
||||
}
|
||||
finishOpacityEdit();
|
||||
try {
|
||||
@@ -108,9 +121,21 @@ void EditorSession::beginBrush(QPointF point)
|
||||
settings.green = settings.red;
|
||||
settings.blue = settings.red;
|
||||
}
|
||||
m_brushStroke = makeRasterEdit(layer, settings, m_tool == NavigationTool::brush);
|
||||
m_brushStroke->clone = clone;
|
||||
m_brushStroke->isBlur = m_tool == NavigationTool::blur;
|
||||
std::unique_ptr<BrushStroke> stroke = makeRasterEdit(layer, settings, m_tool == NavigationTool::brush);
|
||||
if (sourceOffset) {
|
||||
stroke->clone = cloneSample(m_document.value(), *stroke, *sourceOffset);
|
||||
if (!stroke->clone)
|
||||
return;
|
||||
m_cloneOffset = sourceOffset;
|
||||
}
|
||||
// Blur paints a softened copy of the layer, in place.
|
||||
if (m_tool == NavigationTool::blur) {
|
||||
stroke->clone = blurSample(*stroke);
|
||||
if (!stroke->clone)
|
||||
return;
|
||||
}
|
||||
stroke->isBlur = m_tool == NavigationTool::blur;
|
||||
m_brushStroke = std::move(stroke);
|
||||
m_brushStroke->append(point);
|
||||
m_brushAnchor = point;
|
||||
m_brushPointer = point;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Swift's EditorSession+Brush extension: a stroke from press to commit.
|
||||
public:
|
||||
bool canPaint() const;
|
||||
// Why the target refuses a stroke; none while busy.
|
||||
std::optional<QString> paintRefusal() 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);
|
||||
|
||||
@@ -27,7 +27,7 @@ std::optional<ProjectSnapshot> EditorSession::projectSnapshot() const
|
||||
void EditorSession::installProject(const ProjectSnapshot &snapshot, const QString &path)
|
||||
{
|
||||
m_collapsedGroupIDs.clear();
|
||||
m_isMaskSelected = false;
|
||||
setIsMaskSelected(false);
|
||||
m_cropRect.reset();
|
||||
m_guideDrag = std::nullopt;
|
||||
m_transformEdit = std::nullopt;
|
||||
@@ -78,7 +78,7 @@ void EditorSession::setProjectPath(std::optional<QString> path)
|
||||
void EditorSession::clearProject()
|
||||
{
|
||||
m_collapsedGroupIDs.clear();
|
||||
m_isMaskSelected = false;
|
||||
setIsMaskSelected(false);
|
||||
m_cropRect.reset();
|
||||
m_transformEdit = std::nullopt;
|
||||
m_guideDrag = std::nullopt;
|
||||
|
||||
@@ -35,7 +35,7 @@ void EditorSession::setActiveLayerID(std::optional<QUuid> id)
|
||||
{
|
||||
// Another layer: its pixels are the target again.
|
||||
if (id != m_activeLayerID)
|
||||
m_isMaskSelected = false;
|
||||
setIsMaskSelected(false);
|
||||
m_activeLayerID = id;
|
||||
m_selectedLayerIDs = id ? QSet<QUuid>{*id} : QSet<QUuid>();
|
||||
notify();
|
||||
@@ -234,7 +234,7 @@ void EditorSession::restore(const DocumentHistory::Snapshot &snapshot)
|
||||
m_document = snapshot.document;
|
||||
setActiveLayerID(snapshot.activeLayerID);
|
||||
const std::optional<ImageLayer> active = activeLayer();
|
||||
m_isMaskSelected = keepsMaskTarget && active && active->mask;
|
||||
setIsMaskSelected(keepsMaskTarget && active && active->mask);
|
||||
if (changedCanvas && m_document)
|
||||
viewport.fit(m_document->size());
|
||||
qCInfo(lcApp) << "restored a history snapshot of" << (m_document ? int(m_document->layers.size()) : 0) << "layers";
|
||||
@@ -381,3 +381,18 @@ void EditorSession::requestCanvasFocus()
|
||||
++m_canvasFocusRequest;
|
||||
notify();
|
||||
}
|
||||
|
||||
void EditorSession::setIsMaskSelected(bool value)
|
||||
{
|
||||
m_isMaskSelected = value;
|
||||
if (!value)
|
||||
m_viewsMaskAlone = false;
|
||||
}
|
||||
|
||||
void EditorSession::setViewsMaskAlone(bool value)
|
||||
{
|
||||
if (m_viewsMaskAlone == value)
|
||||
return;
|
||||
m_viewsMaskAlone = value;
|
||||
notify();
|
||||
}
|
||||
@@ -93,10 +93,11 @@ public:
|
||||
void selectTool(NavigationTool value);
|
||||
// Tab: the current tool's next mode.
|
||||
void cycleToolMode();
|
||||
|
||||
// Whether the mask thumbnail, not the layer's, is the target.
|
||||
bool isMaskSelected() const { return m_isMaskSelected; }
|
||||
|
||||
// Alt-click: the targeted mask alone, gray, on the canvas.
|
||||
bool viewsMaskAlone() const { return m_viewsMaskAlone; }
|
||||
void setViewsMaskAlone(bool value);
|
||||
// A blend mode tried on by hovering; nothing is saved.
|
||||
struct BlendPreview {
|
||||
QUuid layerID;
|
||||
@@ -132,7 +133,6 @@ public:
|
||||
// An unlinked mask, selected, transforms without its layer.
|
||||
bool transformTargetsMask() const;
|
||||
void nudgeLayer(double dx, double dy);
|
||||
|
||||
const std::optional<LassoDraft> &lassoDraft() const { return m_lassoDraft; }
|
||||
LassoKind lassoKind() const { return m_lassoKind; }
|
||||
void setLassoKind(LassoKind kind);
|
||||
@@ -350,6 +350,7 @@ private:
|
||||
std::optional<BlendPreview> m_blendPreview;
|
||||
std::optional<QUuid> m_opacityEditLayerID;
|
||||
bool m_isMaskSelected = false;
|
||||
bool m_viewsMaskAlone = false;
|
||||
std::optional<QUuid> m_activeLayerID;
|
||||
bool m_isProjectBusy = false;
|
||||
bool m_showsNewDocument = false;
|
||||
|
||||
@@ -347,7 +347,7 @@ void EditorSession::finishFilterCommit()
|
||||
mask.isEnabled = true;
|
||||
mask.placement = std::nullopt;
|
||||
m_document->layers[index].mask = mask;
|
||||
m_isMaskSelected = true;
|
||||
setIsMaskSelected(true);
|
||||
endEdit();
|
||||
finish();
|
||||
return;
|
||||
|
||||
@@ -35,8 +35,13 @@ void EditorSession::setGradientSettings(const GradientSettings &settings)
|
||||
void EditorSession::beginGradient(QPointF point)
|
||||
{
|
||||
const std::optional<ImageLayer> layer = activeLayer();
|
||||
if (m_tool != NavigationTool::gradient || !(canPaint() || m_gradientEdit) || !layer)
|
||||
if (m_tool != NavigationTool::gradient || !layer)
|
||||
return;
|
||||
// Swift explains only past this guard, where nothing refuses.
|
||||
if (!canPaint() && !m_gradientEdit) {
|
||||
setBrushError(paintRefusal());
|
||||
return;
|
||||
}
|
||||
// A new line on this target replaces the pending one.
|
||||
if (m_gradientEdit && m_gradientEdit->raster->layer.id == layer->id && m_gradientEdit->raster->isMask == m_isMaskSelected) {
|
||||
m_gradientEdit->start = point;
|
||||
|
||||
@@ -97,7 +97,7 @@ void EditorSession::selectEffect(LayerEffectKind kind, QUuid id, bool editing)
|
||||
finishEffectsEditing(false);
|
||||
// Swift's `selectedLayerIDs = [id]`: selectLayer sets it, unrefused here.
|
||||
selectLayer(id);
|
||||
m_isMaskSelected = false;
|
||||
setIsMaskSelected(false);
|
||||
m_effectSelection = selection;
|
||||
if (editing && m_effectsEditing != selection) {
|
||||
m_effectsEditingOriginal = m_document->layers[size_t(indexOf(m_document->layers, id))].effects.value_or(LayerEffects());
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
#include "Document/EditorSession.h"
|
||||
|
||||
// Swift's mask shown alone, an Alt-click on its thumbnail.
|
||||
std::optional<ImageLayer> EditorSession::maskAloneLayer() const
|
||||
{
|
||||
const std::optional<ImageLayer> layer = activeLayer();
|
||||
if (!m_viewsMaskAlone || !m_isMaskSelected || !layer || !layer->mask)
|
||||
return std::nullopt;
|
||||
return layer;
|
||||
}
|
||||
|
||||
void EditorSession::toggleMaskAlone(QUuid id)
|
||||
{
|
||||
const std::optional<ImageLayer> shown = maskAloneLayer();
|
||||
const bool showing = shown && shown->id == id;
|
||||
selectLayerTarget(id, true);
|
||||
if (m_activeLayerID != id || !m_isMaskSelected)
|
||||
return;
|
||||
m_viewsMaskAlone = !showing;
|
||||
notify();
|
||||
}
|
||||
@@ -15,6 +15,12 @@ public:
|
||||
std::optional<QImage> maskDistortPreview(const ImageLayer &layer) const;
|
||||
// The selection made a mask; without one, a solid mask.
|
||||
void addMask(bool revealing = true);
|
||||
// The layer whose mask the canvas shows alone, if any.
|
||||
std::optional<ImageLayer> maskAloneLayer() const;
|
||||
// Alt-click on a mask thumbnail: alone, or the composite again.
|
||||
void toggleMaskAlone(QUuid id);
|
||||
|
||||
private:
|
||||
// Swift's didSet: the pixels targeted end the mask view.
|
||||
void setIsMaskSelected(bool value);
|
||||
void commitMaskTransform();
|
||||
@@ -262,7 +262,7 @@ void EditorSession::selectLayerTarget(QUuid id, bool mask)
|
||||
resolveGradient();
|
||||
selectLayer(id);
|
||||
const std::optional<ImageLayer> active = activeLayer();
|
||||
m_isMaskSelected = mask && active && active->mask;
|
||||
setIsMaskSelected(mask && active && active->mask);
|
||||
notify();
|
||||
}
|
||||
|
||||
@@ -286,14 +286,15 @@ void EditorSession::addMask(bool revealing)
|
||||
// The selection's coverage, soft where it is feathered.
|
||||
QImage image = PixelAdjust::coverage(current->clip(m_document->size()), width, height,
|
||||
BrushRaster::pixelToDocument(active->transform, width, height));
|
||||
if (revealing)
|
||||
// Revealing, white inside; hiding, black inside.
|
||||
if (!revealing)
|
||||
image.invertPixels();
|
||||
const LayerMask mask(LayerMask::assetFrom(image));
|
||||
finishOpacityEdit();
|
||||
beginEdit(QStringLiteral("Add Mask from Selection"));
|
||||
beginEdit(revealing ? QStringLiteral("Reveal Selection") : QStringLiteral("Hide Selection"));
|
||||
m_document->layers[size_t(index)].mask = mask;
|
||||
m_document->selection = std::nullopt;
|
||||
m_isMaskSelected = true;
|
||||
setIsMaskSelected(true);
|
||||
endEdit();
|
||||
} catch (const std::runtime_error &error) {
|
||||
qCWarning(lcApp).noquote() << "cannot make a mask from the selection:" << error.what();
|
||||
@@ -312,7 +313,7 @@ void EditorSession::addLayerMask(bool revealing)
|
||||
if (layer.id == active->id)
|
||||
layer.mask = LayerMask::solid(revealing);
|
||||
}
|
||||
m_isMaskSelected = true;
|
||||
setIsMaskSelected(true);
|
||||
endEdit();
|
||||
}
|
||||
|
||||
@@ -341,7 +342,7 @@ void EditorSession::deleteLayerMask()
|
||||
if (layer.id == active->id)
|
||||
layer.mask = std::nullopt;
|
||||
}
|
||||
m_isMaskSelected = false;
|
||||
setIsMaskSelected(false);
|
||||
endEdit();
|
||||
}
|
||||
|
||||
@@ -371,7 +372,7 @@ void EditorSession::copyMask(QUuid source, QUuid target)
|
||||
beginEdit(find(target)->mask ? QStringLiteral("Replace Layer Mask") : QStringLiteral("Copy Layer Mask"));
|
||||
find(target)->mask = mask;
|
||||
selectLayer(target);
|
||||
m_isMaskSelected = true;
|
||||
setIsMaskSelected(true);
|
||||
endEdit();
|
||||
}
|
||||
|
||||
|
||||
@@ -420,5 +420,7 @@ void EditorSession::invertSelection()
|
||||
const std::optional<DocumentSelection> current = selection();
|
||||
if (!m_document || !current)
|
||||
return;
|
||||
setSelection(DocumentSelection{canvasPath(*m_document).subtracted(current->path), current->antialiased, current->feather}, QStringLiteral("Inverse"));
|
||||
const DocumentSelection inverse{canvasPath(*m_document).subtracted(current->path), current->antialiased, current->feather};
|
||||
// The inverse of everything is no selection, as Photoshop's.
|
||||
setSelection(inverse.isEmpty() ? std::nullopt : std::optional(inverse), QStringLiteral("Inverse"));
|
||||
}
|
||||
@@ -196,8 +196,7 @@ void EditorSession::beginWarp(QPointF point)
|
||||
{
|
||||
const std::optional<ImageLayer> layer = activeLayer();
|
||||
if (!canPaint() || m_isMaskSelected || !layer || !layer->asset || !m_document) {
|
||||
if (m_isMaskSelected)
|
||||
setBrushError(QStringLiteral("Smudge and Liquify work on a layer's pixels, not its mask."));
|
||||
setBrushError(m_isMaskSelected ? std::optional(QStringLiteral("Smudge and Liquify work on a layer's pixels, not its mask.")) : paintRefusal());
|
||||
return;
|
||||
}
|
||||
finishOpacityEdit();
|
||||
@@ -235,7 +234,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->clone = BrushStroke::Clone{warp->image(), QRectF(QPointF(0, 0), warp->image().size()), false};
|
||||
stroke->replacesWithClone = true;
|
||||
stroke->editName = rawValue(warp->mode);
|
||||
for (const QPointF step : warp->points())
|
||||
|
||||
Reference in new issue
Block a user