Crop ratios and selection start, Vignette on empty layers, text colour previews (12.10c); mutation runs use 12 cores
This commit is contained in:
1 parent
7a873eb9c7
commit
ffba1753c6
27 files changed
+555
-55
No files matched your search
@@ -25,6 +25,8 @@ public:
|
||||
void openEffectColorPicker(LayerEffectKind kind);
|
||||
// The effect follows the picker's working colour.
|
||||
void previewEffectColor();
|
||||
// The open text draft previews the picker's working colour.
|
||||
void previewTextColor();
|
||||
// Swift writes the picker's hsb in place; here, the session.
|
||||
void setColorPickerHSB(const PickerHSB &hsb);
|
||||
void sampleIntoColorPicker(QPointF point);
|
||||
@@ -32,5 +34,6 @@ public:
|
||||
std::optional<PaletteColor> sampleCompositeColor(QPointF point) const;
|
||||
|
||||
private:
|
||||
void paintText(const PaletteColor &color);
|
||||
void setGradientMapColor(const PaletteColor &color, bool highlights);
|
||||
void setVignetteColor(const PaletteColor &color);
|
||||
@@ -195,7 +195,11 @@ void EditorSession::openColorPicker(bool background)
|
||||
{
|
||||
if (!canEditPalette() || m_isMaskSelected)
|
||||
return;
|
||||
m_colorPicker = ColorPickerState({ColorPickerTarget::Kind::palette, background}, paletteColor(background));
|
||||
ColorPickerState picker({ColorPickerTarget::Kind::palette, background}, paletteColor(background));
|
||||
// Open text follows the foreground: it previews, Cancel restores.
|
||||
if (!background && m_tool == NavigationTool::type && m_textDraft)
|
||||
picker.editedText = ColorPickerState::EditedText{m_textDraft->id, {m_textDraft->style.red, m_textDraft->style.green, m_textDraft->style.blue}};
|
||||
m_colorPicker = picker;
|
||||
notify();
|
||||
}
|
||||
|
||||
@@ -225,20 +229,21 @@ void EditorSession::closeColorPicker(bool commit)
|
||||
const std::optional<QUuid> draftID = m_textDraft ? std::optional(m_textDraft->id) : std::nullopt;
|
||||
if (commit && picker.target.kind == ColorPickerTarget::Kind::palette && !m_isMaskSelected) {
|
||||
setPaletteColor(color, picker.target.background);
|
||||
} else if (commit && picker.target.kind == ColorPickerTarget::Kind::text && m_tool == NavigationTool::type && draftID == picker.target.draftID) {
|
||||
} else if (!commit && picker.target.kind == ColorPickerTarget::Kind::palette && picker.editedText && m_tool == NavigationTool::type
|
||||
&& draftID == picker.editedText->draftID) {
|
||||
paintText(picker.editedText->color);
|
||||
} else if (picker.target.kind == ColorPickerTarget::Kind::text && m_tool == NavigationTool::type && draftID == picker.target.draftID) {
|
||||
// The text previewed the working colour; Cancel puts it back.
|
||||
const PaletteColor chosen = commit ? color : picker.original;
|
||||
if (draftID) {
|
||||
changeTextStyle([&color](LayerTextStyle &style) {
|
||||
style.red = color.red;
|
||||
style.green = color.green;
|
||||
style.blue = color.blue;
|
||||
});
|
||||
} else {
|
||||
m_textDefaults.red = color.red;
|
||||
m_textDefaults.green = color.green;
|
||||
m_textDefaults.blue = color.blue;
|
||||
paintText(chosen);
|
||||
} else if (commit) {
|
||||
m_textDefaults.red = chosen.red;
|
||||
m_textDefaults.green = chosen.green;
|
||||
m_textDefaults.blue = chosen.blue;
|
||||
}
|
||||
// The text colour is the foreground: the swatch follows.
|
||||
if (!m_isMaskSelected)
|
||||
if (commit && !m_isMaskSelected)
|
||||
setForegroundColor(color);
|
||||
} else if (picker.target.kind == ColorPickerTarget::Kind::gradientMap) {
|
||||
// The end previewed the working colour; Cancel puts it back.
|
||||
@@ -280,6 +285,29 @@ void EditorSession::openEffectColorPicker(LayerEffectKind kind)
|
||||
notify();
|
||||
}
|
||||
|
||||
void EditorSession::previewTextColor()
|
||||
{
|
||||
if (!m_colorPicker)
|
||||
return;
|
||||
const ColorPickerTarget &target = m_colorPicker->target;
|
||||
const std::optional<QUuid> draftID = target.kind == ColorPickerTarget::Kind::text ? target.draftID
|
||||
: target.kind == ColorPickerTarget::Kind::palette && m_colorPicker->editedText
|
||||
? std::optional(m_colorPicker->editedText->draftID)
|
||||
: std::nullopt;
|
||||
if (draftID && m_tool == NavigationTool::type && m_textDraft && m_textDraft->id == *draftID)
|
||||
paintText(m_colorPicker->color());
|
||||
}
|
||||
|
||||
// The open draft's colour.
|
||||
void EditorSession::paintText(const PaletteColor &color)
|
||||
{
|
||||
changeTextStyle([&color](LayerTextStyle &style) {
|
||||
style.red = color.red;
|
||||
style.green = color.green;
|
||||
style.blue = color.blue;
|
||||
});
|
||||
}
|
||||
|
||||
void EditorSession::previewEffectColor()
|
||||
{
|
||||
if (!m_colorPicker || m_colorPicker->target.kind != ColorPickerTarget::Kind::effect)
|
||||
|
||||
@@ -57,5 +57,11 @@ struct ColorPickerState {
|
||||
PickerHSB hsb;
|
||||
// Swift's object identity: a new picker opens its panel anew.
|
||||
QUuid id = QUuid::createUuid();
|
||||
// The foreground picker over open text: that text, its colour.
|
||||
struct EditedText {
|
||||
QUuid draftID;
|
||||
PaletteColor color;
|
||||
};
|
||||
std::optional<EditedText> editedText;
|
||||
PaletteColor color() const { return hsb.rgb().quantized(); }
|
||||
};
|
||||
@@ -188,8 +188,12 @@ std::optional<double> EditorSession::cropRatio() const
|
||||
return 1;
|
||||
if (m_cropRatioChoice == QStringLiteral("4:3"))
|
||||
return 4.0 / 3;
|
||||
if (m_cropRatioChoice == QStringLiteral("3:4"))
|
||||
return 3.0 / 4;
|
||||
if (m_cropRatioChoice == QStringLiteral("16:9"))
|
||||
return 16.0 / 9;
|
||||
if (m_cropRatioChoice == QStringLiteral("9:16"))
|
||||
return 9.0 / 16;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "Document/EditorSession.h"
|
||||
#include "Document/Crop.h"
|
||||
#include "Logging.h"
|
||||
#include <cmath>
|
||||
|
||||
@@ -146,10 +147,19 @@ void EditorSession::selectTool(NavigationTool value)
|
||||
m_brushSettings.opacity = parked.opacity;
|
||||
}
|
||||
m_tool = value;
|
||||
// The Crop tool opens on the whole canvas, freely.
|
||||
// The Crop tool opens freely: the selection, else the canvas.
|
||||
if (value == NavigationTool::crop && !m_cropRect && m_document) {
|
||||
m_cropRatioChoice = QStringLiteral("Free");
|
||||
m_cropRect = QRectF(QPointF(0, 0), m_document->size());
|
||||
const QRectF canvas(QPointF(0, 0), m_document->size());
|
||||
m_cropRect = canvas;
|
||||
if (m_document->selection && !m_document->selection->isEmpty()) {
|
||||
// CoreGraphics' `integral`: whole pixels, rounded outwards.
|
||||
const QRectF box = m_document->selection->path.boundingRect();
|
||||
const QRectF whole = QRectF(QPointF(std::floor(box.left()), std::floor(box.top())), QPointF(std::ceil(box.right()), std::ceil(box.bottom())))
|
||||
.intersected(canvas);
|
||||
if (CropGeometry::valid(whole))
|
||||
m_cropRect = whole;
|
||||
}
|
||||
}
|
||||
notify();
|
||||
}
|
||||
|
||||
@@ -20,8 +20,8 @@ void EditorSession::beginFilter(FilterKind kind)
|
||||
{
|
||||
if (kind == FilterKind::contentAwareFill && !canContentAwareFill())
|
||||
return;
|
||||
// Swift beeps; `canAdjustColors` reads the open filter.
|
||||
if (m_hueSaturation || !canAdjustColors()) {
|
||||
// Swift beeps; the gates read the open filter.
|
||||
if (m_hueSaturation || !(kind == FilterKind::vignette ? canVignette() : canAdjustColors())) {
|
||||
qCWarning(lcApp) << "a filter cannot open here";
|
||||
return;
|
||||
}
|
||||
@@ -33,7 +33,7 @@ void EditorSession::beginFilter(FilterKind kind)
|
||||
commitTransform();
|
||||
cancelCrop();
|
||||
cancelLasso();
|
||||
const ImageLayer layer = activeLayer().value();
|
||||
ImageLayer layer = activeLayer().value();
|
||||
const QSizeF size = m_document->size();
|
||||
const std::optional<DocumentSelection> current = selection();
|
||||
FilterSettings settings = m_filterSettings;
|
||||
@@ -41,14 +41,26 @@ void EditorSession::beginFilter(FilterKind kind)
|
||||
if (kind == FilterKind::gradientMap)
|
||||
settings.gradientMap = GradientMapSettings{AdjustmentColor(foregroundColor()), AdjustmentColor(m_backgroundColor)};
|
||||
try {
|
||||
// Content-Aware Fill extends the layer over the selection's reach.
|
||||
std::optional<QRectF> area;
|
||||
// An empty layer has no pixels: Vignette starts them clear.
|
||||
const bool startedEmpty = !layer.asset;
|
||||
if (startedEmpty) {
|
||||
const QImage clear = BrushRaster::context(std::max(1, int(std::round(layer.transform.size.width()))),
|
||||
std::max(1, int(std::round(layer.transform.size.height()))), false);
|
||||
layer.asset = ImportedImage(clear, PixelAdjust::thumbnail(clear), layer.name);
|
||||
}
|
||||
// Fill reaches the selection; Vignette the canvas it frames.
|
||||
const QRectF canvas(QPointF(0, 0), size);
|
||||
const bool fillsCanvas = kind == FilterKind::vignette && startedEmpty;
|
||||
std::optional<QRectF> area = fillsCanvas ? std::optional(canvas) : std::nullopt;
|
||||
if (kind == FilterKind::contentAwareFill && current) {
|
||||
const QRectF box = current->path.boundingRect().intersected(QRectF(QPointF(0, 0), size));
|
||||
const QRectF box = current->path.boundingRect().intersected(canvas);
|
||||
if (!box.isEmpty())
|
||||
area = box;
|
||||
}
|
||||
m_filterEdit.emplace(kind, layer, current ? std::optional(current->clip(size)) : std::nullopt, settings, area);
|
||||
if (fillsCanvas)
|
||||
m_filterEdit->canvas = canvas;
|
||||
m_filterEdit->startedEmpty = startedEmpty;
|
||||
updateFilter(m_filterEdit->settings, true);
|
||||
} catch (const ProjectError &error) {
|
||||
setBrushError(QString::fromUtf8(error.what()));
|
||||
@@ -239,9 +251,11 @@ void EditorSession::commitFilter(std::function<void()> done)
|
||||
// A painted layer flattens in the worker, which catches failures.
|
||||
m_filterCommit.setFuture(QtConcurrent::run([kind = edit.kind, original = edit.original, grownImage = edit.grownImage, settings = rendered,
|
||||
selection = edit.selection, mapping = edit.mapping, seed = edit.seed, cached,
|
||||
grown = edit.grownTransform, spreads]() -> FilterMade {
|
||||
grown = edit.grownTransform, spreads, canvas = edit.canvas]() -> FilterMade {
|
||||
try {
|
||||
QImage image = cached ? *cached : PixelFilter::run(FilterJob{kind, grownImage ? *grownImage : original.image(), settings, 1, selection, mapping, seed});
|
||||
FilterJob job{kind, grownImage ? *grownImage : original.image(), settings, 1, selection, mapping, seed};
|
||||
job.canvas = canvas;
|
||||
QImage image = cached ? *cached : PixelFilter::run(job);
|
||||
std::optional<LayerTransform> placed = grown;
|
||||
// A blur is cut back to the pixels it left.
|
||||
if (spreads && grown) {
|
||||
@@ -316,7 +330,9 @@ void EditorSession::finishFilterCommit()
|
||||
return;
|
||||
}
|
||||
const ImageLayer current = m_document->layers[index];
|
||||
if (!current.asset || current.asset->identity() != edit.original.identity() || current.transform != edit.transform) {
|
||||
// An empty layer the filter began on stays empty.
|
||||
const bool kept = current.asset ? current.asset->identity() == edit.original.identity() : edit.startedEmpty;
|
||||
if (!kept || current.transform != edit.transform) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ FilterSettings FilterSettings::normalized() const
|
||||
result.vignetteAmount = clamp(vignetteAmount, 0, 100, 35);
|
||||
result.vignetteColor = vignetteColor.clamped();
|
||||
result.vignetteMidpoint = clamp(vignetteMidpoint, 0, 100, 50);
|
||||
result.vignetteRoundness = clamp(vignetteRoundness, -100, 100, 0);
|
||||
result.vignetteRoundness = clamp(vignetteRoundness, -100, 100, 100);
|
||||
result.vignetteFeather = clamp(vignetteFeather, 0, 100, 60);
|
||||
result.vignetteHighlights = clamp(vignetteHighlights, 0, 100, 25);
|
||||
result.bloomAmount = clamp(bloomAmount, 0, 100, 40);
|
||||
@@ -167,12 +167,16 @@ QImage PixelFilter::run(const FilterJob &job)
|
||||
noise_add_at(image.bits(), size_t(width), size_t(height), size_t(image.bytesPerLine()), float(settings.amount), settings.gaussian ? 1 : 0,
|
||||
settings.monochromatic ? 1 : 0, job.seed, int64_t(std::floor(job.noiseOrigin.x())), int64_t(std::floor(job.noiseOrigin.y())));
|
||||
break;
|
||||
case FilterKind::vignette:
|
||||
case FilterKind::vignette: {
|
||||
image = drawn(job.image);
|
||||
adjust_colored_vignette(image.bits(), size_t(width), size_t(height), size_t(image.bytesPerLine()), settings.vignetteAmount,
|
||||
// The canvas in this image's pixels, else the image itself.
|
||||
const QRectF frame = job.canvas ? job.mapping.inverted().mapRect(*job.canvas) : QRectF(0, 0, width, height);
|
||||
adjust_colored_vignette(image.bits(), size_t(width), size_t(height), size_t(image.bytesPerLine()), frame.x(), frame.y(), frame.width(),
|
||||
frame.height(), job.canvas ? 1 : 0, settings.vignetteAmount,
|
||||
settings.vignetteMidpoint, settings.vignetteRoundness, settings.vignetteFeather, settings.vignetteHighlights,
|
||||
settings.vignetteColor.red, settings.vignetteColor.green, settings.vignetteColor.blue);
|
||||
break;
|
||||
}
|
||||
case FilterKind::bloomGlow: image = bloom(drawn(job.image), settings.bloomRadius * job.scale, settings.bloomAmount / 50); break;
|
||||
case FilterKind::tonalContrast: {
|
||||
// The detail's base: the layer softened, clear past its edge.
|
||||
@@ -306,7 +310,9 @@ FilterJob FilterEdit::previewJob() const
|
||||
rawPanel.showsShadowClipping,
|
||||
rawPanel.showsHighlightClipping,
|
||||
rawPanel.pointColorVisualizeIndex(settings.cameraRaw),
|
||||
rawPanel.sharpenMask};
|
||||
rawPanel.sharpenMask,
|
||||
{},
|
||||
canvas};
|
||||
}
|
||||
|
||||
FilterSettings FilterEdit::renderSettings() const
|
||||
|
||||
@@ -54,7 +54,7 @@ struct FilterSettings {
|
||||
double vignetteAmount = 35;
|
||||
AdjustmentColor vignetteColor = AdjustmentColor(0, 0, 0);
|
||||
double vignetteMidpoint = 50;
|
||||
double vignetteRoundness = 0;
|
||||
double vignetteRoundness = 100;
|
||||
double vignetteFeather = 60;
|
||||
double vignetteHighlights = 25;
|
||||
// Bloom / Glow: strength, and blur radius in layer pixels.
|
||||
@@ -106,6 +106,8 @@ struct FilterJob {
|
||||
bool showsSharpenMask = false;
|
||||
// A live adjustment's noise field, fixed in its region's pixels.
|
||||
QPointF noiseOrigin{};
|
||||
// Vignette on an empty layer: the canvas it fills.
|
||||
std::optional<QRectF> canvas = std::nullopt;
|
||||
};
|
||||
|
||||
// Swift's PixelFilter.
|
||||
@@ -172,6 +174,10 @@ public:
|
||||
quint64 previewSourceVersion = 0;
|
||||
std::optional<FilterJob> pending;
|
||||
CameraRawPanel rawPanel;
|
||||
// Vignette on an empty layer: the canvas it fills.
|
||||
std::optional<QRectF> canvas;
|
||||
// The layer had no pixels: the filter began clear.
|
||||
bool startedEmpty = false;
|
||||
|
||||
private:
|
||||
void grow(const QRectF &extent);
|
||||
|
||||
@@ -17,11 +17,23 @@ SessionJobs::Adjusted adjusted(const HueSaturationJob &job)
|
||||
|
||||
// Swift's session extension in HueSaturation.swift.
|
||||
bool EditorSession::canAdjustColors() const
|
||||
{
|
||||
return canAdjust(false);
|
||||
}
|
||||
|
||||
// Vignette also paints an empty layer, without pixels yet.
|
||||
bool EditorSession::canVignette() const
|
||||
{
|
||||
const std::optional<ImageLayer> layer = activeLayer();
|
||||
return canAdjust(!(layer && layer->adjustment));
|
||||
}
|
||||
|
||||
bool EditorSession::canAdjust(bool allowingEmpty) const
|
||||
{
|
||||
const std::optional<ImageLayer> layer = activeLayer();
|
||||
const std::optional<DocumentSelection> current = selection();
|
||||
return !m_levels && !m_filterEdit && m_document && layer && !m_isProjectBusy && !m_isImporting && !m_brushStroke && !m_pixelMove && !m_renamingLayerID && !m_showsNewDocument
|
||||
&& !m_showsImporter && m_selectedLayerIDs.size() == 1 && !layer->isGroup && !m_isMaskSelected && layer->asset
|
||||
&& !m_showsImporter && m_selectedLayerIDs.size() == 1 && !layer->isGroup && !m_isMaskSelected && (layer->asset || allowingEmpty)
|
||||
&& m_document->effectiveVisibleIDs().contains(layer->id) && !(current && current->isEmpty());
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Swift's HueSaturation extension.
|
||||
public:
|
||||
bool canAdjustColors() const;
|
||||
bool canVignette() const;
|
||||
void beginHueSaturation();
|
||||
// Previews coalesce: the newest waits for the running one.
|
||||
void updateHueSaturation(const HueSaturationSettings &settings, bool preview);
|
||||
@@ -21,6 +22,7 @@ public:
|
||||
void setHueTargeting(bool targeting);
|
||||
|
||||
private:
|
||||
bool canAdjust(bool allowingEmpty) const;
|
||||
void renderPendingHuePreview();
|
||||
void finishHueSaturationPreview();
|
||||
void finishHueSaturationCommit();
|
||||
Reference in new issue
Block a user