Magic tool Object mode (Compositor 1.1.8)
This commit is contained in:
1 parent
78bfa2445d
commit
3d56688ce4
39 files changed
+1193
-134
No files matched your search
@@ -20,6 +20,8 @@ ToolIconKind ContentView::iconKind(NavigationTool tool) const
|
||||
return ToolIconKind::polygonal;
|
||||
if (tool == NavigationTool::brush && m_session.brushMode() == BrushToolMode::erase)
|
||||
return ToolIconKind::eraser;
|
||||
if (tool == NavigationTool::wand && m_session.wandMode() == WandMode::object)
|
||||
return ToolIconKind::object;
|
||||
return ToolIconKind::plain;
|
||||
}
|
||||
|
||||
@@ -29,6 +31,8 @@ QString ContentView::hint(NavigationTool tool, ToolIconKind kind, BlurToolMode s
|
||||
return QStringLiteral("Drag an ellipse · Shift add · Alt subtract · Shift again mid-drag circle · Drag inside to move · Delete clears · Ctrl+D deselect");
|
||||
if (tool == NavigationTool::lasso && kind == ToolIconKind::polygonal)
|
||||
return QStringLiteral("Click corners · Click start, double-click or Enter to close · Delete removes corner · Escape cancel");
|
||||
if (tool == NavigationTool::wand && kind == ToolIconKind::object)
|
||||
return QStringLiteral("Click an object to select its outline · Tab for Wand · Shift add · Alt subtract · Drag inside to move · Ctrl-drag moves pixels · Delete clears · Ctrl+D deselect");
|
||||
if (tool == NavigationTool::brush && kind == ToolIconKind::eraser)
|
||||
return QStringLiteral("Drag to erase · [ ] size · Shift-[ ] hardness · 1–0 opacity · Escape cancel · Space to pan");
|
||||
if (tool == NavigationTool::blur)
|
||||
@@ -42,7 +46,7 @@ QString ContentView::hint(NavigationTool tool, ToolIconKind kind, BlurToolMode s
|
||||
// Swift's last branch: the Eyedropper shows Zoom's words too.
|
||||
static const std::map<NavigationTool, const char *> hints = {
|
||||
{NavigationTool::marquee, "Drag a rectangle · Shift add · Alt subtract · Shift again mid-drag square · Drag inside to move · Ctrl-drag moves pixels · Delete clears · Ctrl+D deselect"},
|
||||
{NavigationTool::wand, "Click to select similar colors · Shift add · Alt subtract · Drag inside to move · Ctrl-drag moves pixels · Delete clears · Ctrl+D deselect"},
|
||||
{NavigationTool::wand, "Click to select similar colors · Tab for Object · Shift add · Alt subtract · Drag inside to move · Ctrl-drag moves pixels · Delete clears · Ctrl+D deselect"},
|
||||
{NavigationTool::lasso, "Drag to select · Drag inside to move · Shift add · Alt subtract · Delete clears · Alt+Backspace/Ctrl+Backspace fill · Ctrl+D deselect"},
|
||||
{NavigationTool::brush, "Drag to paint · [ ] size · Shift-[ ] hardness · 1–0 opacity · Escape cancel · Space to pan"},
|
||||
{NavigationTool::cloneStamp, "Alt-click to set the source · Drag to clone · [ ] size · Shift-[ ] hardness · 1–0 opacity · Space to pan"},
|
||||
|
||||
@@ -46,11 +46,12 @@ struct SessionJobs {
|
||||
std::optional<QImage> image;
|
||||
std::optional<QString> failure;
|
||||
};
|
||||
// What a wand click waits for and applies.
|
||||
// The Magic tool's job, either mode, and its step's name.
|
||||
struct Wanding {
|
||||
SelectionMode mode;
|
||||
QUuid documentID;
|
||||
std::function<void()> done;
|
||||
QString name;
|
||||
};
|
||||
struct Wanded {
|
||||
std::optional<QPainterPath> path;
|
||||
|
||||
@@ -96,7 +96,7 @@ QString label(NavigationTool tool)
|
||||
case NavigationTool::move: return QStringLiteral("Move / Transform (V)");
|
||||
case NavigationTool::marquee: return QStringLiteral("Marquee (M)");
|
||||
case NavigationTool::lasso: return QStringLiteral("Lasso (L)");
|
||||
case NavigationTool::wand: return QStringLiteral("Magic Wand (W)");
|
||||
case NavigationTool::wand: return QStringLiteral("Magic (W) · Tab switches Wand and Object");
|
||||
case NavigationTool::crop: return QStringLiteral("Crop (C)");
|
||||
case NavigationTool::brush: return QStringLiteral("Brush (B) · Eraser (E)");
|
||||
case NavigationTool::spotHealing: return QStringLiteral("Spot Healing Brush (J)");
|
||||
|
||||
@@ -165,6 +165,8 @@ void EditorSession::cycleToolMode()
|
||||
return;
|
||||
if (m_tool == NavigationTool::marquee)
|
||||
toggleMarqueeKind();
|
||||
else if (m_tool == NavigationTool::wand)
|
||||
setWandMode(allWandModes[(size_t(m_wandMode) + 1) % allWandModes.size()]);
|
||||
else if (m_tool == NavigationTool::lasso)
|
||||
toggleLassoKind();
|
||||
else if (m_tool == NavigationTool::shape)
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "Document/LevelsAutomatic.h"
|
||||
#include "Document/FloatingSelection.h"
|
||||
#include "Document/MagicWand.h"
|
||||
#include "Document/ObjectSelection.h"
|
||||
#include "Document/SelectionClipboard.h"
|
||||
#include "Document/SelectionEdits.h"
|
||||
#include "Document/SmudgeLiquify.h"
|
||||
@@ -298,6 +299,7 @@ public:
|
||||
#include "Document/AdjustmentEditing+Session.h"
|
||||
#include "Document/LayerEffects+Session.h"
|
||||
#include "Document/Guides+Session.h"
|
||||
#include "Document/ObjectSelection+Session.h"
|
||||
signals:
|
||||
void changed();
|
||||
|
||||
@@ -376,6 +378,8 @@ private:
|
||||
std::optional<Inverting> m_inverting;
|
||||
QFutureWatcher<Inverted> m_inverter;
|
||||
WandSettings m_wandSettings;
|
||||
WandMode m_wandMode = WandMode::wand;
|
||||
ObjectSelectionSettings m_objectSelectionSettings;
|
||||
std::optional<Wanding> m_wanding;
|
||||
QFutureWatcher<Wanded> m_wand;
|
||||
std::optional<Subjecting> m_subjecting;
|
||||
|
||||
@@ -3,6 +3,8 @@ public:
|
||||
// Off the UI thread; `done` follows the step or refusal.
|
||||
void magicWand(QPointF point, SelectionMode mode, std::function<void()> done = {});
|
||||
|
||||
// What selection-from-image tools read, at document size.
|
||||
std::optional<QImage> selectionSample(const CanvasDocument &document, bool sampleAllLayers) const;
|
||||
|
||||
private:
|
||||
void finishWand();
|
||||
std::optional<QImage> wandSample(const CanvasDocument &document) const;
|
||||
@@ -97,13 +97,13 @@ void EditorSession::setWandSettings(const WandSettings &settings)
|
||||
}
|
||||
|
||||
// What the wand reads: composite, or the active layer's pixels.
|
||||
std::optional<QImage> EditorSession::wandSample(const CanvasDocument &document) const
|
||||
std::optional<QImage> EditorSession::selectionSample(const CanvasDocument &document, bool sampleAllLayers) const
|
||||
{
|
||||
try {
|
||||
QImage context = BrushRaster::context(document.width, document.height, false);
|
||||
QPainter painter(&context);
|
||||
// A painted asset flattens here; the catch covers it too.
|
||||
if (m_wandSettings.sampleAllLayers) {
|
||||
if (sampleAllLayers) {
|
||||
drawLiveComposite(document, painter);
|
||||
} else if (const std::optional<ImageLayer> layer = activeLayer(); layer && !layer->isGroup && layer->asset) {
|
||||
const LayerTransform transform = displayedTransform(*layer);
|
||||
@@ -112,7 +112,7 @@ std::optional<QImage> EditorSession::wandSample(const CanvasDocument &document)
|
||||
painter.end();
|
||||
return context;
|
||||
} catch (const ExportError &error) {
|
||||
qCWarning(lcApp) << "the wand cannot sample the canvas:" << error.what();
|
||||
qCWarning(lcApp) << "a selection tool cannot sample the canvas:" << error.what();
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
@@ -131,13 +131,13 @@ void EditorSession::magicWand(QPointF point, SelectionMode mode, std::function<v
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
const std::optional<QImage> sample = wandSample(*document);
|
||||
const std::optional<QImage> sample = selectionSample(*document, m_wandSettings.sampleAllLayers);
|
||||
if (!sample) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
setIsProjectBusy(true);
|
||||
m_wanding = Wanding{mode, document->id, std::move(done)};
|
||||
m_wanding = Wanding{mode, document->id, std::move(done), QStringLiteral("Magic Wand")};
|
||||
m_wand.setFuture(QtConcurrent::run([sample = *sample, point, settings = m_wandSettings]() -> Wanded {
|
||||
try {
|
||||
return Wanded{MagicWand::select(sample, point, settings), std::nullopt};
|
||||
@@ -155,7 +155,7 @@ void EditorSession::finishWand()
|
||||
const Wanding pending = std::exchange(m_wanding, std::nullopt).value();
|
||||
setIsProjectBusy(false);
|
||||
if (result.failure) {
|
||||
qCWarning(lcApp).noquote() << "the wand could not select:" << *result.failure;
|
||||
qCWarning(lcApp).noquote() << pending.name << "could not select:" << *result.failure;
|
||||
setBrushError(result.failure);
|
||||
} else if (m_document && m_document->id == pending.documentID) {
|
||||
if (!result.path) {
|
||||
@@ -164,9 +164,9 @@ void EditorSession::finishWand()
|
||||
deselect();
|
||||
} else if (pending.mode == SelectionMode::replace) {
|
||||
// A traced outline lies on the canvas: no costly clip.
|
||||
setSelection(DocumentSelection{*result.path, m_selectionAntialiased}, QStringLiteral("Magic Wand"));
|
||||
setSelection(DocumentSelection{*result.path, m_selectionAntialiased}, pending.name);
|
||||
} else {
|
||||
applySelection(*result.path, pending.mode, QStringLiteral("Magic Wand"));
|
||||
applySelection(*result.path, pending.mode, pending.name);
|
||||
}
|
||||
}
|
||||
if (pending.done)
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// Swift's ObjectSelection extension, and the Magic tool's mode.
|
||||
public:
|
||||
WandMode wandMode() const { return m_wandMode; }
|
||||
void setWandMode(WandMode mode);
|
||||
const ObjectSelectionSettings &objectSelectionSettings() const { return m_objectSelectionSettings; }
|
||||
void setObjectSelectionSettings(const ObjectSelectionSettings &settings);
|
||||
// Off the UI thread; `done` follows the step or refusal.
|
||||
void selectObject(QPointF point, SelectionMode mode, std::function<void()> done = {});
|
||||
@@ -0,0 +1,272 @@
|
||||
#include "Document/ObjectSelection.h"
|
||||
#include "Document/GuidedMatte.h"
|
||||
#include "Document/EditorSession.h"
|
||||
#include "Document/MagicWand.h"
|
||||
#include "Document/SubjectRemoval.h"
|
||||
#include "IO/ImageExporter.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <deque>
|
||||
#include <QtConcurrent>
|
||||
#include <limits>
|
||||
|
||||
namespace {
|
||||
double perpendicularDistance(QPointF point, QPointF a, QPointF b)
|
||||
{
|
||||
const double dx = b.x() - a.x(), dy = b.y() - a.y();
|
||||
const double length = std::hypot(dx, dy);
|
||||
if (!(length > 0))
|
||||
return std::hypot(point.x() - a.x(), point.y() - a.y());
|
||||
return std::abs(dy * point.x() - dx * point.y() + b.x() * a.y() - b.y() * a.x()) / length;
|
||||
}
|
||||
|
||||
// Swift's Douglas–Peucker on an open polyline.
|
||||
std::vector<QPointF> simplifyOpen(const std::vector<QPointF> &points, size_t first, size_t last, double tolerance)
|
||||
{
|
||||
if (last <= first + 1)
|
||||
return {points[first], points[last]};
|
||||
size_t farthest = first + 1;
|
||||
double greatest = 0;
|
||||
for (size_t index = first + 1; index < last; ++index) {
|
||||
const double distance = perpendicularDistance(points[index], points[first], points[last]);
|
||||
if (distance > greatest) {
|
||||
greatest = distance;
|
||||
farthest = index;
|
||||
}
|
||||
}
|
||||
if (!(greatest > tolerance))
|
||||
return {points[first], points[last]};
|
||||
std::vector<QPointF> left = simplifyOpen(points, first, farthest, tolerance);
|
||||
const std::vector<QPointF> right = simplifyOpen(points, farthest, last, tolerance);
|
||||
left.pop_back();
|
||||
left.insert(left.end(), right.begin(), right.end());
|
||||
return left;
|
||||
}
|
||||
|
||||
std::vector<QPointF> simplifyClosed(const std::vector<QPointF> &points, double tolerance)
|
||||
{
|
||||
if (points.size() < 4)
|
||||
return points;
|
||||
// Break at a stable extreme so the whole contour survives.
|
||||
const size_t start = size_t(std::min_element(points.begin(), points.end(), [](QPointF lhs, QPointF rhs) {
|
||||
return lhs.x() == rhs.x() ? lhs.y() < rhs.y() : lhs.x() < rhs.x();
|
||||
}) - points.begin());
|
||||
std::vector<QPointF> open(points.begin() + std::ptrdiff_t(start), points.end());
|
||||
open.insert(open.end(), points.begin(), points.begin() + std::ptrdiff_t(start));
|
||||
open.push_back(open.front());
|
||||
open = simplifyOpen(open, 0, open.size() - 1, tolerance);
|
||||
// Its ends are the same point.
|
||||
open.pop_back();
|
||||
return open.size() >= 3 ? open : points;
|
||||
}
|
||||
|
||||
std::vector<QPointF> chaikin(std::vector<QPointF> points, int iterations)
|
||||
{
|
||||
for (int round = 0; round < iterations; ++round) {
|
||||
std::vector<QPointF> next;
|
||||
next.reserve(points.size() * 2);
|
||||
for (size_t index = 0; index < points.size(); ++index) {
|
||||
const QPointF a = points[index], b = points[(index + 1) % points.size()];
|
||||
next.push_back(a * 0.75 + b * 0.25);
|
||||
next.push_back(a * 0.25 + b * 0.75);
|
||||
}
|
||||
points = std::move(next);
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
bool any(const std::vector<uchar> &mask, int width, int height, int x, int y, bool set)
|
||||
{
|
||||
for (int ny = std::max(0, y - 1); ny <= std::min(height - 1, y + 1); ++ny) {
|
||||
for (int nx = std::max(0, x - 1); nx <= std::min(width - 1, x + 1); ++nx) {
|
||||
if ((mask[size_t(ny) * size_t(width) + size_t(nx)] != 0) == set)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::optional<QPainterPath> traced(const QImage &image, QPointF point, int edgeOffset, bool smoothEdges)
|
||||
{
|
||||
const int width = image.width(), height = image.height();
|
||||
const int x = int(std::floor(point.x())), y = int(std::floor(point.y()));
|
||||
if (!std::isfinite(point.x()) || !std::isfinite(point.y()) || x < 0 || x >= width || y < 0 || y >= height)
|
||||
return std::nullopt;
|
||||
QImage foreground;
|
||||
try {
|
||||
foreground = SubjectRemoval::foreground(image);
|
||||
} catch (const SubjectRemovalError &error) {
|
||||
// No subject anywhere: Vision finds no instance.
|
||||
if (error.kind == SubjectRemovalError::Kind::noSubject)
|
||||
return std::nullopt;
|
||||
throw;
|
||||
}
|
||||
using ObjectSelection::grid;
|
||||
// Regions on the model's grid stand for Vision's instances.
|
||||
const QImage cells = foreground.scaled(grid, grid, Qt::IgnoreAspectRatio, Qt::SmoothTransformation).convertToFormat(QImage::Format_Grayscale8);
|
||||
if (cells.isNull())
|
||||
throw ExportError(ExportError::Kind::render);
|
||||
std::vector<uchar> binary(size_t(grid) * grid);
|
||||
for (int row = 0; row < grid; ++row) {
|
||||
for (int column = 0; column < grid; ++column)
|
||||
binary[size_t(row) * grid + size_t(column)] = cells.constScanLine(row)[column] >= 128;
|
||||
}
|
||||
const int gx = std::clamp(int(point.x() / width * grid), 0, grid - 1), gy = std::clamp(int(point.y() / height * grid), 0, grid - 1);
|
||||
const std::optional<std::vector<uchar>> coarse = ObjectSelection::region(binary, grid, grid, gx, gy);
|
||||
if (!coarse)
|
||||
return std::nullopt;
|
||||
QImage small(grid, grid, QImage::Format_Grayscale8);
|
||||
if (small.isNull())
|
||||
throw ExportError(ExportError::Kind::render);
|
||||
for (int row = 0; row < grid; ++row) {
|
||||
for (int column = 0; column < grid; ++column)
|
||||
small.scanLine(row)[column] = (*coarse)[size_t(row) * grid + size_t(column)] ? 255 : 0;
|
||||
}
|
||||
// Swift's edge-preserving upsampling: the image guides the edge.
|
||||
const QImage up = small.scaled(width, height, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
||||
const QImage refined = GuidedMatte::refine(up, image, 5, std::numeric_limits<double>::max());
|
||||
std::vector<uchar> mask(size_t(width) * size_t(height));
|
||||
for (int row = 0; row < height; ++row) {
|
||||
const uchar *line = refined.constScanLine(row);
|
||||
for (int column = 0; column < width; ++column)
|
||||
mask[size_t(row) * size_t(width) + size_t(column)] = line[column] >= 128 ? 255 : 0;
|
||||
}
|
||||
mask = ObjectSelection::adjusted(std::move(mask), width, height, edgeOffset);
|
||||
const std::optional<QPainterPath> outline = MagicWand::outline(mask, width, height);
|
||||
if (!outline)
|
||||
return std::nullopt;
|
||||
return smoothEdges ? ObjectSelection::smoothed(*outline) : *outline;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<QPainterPath> ObjectSelection::select(const QImage &image, QPointF point, int edgeOffset, bool smoothEdges)
|
||||
{
|
||||
try {
|
||||
return traced(image, point, edgeOffset, smoothEdges);
|
||||
} catch (const std::bad_alloc &) {
|
||||
// Masks the size of the image found no memory.
|
||||
throw ExportError(ExportError::Kind::render);
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<std::vector<uchar>> ObjectSelection::region(const std::vector<uchar> &mask, int width, int height, int x, int y)
|
||||
{
|
||||
const auto at = [width](int column, int row) { return size_t(row) * size_t(width) + size_t(column); };
|
||||
if (!mask[at(x, y)])
|
||||
return std::nullopt;
|
||||
std::vector<uchar> result(mask.size());
|
||||
std::deque<std::pair<int, int>> pending{{x, y}};
|
||||
// Eight neighbours join a region, the cell itself included.
|
||||
while (!pending.empty()) {
|
||||
const auto [column, row] = pending.front();
|
||||
pending.pop_front();
|
||||
for (int ny = std::max(0, row - 1); ny <= std::min(height - 1, row + 1); ++ny) {
|
||||
for (int nx = std::max(0, column - 1); nx <= std::min(width - 1, column + 1); ++nx) {
|
||||
if (mask[at(nx, ny)] && !result[at(nx, ny)]) {
|
||||
result[at(nx, ny)] = 1;
|
||||
pending.emplace_back(nx, ny);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<uchar> ObjectSelection::adjusted(std::vector<uchar> mask, int width, int height, int edgeOffset)
|
||||
{
|
||||
const int steps = std::min(10, std::abs(edgeOffset));
|
||||
for (int step = 0; step < steps; ++step) {
|
||||
std::vector<uchar> result = mask;
|
||||
for (int y = 0; y < height; ++y) {
|
||||
for (int x = 0; x < width; ++x) {
|
||||
const size_t index = size_t(y) * size_t(width) + size_t(x);
|
||||
if (edgeOffset > 0 && mask[index])
|
||||
result[index] = any(mask, width, height, x, y, false) ? 0 : 255;
|
||||
else if (edgeOffset < 0 && !mask[index] && any(mask, width, height, x, y, true))
|
||||
result[index] = 255;
|
||||
}
|
||||
}
|
||||
mask = std::move(result);
|
||||
}
|
||||
return mask;
|
||||
}
|
||||
|
||||
QPainterPath ObjectSelection::smoothed(const QPainterPath &path)
|
||||
{
|
||||
std::vector<std::vector<QPointF>> subpaths;
|
||||
std::vector<QPointF> current;
|
||||
const auto finish = [&] {
|
||||
// Qt's close repeats the start, where a CGPath's adds none.
|
||||
if (!current.empty() && current.front() == current.back())
|
||||
current.pop_back();
|
||||
if (current.size() >= 3)
|
||||
subpaths.push_back(current);
|
||||
};
|
||||
for (int index = 0; index < path.elementCount(); ++index) {
|
||||
const QPainterPath::Element element = path.elementAt(index);
|
||||
if (element.isMoveTo()) {
|
||||
finish();
|
||||
current = {element};
|
||||
} else if (element.isLineTo()) {
|
||||
current.push_back(element);
|
||||
} else if (element.type == QPainterPath::CurveToDataElement && (index + 1 == path.elementCount() || path.elementAt(index + 1).type != QPainterPath::CurveToDataElement)) {
|
||||
// A curve's end point, as Swift keeps it.
|
||||
current.push_back(element);
|
||||
}
|
||||
}
|
||||
finish();
|
||||
QPainterPath result;
|
||||
result.setFillRule(path.fillRule());
|
||||
for (const std::vector<QPointF> &subpath : subpaths) {
|
||||
const std::vector<QPointF> points = chaikin(simplifyClosed(subpath, 1.6), 3);
|
||||
result.moveTo(points.front());
|
||||
for (size_t index = 1; index < points.size(); ++index)
|
||||
result.lineTo(points[index]);
|
||||
result.closeSubpath();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void EditorSession::setWandMode(WandMode mode)
|
||||
{
|
||||
m_wandMode = mode;
|
||||
notify();
|
||||
}
|
||||
|
||||
void EditorSession::setObjectSelectionSettings(const ObjectSelectionSettings &settings)
|
||||
{
|
||||
m_objectSelectionSettings = settings;
|
||||
notify();
|
||||
}
|
||||
|
||||
void EditorSession::selectObject(QPointF point, SelectionMode mode, std::function<void()> done)
|
||||
{
|
||||
// The caller resumes from the event loop, as after await.
|
||||
const auto finish = [this, done] {
|
||||
if (done)
|
||||
QMetaObject::invokeMethod(this, done, Qt::QueuedConnection);
|
||||
};
|
||||
const std::optional<CanvasDocument> &document = m_document;
|
||||
// Swift's positive bounds refuse a point that is no number.
|
||||
const bool inside = document && point.x() >= 0 && point.y() >= 0 && point.x() < document->width && point.y() < document->height;
|
||||
const std::optional<QImage> sample = canEditSelection() && !m_selectionMoveOrigin && inside
|
||||
? selectionSample(*document, m_objectSelectionSettings.sampleAllLayers)
|
||||
: std::nullopt;
|
||||
if (!sample) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
setIsProjectBusy(true);
|
||||
m_wanding = Wanding{mode, document->id, std::move(done), QStringLiteral("Object Selection")};
|
||||
m_wand.setFuture(QtConcurrent::run([sample = *sample, point, offset = m_objectSelectionSettings.edgeOffset, smooth = m_selectionAntialiased]() -> Wanded {
|
||||
try {
|
||||
return Wanded{ObjectSelection::select(sample, point, offset, smooth), std::nullopt};
|
||||
} catch (const SubjectRemovalError &error) {
|
||||
return Wanded{std::nullopt, QString::fromUtf8(error.what())};
|
||||
} catch (const MagicWandError &error) {
|
||||
return Wanded{std::nullopt, QString::fromUtf8(error.what())};
|
||||
} catch (const ExportError &error) {
|
||||
return Wanded{std::nullopt, QString::fromUtf8(error.what())};
|
||||
}
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
#include <QImage>
|
||||
#include <QPainterPath>
|
||||
#include <optional>
|
||||
|
||||
struct ObjectSelectionSettings {
|
||||
// Read the visible composite rather than the active layer.
|
||||
bool sampleAllLayers = true;
|
||||
// Positive erodes the mask inward; negative grows it outward.
|
||||
int edgeOffset = 0;
|
||||
friend bool operator==(const ObjectSelectionSettings &, const ObjectSelectionSettings &) = default;
|
||||
};
|
||||
|
||||
// Swift's ObjectSelection; U²-Net regions stand for Vision's instances.
|
||||
namespace ObjectSelection {
|
||||
// The model's grid the regions are found on.
|
||||
inline constexpr int grid = 320;
|
||||
// The outline in pixels; none off the image or background.
|
||||
std::optional<QPainterPath> select(const QImage &image, QPointF point, int edgeOffset, bool smoothEdges);
|
||||
// The binary grid's region holding the cell, else none.
|
||||
std::optional<std::vector<uchar>> region(const std::vector<uchar> &mask, int width, int height, int x, int y);
|
||||
// Swift's steps, a pixel ring each: erode inward, dilate outward.
|
||||
std::vector<uchar> adjusted(std::vector<uchar> mask, int width, int height, int edgeOffset);
|
||||
// Stair steps rounded off; winding and subpath order kept.
|
||||
QPainterPath smoothed(const QPainterPath &path);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
#include "Document/EditorSession.h"
|
||||
#include <cmath>
|
||||
|
||||
// Select's Expand, Contract and Feather, and their amounts.
|
||||
void EditorSession::setSelectionExpandAmount(int amount)
|
||||
{
|
||||
m_selectionExpandAmount = amount;
|
||||
notify();
|
||||
}
|
||||
|
||||
void EditorSession::setSelectionContractAmount(int amount)
|
||||
{
|
||||
m_selectionContractAmount = amount;
|
||||
notify();
|
||||
}
|
||||
|
||||
void EditorSession::setSelectionFeatherAmount(int amount)
|
||||
{
|
||||
m_selectionFeatherAmount = amount;
|
||||
notify();
|
||||
}
|
||||
|
||||
void EditorSession::setSelectionAmountOperation(std::optional<SelectionAmountOperation> operation)
|
||||
{
|
||||
m_selectionAmountOperation = operation;
|
||||
resumeFileRequests();
|
||||
notify();
|
||||
}
|
||||
|
||||
bool EditorSession::canModifySelection() const
|
||||
{
|
||||
const std::optional<DocumentSelection> current = selection();
|
||||
return current && !current->isEmpty() && canEditSelection() && !m_lassoDraft;
|
||||
}
|
||||
|
||||
void EditorSession::expandSelection(int amount)
|
||||
{
|
||||
resizeSelection(amount, QStringLiteral("Expand Selection"));
|
||||
}
|
||||
|
||||
void EditorSession::contractSelection(int amount)
|
||||
{
|
||||
resizeSelection(-amount, QStringLiteral("Contract Selection"));
|
||||
}
|
||||
|
||||
void EditorSession::promptSelectionAmount(SelectionAmountOperation operation)
|
||||
{
|
||||
if (!canModifySelection())
|
||||
return;
|
||||
setSelectionAmountOperation(operation);
|
||||
}
|
||||
|
||||
void EditorSession::confirmSelectionAmount(int amount)
|
||||
{
|
||||
const std::optional<SelectionAmountOperation> operation = m_selectionAmountOperation;
|
||||
if (!operation || amount < 1 || amount > (*operation == SelectionAmountOperation::feather ? 250 : 500))
|
||||
return;
|
||||
setSelectionAmountOperation(std::nullopt);
|
||||
switch (*operation) {
|
||||
case SelectionAmountOperation::expand:
|
||||
setSelectionExpandAmount(amount);
|
||||
expandSelection(amount);
|
||||
break;
|
||||
case SelectionAmountOperation::contract:
|
||||
setSelectionContractAmount(amount);
|
||||
contractSelection(amount);
|
||||
break;
|
||||
case SelectionAmountOperation::feather:
|
||||
setSelectionFeatherAmount(amount);
|
||||
featherSelection(amount);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void EditorSession::featherSelection(int amount)
|
||||
{
|
||||
const std::optional<DocumentSelection> current = selection();
|
||||
if (!canModifySelection() || !current || amount <= 0)
|
||||
return;
|
||||
// Two soft edges spread less than their sum.
|
||||
const double softened = std::sqrt(current->feather * current->feather + double(amount) * amount);
|
||||
setSelection(DocumentSelection{current->path, current->antialiased, std::min(250.0, softened)}, QStringLiteral("Feather Selection"));
|
||||
}
|
||||
@@ -16,6 +16,7 @@ public:
|
||||
void cancelLasso();
|
||||
void pressMarqueeKey();
|
||||
void pressLassoKey();
|
||||
void pressWandKey();
|
||||
void toggleMarqueeKind();
|
||||
void toggleLassoKind();
|
||||
// Closes the outline and combines it with the selection.
|
||||
|
||||
+11
-80
@@ -54,6 +54,11 @@ QString rawValue(SelectionAmountOperation operation)
|
||||
throw std::logic_error("unknown selection amount operation");
|
||||
}
|
||||
|
||||
QString rawValue(WandMode mode)
|
||||
{
|
||||
return mode == WandMode::object ? QStringLiteral("Object") : QStringLiteral("Wand");
|
||||
}
|
||||
|
||||
QString rawValue(SelectionMode mode)
|
||||
{
|
||||
switch (mode) {
|
||||
@@ -173,31 +178,6 @@ void EditorSession::setSelectionAntialiased(bool antialiased)
|
||||
notify();
|
||||
}
|
||||
|
||||
void EditorSession::setSelectionExpandAmount(int amount)
|
||||
{
|
||||
m_selectionExpandAmount = amount;
|
||||
notify();
|
||||
}
|
||||
|
||||
void EditorSession::setSelectionContractAmount(int amount)
|
||||
{
|
||||
m_selectionContractAmount = amount;
|
||||
notify();
|
||||
}
|
||||
|
||||
void EditorSession::setSelectionFeatherAmount(int amount)
|
||||
{
|
||||
m_selectionFeatherAmount = amount;
|
||||
notify();
|
||||
}
|
||||
|
||||
void EditorSession::setSelectionAmountOperation(std::optional<SelectionAmountOperation> operation)
|
||||
{
|
||||
m_selectionAmountOperation = operation;
|
||||
resumeFileRequests();
|
||||
notify();
|
||||
}
|
||||
|
||||
void EditorSession::beginLasso(QPointF point, SelectionMode mode)
|
||||
{
|
||||
// The Magic Wand selects with a click, never an outline.
|
||||
@@ -266,6 +246,12 @@ void EditorSession::pressMarqueeKey()
|
||||
selectTool(NavigationTool::marquee);
|
||||
}
|
||||
|
||||
// W picks the Magic tool; Tab switches its two modes.
|
||||
void EditorSession::pressWandKey()
|
||||
{
|
||||
selectTool(NavigationTool::wand);
|
||||
}
|
||||
|
||||
void EditorSession::pressLassoKey()
|
||||
{
|
||||
selectTool(NavigationTool::lasso);
|
||||
@@ -400,61 +386,6 @@ void EditorSession::nudgeSelection(double dx, double dy)
|
||||
endSelectionMove();
|
||||
}
|
||||
|
||||
bool EditorSession::canModifySelection() const
|
||||
{
|
||||
const std::optional<DocumentSelection> current = selection();
|
||||
return current && !current->isEmpty() && canEditSelection() && !m_lassoDraft;
|
||||
}
|
||||
|
||||
void EditorSession::expandSelection(int amount)
|
||||
{
|
||||
resizeSelection(amount, QStringLiteral("Expand Selection"));
|
||||
}
|
||||
|
||||
void EditorSession::contractSelection(int amount)
|
||||
{
|
||||
resizeSelection(-amount, QStringLiteral("Contract Selection"));
|
||||
}
|
||||
|
||||
void EditorSession::promptSelectionAmount(SelectionAmountOperation operation)
|
||||
{
|
||||
if (!canModifySelection())
|
||||
return;
|
||||
setSelectionAmountOperation(operation);
|
||||
}
|
||||
|
||||
void EditorSession::confirmSelectionAmount(int amount)
|
||||
{
|
||||
const std::optional<SelectionAmountOperation> operation = m_selectionAmountOperation;
|
||||
if (!operation || amount < 1 || amount > (*operation == SelectionAmountOperation::feather ? 250 : 500))
|
||||
return;
|
||||
setSelectionAmountOperation(std::nullopt);
|
||||
switch (*operation) {
|
||||
case SelectionAmountOperation::expand:
|
||||
setSelectionExpandAmount(amount);
|
||||
expandSelection(amount);
|
||||
break;
|
||||
case SelectionAmountOperation::contract:
|
||||
setSelectionContractAmount(amount);
|
||||
contractSelection(amount);
|
||||
break;
|
||||
case SelectionAmountOperation::feather:
|
||||
setSelectionFeatherAmount(amount);
|
||||
featherSelection(amount);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void EditorSession::featherSelection(int amount)
|
||||
{
|
||||
const std::optional<DocumentSelection> current = selection();
|
||||
if (!canModifySelection() || !current || amount <= 0)
|
||||
return;
|
||||
// Two soft edges spread less than their sum.
|
||||
const double softened = std::sqrt(current->feather * current->feather + double(amount) * amount);
|
||||
setSelection(DocumentSelection{current->path, current->antialiased, std::min(250.0, softened)}, QStringLiteral("Feather Selection"));
|
||||
}
|
||||
|
||||
void EditorSession::resizeSelection(double delta, const QString &name)
|
||||
{
|
||||
const std::optional<DocumentSelection> current = selection();
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <QPointF>
|
||||
#include <QRectF>
|
||||
#include <QString>
|
||||
#include <array>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
@@ -31,6 +32,11 @@ struct DocumentSelection {
|
||||
friend bool operator==(const DocumentSelection &, const DocumentSelection &) = default;
|
||||
};
|
||||
|
||||
// The Magic tool's modes: similar colours, or the object clicked.
|
||||
enum class WandMode { wand, object };
|
||||
inline constexpr std::array allWandModes{WandMode::wand, WandMode::object};
|
||||
QString rawValue(WandMode mode);
|
||||
|
||||
// The Marquee's outlines are rectangle and ellipse.
|
||||
enum class LassoKind { freehand, polygonal, rectangle, ellipse };
|
||||
// The name the tool bar shows.
|
||||
|
||||
@@ -222,6 +222,11 @@ QImage SubjectRemoval::subjectMask(const QImage &image, const std::optional<QIma
|
||||
return combined;
|
||||
}
|
||||
|
||||
QImage SubjectRemoval::foreground(const QImage &image)
|
||||
{
|
||||
return vision(image);
|
||||
}
|
||||
|
||||
QImage SubjectRemoval::run(const QImage &image, const FilterSettings &settings)
|
||||
{
|
||||
// The preview refines a copy 1400 long at most: quick.
|
||||
|
||||
@@ -23,6 +23,8 @@ std::vector<float> modelInput(const QImage &image);
|
||||
QImage modelMask(const float *prediction, QSize size);
|
||||
// White over the subject in the layer's grid, times `existing`.
|
||||
QImage subjectMask(const QImage &image, const std::optional<QImage> &existing, const FilterSettings &settings);
|
||||
// The model's mask at the image's size, cached by image.
|
||||
QImage foreground(const QImage &image);
|
||||
// The preview: the layer with its background cleared.
|
||||
QImage run(const QImage &image, const FilterSettings &settings);
|
||||
}
|
||||
@@ -26,8 +26,6 @@ std::optional<NavigationTool> toolForKey(int key)
|
||||
return NavigationTool::shape;
|
||||
case Qt::Key_I:
|
||||
return NavigationTool::eyedropper;
|
||||
case Qt::Key_W:
|
||||
return NavigationTool::wand;
|
||||
case Qt::Key_A:
|
||||
return NavigationTool::idle;
|
||||
case Qt::Key_R:
|
||||
|
||||
@@ -23,9 +23,11 @@ QCursor CanvasView::lassoCursor(Qt::KeyboardModifiers modifiers, std::optional<Q
|
||||
if ((command || mode == SelectionMode::replace) && document && location
|
||||
&& m_session.canMoveSelection(m_session.viewport.documentPoint(*location, document->size())))
|
||||
return command ? pixelDragCursor(modifiers.testFlag(Qt::AltModifier), ratio) : moveSelectionCursor(ratio);
|
||||
if (m_session.tool() == NavigationTool::wand)
|
||||
const bool wand = m_session.tool() == NavigationTool::wand;
|
||||
if (wand && m_session.wandMode() == WandMode::wand)
|
||||
return wandCursor(mode, ratio);
|
||||
const SelectionIcon icon = m_session.tool() == NavigationTool::marquee
|
||||
const SelectionIcon icon = wand ? SelectionIcon::objectSelection
|
||||
: m_session.tool() == NavigationTool::marquee
|
||||
? (m_session.marqueeKind() == LassoKind::ellipse ? SelectionIcon::ellipseMarquee : SelectionIcon::rectangleMarquee)
|
||||
: (m_session.lassoKind() == LassoKind::polygonal ? SelectionIcon::polygonalLasso : SelectionIcon::freehandLasso);
|
||||
return selectionCursor(icon, mode, ratio);
|
||||
@@ -56,6 +58,11 @@ void CanvasView::lassoMouseDown(QPointF point, Qt::KeyboardModifiers modifiers)
|
||||
updateCursor();
|
||||
return;
|
||||
}
|
||||
// Object mode runs before the colour wand, as Swift's.
|
||||
if (m_session.tool() == NavigationTool::wand && m_session.wandMode() == WandMode::object) {
|
||||
m_session.selectObject(pixel, mode);
|
||||
return;
|
||||
}
|
||||
if (m_session.tool() == NavigationTool::wand) {
|
||||
m_session.magicWand(pixel, mode);
|
||||
return;
|
||||
@@ -130,7 +137,9 @@ void CanvasView::releaseSelectionTool()
|
||||
const bool moved = m_session.selectionMoveOrigin() != m_session.selection();
|
||||
m_session.endSelectionMove();
|
||||
// The wand's click inside selects afresh; another's deselects.
|
||||
if (!moved && m_session.tool() == NavigationTool::wand)
|
||||
if (!moved && m_session.tool() == NavigationTool::wand && m_session.wandMode() == WandMode::object)
|
||||
m_session.selectObject(start, SelectionMode::replace);
|
||||
else if (!moved && m_session.tool() == NavigationTool::wand)
|
||||
m_session.magicWand(start, SelectionMode::replace);
|
||||
else if (!moved)
|
||||
m_session.deselect();
|
||||
@@ -169,12 +178,14 @@ bool CanvasView::selectionKey(const QKeyEvent &key)
|
||||
key.key() == Qt::Key_Up ? -step : key.key() == Qt::Key_Down ? step : 0);
|
||||
return true;
|
||||
}
|
||||
// M and L choose their tools; a repeat does nothing.
|
||||
if (plain && (key.key() == Qt::Key_M || key.key() == Qt::Key_L)) {
|
||||
// M, W and L choose tools; a repeat does nothing.
|
||||
if (plain && (key.key() == Qt::Key_M || key.key() == Qt::Key_W || key.key() == Qt::Key_L)) {
|
||||
if (key.isAutoRepeat())
|
||||
return true;
|
||||
if (key.key() == Qt::Key_M)
|
||||
m_session.pressMarqueeKey();
|
||||
else if (key.key() == Qt::Key_W)
|
||||
m_session.pressWandKey();
|
||||
else
|
||||
m_session.pressLassoKey();
|
||||
updateCursor();
|
||||
|
||||
@@ -32,6 +32,19 @@ void SelectionIcons::paint(QPainter &painter, SelectionIcon icon)
|
||||
painter.drawPath(tail);
|
||||
return;
|
||||
}
|
||||
// Swift's ObjectSelectionToolIcon: four corners round a pointer.
|
||||
case SelectionIcon::objectSelection: {
|
||||
QPen pen = painter.pen();
|
||||
pen.setWidthF(1.6);
|
||||
painter.setPen(pen);
|
||||
for (const QPolygonF &corner : {QPolygonF{QPointF(2, 6), QPointF(2, 2), QPointF(6, 2)}, QPolygonF{QPointF(12, 2), QPointF(16, 2), QPointF(16, 6)},
|
||||
QPolygonF{QPointF(16, 12), QPointF(16, 16), QPointF(12, 16)}, QPolygonF{QPointF(6, 16), QPointF(2, 16), QPointF(2, 12)}})
|
||||
painter.drawPolyline(corner);
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.setBrush(pen.color());
|
||||
painter.drawPolygon(QPolygonF{QPointF(7, 5), QPointF(7, 14), QPointF(9.6, 11.7), QPointF(11.3, 15.3), QPointF(13.2, 14.4), QPointF(11.5, 10.9), QPointF(14.5, 10.9)});
|
||||
return;
|
||||
}
|
||||
// Swift's PolygonalLassoToolIcon: loop, knot and rope as segments.
|
||||
case SelectionIcon::polygonalLasso:
|
||||
painter.drawPolygon(QPolygonF{QPointF(1.2, 7.0), QPointF(4.0, 2.4), QPointF(11.8, 1.8), QPointF(16.8, 5.2), QPointF(15.6, 10.4), QPointF(7.0, 11.6)});
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#include <QPainter>
|
||||
|
||||
// Which selection tool an icon names, by its kind.
|
||||
enum class SelectionIcon { freehandLasso, polygonalLasso, rectangleMarquee, ellipseMarquee };
|
||||
enum class SelectionIcon { freehandLasso, polygonalLasso, rectangleMarquee, ellipseMarquee, objectSelection };
|
||||
|
||||
// Icons the rail and the cursors share, 18-unit grid.
|
||||
namespace SelectionIcons {
|
||||
|
||||
@@ -140,7 +140,7 @@ const std::vector<ShortcutDefinition> &ShortcutDefinition::all()
|
||||
const std::vector<std::pair<const char *, QString>> tools{
|
||||
{"Select tool", "a"}, {"Move / Transform tool", "v"}, {"Hand tool", "h"}, {"Zoom tool", "z"}, {"Brush tool", "b"},
|
||||
{"Eraser", "e"}, {"Spot Healing", "j"}, {"Clone Stamp", "s"}, {"Type tool", "t"}, {"Gradient tool", "g"},
|
||||
{"Shape tool", "u"}, {"Eyedropper tool", "i"}, {"Marquee / cycle shape", "m"}, {"Magic Wand", "w"},
|
||||
{"Shape tool", "u"}, {"Eyedropper tool", "i"}, {"Marquee / cycle shape", "m"}, {"Magic", "w"},
|
||||
{"Lasso / cycle mode", "l"}, {"Blur / Smudge / Liquify", "r"}, {"Crop tool", "c"}, {"Swap foreground/background", "x"},
|
||||
{"Reset colors", "d"}, {"Cycle tool mode", "\t"}, {"Temporary Hand tool (hold)", " "},
|
||||
{"Delete selection / layer / effect / lasso point", backspace}, {"Apply current canvas operation", "\r"},
|
||||
|
||||
+60
-18
@@ -17,15 +17,15 @@ SelectionAmountField::SelectionAmountField(EditorSession &session, int low, int
|
||||
setFont(ToolHeaderStyle::controlFont());
|
||||
setFixedWidth(40);
|
||||
setAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
||||
// Digits only; the bounds clamp, as Swift's binding does.
|
||||
setValidator(new QRegularExpressionValidator(QRegularExpression(QStringLiteral("[0-9]*")), this));
|
||||
// Typing applies at once; an overflow reads as the top.
|
||||
// Digits, signed below zero; the bounds clamp, as Swift's binding.
|
||||
setValidator(new QRegularExpressionValidator(QRegularExpression(low < 0 ? QStringLiteral("-?[0-9]*") : QStringLiteral("[0-9]*")), this));
|
||||
// Typing applies at once; an overflow reads as its end.
|
||||
connect(this, &QLineEdit::textEdited, this, [this](const QString &text) {
|
||||
if (text.isEmpty())
|
||||
if (text.isEmpty() || text == QLatin1String("-"))
|
||||
return;
|
||||
bool fits = false;
|
||||
const qlonglong typed = text.toLongLong(&fits);
|
||||
m_change(double(fits ? std::clamp<qlonglong>(typed, m_low, m_high) : m_high));
|
||||
m_change(double(fits ? std::clamp<qlonglong>(typed, m_low, m_high) : text.startsWith(QLatin1Char('-')) ? m_low : m_high));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -95,7 +95,23 @@ LassoControls::LassoControls(EditorSession &session, QWidget *parent)
|
||||
m_allLayers(choice(m_sources, QStringLiteral("wandAllLayers"), QStringLiteral("All Layers"),
|
||||
QStringLiteral("Read colors from the active layer only, or from every visible layer as shown"),
|
||||
[this] { changeWand([](WandSettings &wand) { wand.sampleAllLayers = true; }); })),
|
||||
m_contiguous(new QCheckBox(QStringLiteral("Contiguous"), this)),
|
||||
m_contiguous(new QCheckBox(QStringLiteral("Contiguous"), this)), m_wandModes(new QButtonGroup(this)),
|
||||
m_wandMode(choice(m_wandModes, QStringLiteral("magicWandMode"), rawValue(WandMode::wand), QStringLiteral("Press Tab to switch between Wand and Object"),
|
||||
[this] { m_session.setWandMode(WandMode::wand); })),
|
||||
m_objectMode(choice(m_wandModes, QStringLiteral("magicObjectMode"), rawValue(WandMode::object), QStringLiteral("Press Tab to switch between Wand and Object"),
|
||||
[this] { m_session.setWandMode(WandMode::object); })),
|
||||
m_objectSources(new QButtonGroup(this)),
|
||||
m_objectThisLayer(choice(m_objectSources, QStringLiteral("objectThisLayer"), QStringLiteral("This Layer"),
|
||||
QStringLiteral("Analyze the active layer only, or every visible layer as shown"),
|
||||
[this] { changeObject([](ObjectSelectionSettings &object) { object.sampleAllLayers = false; }); })),
|
||||
m_objectAllLayers(choice(m_objectSources, QStringLiteral("objectAllLayers"), QStringLiteral("All Layers"),
|
||||
QStringLiteral("Analyze the active layer only, or every visible layer as shown"),
|
||||
[this] { changeObject([](ObjectSelectionSettings &object) { object.sampleAllLayers = true; }); })),
|
||||
m_edgeLabel(new QLabel(QStringLiteral("Edge"), this)),
|
||||
m_edge(new SelectionAmountField(session, -10, 10, [this] { return double(m_session.objectSelectionSettings().edgeOffset); },
|
||||
[this](double edge) { changeObject([edge](ObjectSelectionSettings &object) { object.edgeOffset = int(std::lround(edge)); }); },
|
||||
this)),
|
||||
m_edgeUnit(new QLabel(QStringLiteral("px"), this)),
|
||||
m_antialias(new QCheckBox(QStringLiteral("Anti-alias"), this)), m_expand(new QPushButton(QStringLiteral("Expand"), this)),
|
||||
m_expandAmount(new SelectionAmountField(session, 1, 500, [this] { return double(m_session.selectionExpandAmount()); },
|
||||
[this](double amount) { m_session.setSelectionExpandAmount(int(std::lround(amount))); }, this)),
|
||||
@@ -119,12 +135,16 @@ LassoControls::LassoControls(EditorSession &session, QWidget *parent)
|
||||
connect(m_sampleSize, &QComboBox::activated, this, [this](int index) {
|
||||
changeWand([index](WandSettings &wand) { wand.sampleSize = WandSampleSize(index); });
|
||||
});
|
||||
m_edge->setObjectName(QStringLiteral("objectEdge"));
|
||||
for (QWidget *edge : std::initializer_list<QWidget *>{m_edgeLabel, m_edge, m_edgeUnit})
|
||||
edge->setToolTip(QStringLiteral("Positive values tighten the detected mask inward; negative values expand it outward"));
|
||||
m_edgeLabel->setObjectName(QStringLiteral("objectEdgeLabel"));
|
||||
m_edgeLabel->setFont(ToolHeaderStyle::controlFont());
|
||||
m_contiguous->setObjectName(QStringLiteral("wandContiguous"));
|
||||
m_contiguous->setToolTip(QStringLiteral("Select only similar pixels connected to the one you click; off selects them everywhere"));
|
||||
connect(m_contiguous, &QCheckBox::toggled, this, [this](bool on) { changeWand([on](WandSettings &wand) { wand.contiguous = on; }); });
|
||||
// Rectangles snap to whole pixels, so smoothing applies elsewhere.
|
||||
m_antialias->setObjectName(QStringLiteral("selectionAntialiased"));
|
||||
m_antialias->setToolTip(QStringLiteral("Smooth selection edges; turn off for hard pixel edges"));
|
||||
connect(m_antialias, &QCheckBox::toggled, this, [this](bool on) { m_session.setSelectionAntialiased(on); });
|
||||
auto *divider = new QFrame(this);
|
||||
divider->setFrameShape(QFrame::VLine);
|
||||
@@ -150,8 +170,9 @@ LassoControls::LassoControls(EditorSession &session, QWidget *parent)
|
||||
m_deselect->setObjectName(QStringLiteral("deselect"));
|
||||
connect(m_deselect, &QPushButton::clicked, this, [this] { m_session.deselect(); });
|
||||
// Swift's `unitSuffix`: a unit sits by each field.
|
||||
for (QWidget *widget : std::initializer_list<QWidget *>{m_rectangle, m_ellipse, m_freehand, m_polygonal, m_replace, m_add, m_subtract, m_toleranceLabel,
|
||||
m_tolerance, m_sampleSize, m_thisLayer, m_allLayers, m_contiguous, m_antialias,
|
||||
for (QWidget *widget : std::initializer_list<QWidget *>{m_rectangle, m_ellipse, m_wandMode, m_objectMode, m_freehand, m_polygonal, m_replace, m_add,
|
||||
m_subtract, m_toleranceLabel, m_tolerance, m_sampleSize, m_thisLayer, m_allLayers, m_contiguous,
|
||||
m_objectThisLayer, m_objectAllLayers, m_edgeLabel, m_edge, m_edgeUnit, m_antialias,
|
||||
divider, m_expand, m_expandAmount, new QLabel(QStringLiteral("px"), this), m_contract, m_contractAmount,
|
||||
new QLabel(QStringLiteral("px"), this), m_feather, m_featherAmount,
|
||||
new QLabel(QStringLiteral("px"), this)})
|
||||
@@ -170,6 +191,14 @@ void LassoControls::changeWand(const std::function<void(WandSettings &)> &change
|
||||
m_session.setWandSettings(wand);
|
||||
}
|
||||
|
||||
// One field of Object mode's settings changes; the rest stay.
|
||||
void LassoControls::changeObject(const std::function<void(ObjectSelectionSettings &)> &change)
|
||||
{
|
||||
ObjectSelectionSettings object = m_session.objectSelectionSettings();
|
||||
change(object);
|
||||
m_session.setObjectSelectionSettings(object);
|
||||
}
|
||||
|
||||
// A segment of Swift's segmented picker: one checkable button.
|
||||
QToolButton *LassoControls::choice(QButtonGroup *group, const QString &name, const QString &text, const QString &tip, const std::function<void()> &pick)
|
||||
{
|
||||
@@ -188,7 +217,18 @@ QToolButton *LassoControls::choice(QButtonGroup *group, const QString &name, con
|
||||
void LassoControls::synchronize()
|
||||
{
|
||||
const NavigationTool tool = m_session.tool();
|
||||
title->setText(tool == NavigationTool::marquee ? QStringLiteral("Marquee") : tool == NavigationTool::wand ? QStringLiteral("Magic Wand") : QStringLiteral("Lasso"));
|
||||
title->setText(tool == NavigationTool::marquee ? QStringLiteral("Marquee") : tool == NavigationTool::wand ? QStringLiteral("Magic") : QStringLiteral("Lasso"));
|
||||
const bool wand = tool == NavigationTool::wand;
|
||||
const bool object = wand && m_session.wandMode() == WandMode::object;
|
||||
for (QToolButton *button : {m_wandMode, m_objectMode})
|
||||
button->setVisible(wand);
|
||||
m_wandMode->setChecked(!object);
|
||||
m_objectMode->setChecked(object);
|
||||
for (QWidget *widget : std::initializer_list<QWidget *>{m_objectThisLayer, m_objectAllLayers, m_edgeLabel, m_edge, m_edgeUnit})
|
||||
widget->setVisible(object);
|
||||
m_objectThisLayer->setChecked(!m_session.objectSelectionSettings().sampleAllLayers);
|
||||
m_objectAllLayers->setChecked(m_session.objectSelectionSettings().sampleAllLayers);
|
||||
m_edge->sync(m_session.objectSelectionSettings().edgeOffset);
|
||||
for (QToolButton *button : {m_rectangle, m_ellipse})
|
||||
button->setVisible(tool == NavigationTool::marquee);
|
||||
for (QToolButton *button : {m_freehand, m_polygonal})
|
||||
@@ -202,16 +242,18 @@ void LassoControls::synchronize()
|
||||
m_replace->setChecked(mode == SelectionMode::replace);
|
||||
m_add->setChecked(mode == SelectionMode::add);
|
||||
m_subtract->setChecked(mode == SelectionMode::subtract);
|
||||
const WandSettings &wand = m_session.wandSettings();
|
||||
const WandSettings &colours = m_session.wandSettings();
|
||||
for (QWidget *widget : std::initializer_list<QWidget *>{m_toleranceLabel, m_tolerance, m_sampleSize, m_thisLayer, m_allLayers, m_contiguous})
|
||||
widget->setVisible(tool == NavigationTool::wand);
|
||||
m_tolerance->sync(wand.tolerance);
|
||||
m_sampleSize->setCurrentIndex(int(wand.sampleSize));
|
||||
m_thisLayer->setChecked(!wand.sampleAllLayers);
|
||||
m_allLayers->setChecked(wand.sampleAllLayers);
|
||||
if (m_contiguous->isChecked() != wand.contiguous)
|
||||
m_contiguous->setChecked(wand.contiguous);
|
||||
widget->setVisible(wand && !object);
|
||||
m_tolerance->sync(colours.tolerance);
|
||||
m_sampleSize->setCurrentIndex(int(colours.sampleSize));
|
||||
m_thisLayer->setChecked(!colours.sampleAllLayers);
|
||||
m_allLayers->setChecked(colours.sampleAllLayers);
|
||||
if (m_contiguous->isChecked() != colours.contiguous)
|
||||
m_contiguous->setChecked(colours.contiguous);
|
||||
m_antialias->setVisible(tool == NavigationTool::lasso || tool == NavigationTool::wand || (tool == NavigationTool::marquee && m_session.marqueeKind() == LassoKind::ellipse));
|
||||
m_antialias->setToolTip(object ? QStringLiteral("Smooth the detected object outline; turn off for the raw pixel mask")
|
||||
: QStringLiteral("Smooth selection edges; turn off for hard pixel edges"));
|
||||
if (m_antialias->isChecked() != m_session.selectionAntialiased())
|
||||
m_antialias->setChecked(m_session.selectionAntialiased());
|
||||
const bool modifies = m_session.canModifySelection();
|
||||
|
||||
@@ -45,6 +45,7 @@ public:
|
||||
private:
|
||||
QToolButton *choice(QButtonGroup *group, const QString &name, const QString &text, const QString &tip, const std::function<void()> &pick);
|
||||
void changeWand(const std::function<void(WandSettings &)> &change);
|
||||
void changeObject(const std::function<void(ObjectSelectionSettings &)> &change);
|
||||
void synchronize();
|
||||
|
||||
EditorSession &m_session;
|
||||
@@ -66,6 +67,16 @@ private:
|
||||
QToolButton *const m_thisLayer;
|
||||
QToolButton *const m_allLayers;
|
||||
QCheckBox *const m_contiguous;
|
||||
// The Magic tool's modes, and Object mode's own settings.
|
||||
QButtonGroup *const m_wandModes;
|
||||
QToolButton *const m_wandMode;
|
||||
QToolButton *const m_objectMode;
|
||||
QButtonGroup *const m_objectSources;
|
||||
QToolButton *const m_objectThisLayer;
|
||||
QToolButton *const m_objectAllLayers;
|
||||
QLabel *const m_edgeLabel;
|
||||
SelectionAmountField *const m_edge;
|
||||
QLabel *const m_edgeUnit;
|
||||
QCheckBox *const m_antialias;
|
||||
QPushButton *const m_expand;
|
||||
SelectionAmountField *const m_expandAmount;
|
||||
|
||||
@@ -187,7 +187,7 @@ void NativeLayerList::pressKey(QKeyEvent *event)
|
||||
static const std::map<int, NavigationTool> tools{
|
||||
{Qt::Key_A, NavigationTool::idle}, {Qt::Key_V, NavigationTool::move}, {Qt::Key_H, NavigationTool::hand},
|
||||
{Qt::Key_Z, NavigationTool::zoom}, {Qt::Key_G, NavigationTool::gradient}, {Qt::Key_L, NavigationTool::lasso}, {Qt::Key_M, NavigationTool::marquee},
|
||||
{Qt::Key_W, NavigationTool::wand}, {Qt::Key_J, NavigationTool::spotHealing}, {Qt::Key_S, NavigationTool::cloneStamp},
|
||||
{Qt::Key_J, NavigationTool::spotHealing}, {Qt::Key_S, NavigationTool::cloneStamp},
|
||||
{Qt::Key_U, NavigationTool::shape}, {Qt::Key_R, NavigationTool::blur}, {Qt::Key_I, NavigationTool::eyedropper},
|
||||
{Qt::Key_C, NavigationTool::crop}, {Qt::Key_T, NavigationTool::type}};
|
||||
if (plain && (key == Qt::Key_Backspace || key == Qt::Key_Delete)) {
|
||||
@@ -209,6 +209,10 @@ void NativeLayerList::pressKey(QKeyEvent *event)
|
||||
m_session.swapPaletteColors();
|
||||
else
|
||||
m_session.resetPaletteColors();
|
||||
} else if (plain && key == Qt::Key_W) {
|
||||
// A held W picks the Magic tool once, as Swift's.
|
||||
if (!event->isAutoRepeat())
|
||||
m_session.pressWandKey();
|
||||
} else if (plain && tools.contains(key)) {
|
||||
m_session.selectTool(tools.at(key));
|
||||
} else if (plain && event->text().size() == 1 && event->text().front().isDigit() && m_session.usesOpacityKeys()) {
|
||||
|
||||
@@ -211,7 +211,7 @@ void ToolIcons::paint(QPainter &painter, NavigationTool tool, QPointF origin, do
|
||||
case NavigationTool::move: move(painter); break;
|
||||
case NavigationTool::marquee: marquee(painter, kind); break;
|
||||
case NavigationTool::lasso: lasso(painter, kind); break;
|
||||
case NavigationTool::wand: wand(painter); break;
|
||||
case NavigationTool::wand: kind == ToolIconKind::object ? SelectionIcons::paint(painter, SelectionIcon::objectSelection) : wand(painter); break;
|
||||
case NavigationTool::crop: crop(painter); break;
|
||||
case NavigationTool::brush: kind == ToolIconKind::eraser ? eraser(painter) : brush(painter); break;
|
||||
case NavigationTool::spotHealing: spotHealing(painter); break;
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
#include <QRectF>
|
||||
|
||||
// The Marquee's and the Lasso's icons follow their kinds.
|
||||
enum class ToolIconKind { plain, ellipse, polygonal, eraser };
|
||||
enum class ToolIconKind { plain, ellipse, polygonal, eraser, object };
|
||||
|
||||
// The tool rail's icons, drawn: SF Symbols have no twin.
|
||||
namespace ToolIcons {
|
||||
|
||||
Reference in new issue
Block a user