Crop ratios and selection start, Vignette on empty layers, text colour previews (12.10c); mutation runs use 12 cores

This commit is contained in:
雷电芽衣 committed 2026-09-28 21:03:16 -04:00
1 parent 7a873eb9c7
commit ffba1753c6
27 files changed
+555 -55

No files matched your search

+3
View File
@@ -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);
+40 -12
View File
@@ -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)
+6
View File
@@ -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(); }
};
+4
View File
@@ -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;
}
+12 -2
View File
@@ -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();
}
+25 -9
View File
@@ -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;
}
+10 -4
View File
@@ -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
+7 -1
View File
@@ -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);
+13 -1
View File
@@ -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());
}
+2
View File
@@ -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();
+2 -1
View File
@@ -336,7 +336,8 @@ void CanvasView::drawLayers(const CanvasDocument &document, double scale, const
: gradient && gradient->raster->layer.id == id ? gradient->raster.get()
: move && move->raster->layer.id == id ? move->raster.get()
: nullptr;
if (!layer.asset && !stroke)
// An empty layer draws nothing, unless Vignette previews onto it.
if (!layer.asset && !stroke && !(m_session.filterEdit() && m_session.filterEdit()->previewImage(id)))
return;
// Smudge or Liquify under way: the reshaped copy, canvas-wide.
if (const WarpStroke *warp = m_session.warpStroke(); warp && warp->layer.id == id) {
+1 -1
View File
@@ -366,7 +366,7 @@ void CompositorMenus::synchronize()
action(QStringLiteral("levels"))->setEnabled(s.canAdjustColors() && !s.hueSaturation());
for (const FilterKind kind : allFilterKinds) {
if (kind != FilterKind::contentAwareFill)
action(filterName(kind))->setEnabled(s.canAdjustColors() && !s.hueSaturation());
action(filterName(kind))->setEnabled((kind == FilterKind::vignette ? s.canVignette() : s.canAdjustColors()) && !s.hueSaturation());
}
action(QStringLiteral("hueSaturation"))->setEnabled(s.canAdjustColors());
action(QStringLiteral("invert"))->setText(s.isMaskSelected() ? QStringLiteral("Invert Mask") : QStringLiteral("Invert"));
+2 -1
View File
@@ -19,7 +19,8 @@ CropControls::CropControls(EditorSession &session, QWidget *parent)
pickerRow->addWidget(label);
pickerRow->addWidget(m_ratio, 1);
m_ratio->setObjectName(QStringLiteral("cropRatio"));
for (const QString &choice : {QStringLiteral("Free"), QStringLiteral("Original"), QStringLiteral("1:1"), QStringLiteral("4:3"), QStringLiteral("16:9")})
for (const QString &choice : {QStringLiteral("Free"), QStringLiteral("Original"), QStringLiteral("1:1"), QStringLiteral("4:3"), QStringLiteral("3:4"), QStringLiteral("16:9"),
QStringLiteral("9:16")})
m_ratio->addItem(choice);
// Swift's onChange: the frame takes a ratio that changed.
connect(m_ratio, &QComboBox::activated, this, [this](int index) {
+6
View File
@@ -272,6 +272,12 @@ QToolButton *TypeControls::alignment(TextAlignment value)
void TypeControls::synchronize()
{
// Swift's onChange: open text previews the picker's colour.
const std::optional<PaletteColor> picked = m_session.colorPicker() ? std::optional(m_session.colorPicker()->color()) : std::nullopt;
if (picked != m_pickerColour) {
m_pickerColour = picked;
m_session.previewTextColor();
}
const LayerTextStyle style = m_session.currentTextStyle();
m_font->sync(style.fontName);
for (size_t index = 0; index < m_alignments.size(); ++index)
+2
View File
@@ -1,4 +1,5 @@
#pragma once
#include "Document/ColorPalette.h"
#include "Document/TypeTool.h"
#include "UI/ToolHeaderStyle.h"
#include <QComboBox>
@@ -74,4 +75,5 @@ private:
QPushButton *const m_cancel;
QPushButton *const m_done;
QPushButton *const m_edit;
std::optional<PaletteColor> m_pickerColour;
};