152 lines
5.9 KiB
C++
152 lines
5.9 KiB
C++
#include "Document/MaskTracing.h"
|
|
#include "Document/BrushStroke.h"
|
|
#include "Document/EditorSession.h"
|
|
#include "IO/ImageExporter.h"
|
|
#include "Logging.h"
|
|
#include <QPainter>
|
|
#include <functional>
|
|
#include <map>
|
|
#include <vector>
|
|
|
|
namespace {
|
|
// Outer loops run clockwise, holes counterclockwise, for winding fill.
|
|
std::optional<QPainterPath> trace(const QImage &image, bool alpha, const std::function<bool(uchar)> &test)
|
|
{
|
|
const int width = image.width(), height = image.height();
|
|
if (width <= 0 || height <= 0)
|
|
return std::nullopt;
|
|
const int channels = alpha ? 4 : 1;
|
|
// Drawn over a cleared bitmap, as the Swift tracer does.
|
|
QImage pixels(width, height, alpha ? QImage::Format_RGBA8888_Premultiplied : QImage::Format_Grayscale8);
|
|
if (pixels.isNull())
|
|
return std::nullopt;
|
|
pixels.fill(0);
|
|
QPainter(&pixels).drawImage(QRectF(0, 0, width, height), image);
|
|
const auto selected = [&](int x, int y) {
|
|
return x >= 0 && y >= 0 && x < width && y < height
|
|
&& test(pixels.constScanLine(y)[x * channels + channels - 1]);
|
|
};
|
|
// Directed unit edges, keyed by their start vertex.
|
|
const int stride = width + 1;
|
|
std::map<int, std::vector<int>> outgoing;
|
|
const auto edge = [&](int x0, int y0, int x1, int y1) { outgoing[y0 * stride + x0].push_back(y1 * stride + x1); };
|
|
for (int y = 0; y < height; ++y) {
|
|
for (int x = 0; x < width; ++x) {
|
|
if (!selected(x, y))
|
|
continue;
|
|
if (!selected(x, y - 1))
|
|
edge(x, y, x + 1, y);
|
|
if (!selected(x + 1, y))
|
|
edge(x + 1, y, x + 1, y + 1);
|
|
if (!selected(x, y + 1))
|
|
edge(x + 1, y + 1, x, y + 1);
|
|
if (!selected(x - 1, y))
|
|
edge(x, y + 1, x, y);
|
|
}
|
|
}
|
|
if (outgoing.empty())
|
|
return std::nullopt;
|
|
QPainterPath path;
|
|
path.setFillRule(Qt::WindingFill);
|
|
while (!outgoing.empty()) {
|
|
const int start = outgoing.begin()->first;
|
|
std::vector<int> loop;
|
|
int current = start;
|
|
do {
|
|
const auto found = outgoing.find(current);
|
|
if (found == outgoing.end())
|
|
break;
|
|
const int end = found->second.back();
|
|
found->second.pop_back();
|
|
if (found->second.empty())
|
|
outgoing.erase(found);
|
|
loop.push_back(current);
|
|
current = end;
|
|
} while (current != start);
|
|
// Keep corners; drop vertices that continue straight.
|
|
std::vector<QPointF> corners;
|
|
const int count = int(loop.size());
|
|
for (int index = 0; index < count; ++index) {
|
|
const int vertex = loop[index], previous = loop[(index + count - 1) % count], following = loop[(index + 1) % count];
|
|
const int inX = vertex % stride - previous % stride, inY = vertex / stride - previous / stride;
|
|
const int outX = following % stride - vertex % stride, outY = following / stride - vertex / stride;
|
|
if (inX != outX || inY != outY)
|
|
corners.push_back(QPointF(vertex % stride, vertex / stride));
|
|
}
|
|
if (corners.size() < 3)
|
|
continue;
|
|
path.moveTo(corners.front());
|
|
for (std::size_t index = 1; index < corners.size(); ++index)
|
|
path.lineTo(corners[index]);
|
|
path.closeSubpath();
|
|
}
|
|
return path.isEmpty() ? std::nullopt : std::optional(path);
|
|
}
|
|
}
|
|
|
|
std::optional<QPainterPath> MaskTracing::darkPixels(const QImage &image)
|
|
{
|
|
return trace(image, false, [](uchar value) { return value < 128; });
|
|
}
|
|
|
|
std::optional<QPainterPath> MaskTracing::whitePixels(const QImage &image)
|
|
{
|
|
return trace(image, false, [](uchar value) { return value >= 128; });
|
|
}
|
|
|
|
std::optional<QPainterPath> MaskTracing::opaquePixels(const QImage &image)
|
|
{
|
|
return trace(image, true, [](uchar value) { return value >= 128; });
|
|
}
|
|
|
|
void EditorSession::loadMaskSelection(QUuid layerID, SelectionMode mode)
|
|
{
|
|
if (!canEditSelection())
|
|
return;
|
|
const int index = indexOf(m_document->layers, layerID);
|
|
if (index < 0 || !m_document->layers[size_t(index)].mask)
|
|
return;
|
|
const ImageLayer &layer = m_document->layers[size_t(index)];
|
|
QImage mask;
|
|
// A painted mask flattens here; a failure loads nothing.
|
|
try {
|
|
mask = layer.mask->asset.image();
|
|
} catch (const ExportError &error) {
|
|
qCWarning(lcApp).noquote() << "cannot read the mask of" << layer.name << ":" << error.what();
|
|
return;
|
|
}
|
|
const std::optional<QPainterPath> traced = MaskTracing::darkPixels(mask);
|
|
// Swift beeps: nothing black to select.
|
|
if (!traced) {
|
|
qCWarning(lcApp).noquote() << "no black to select in the mask of" << layer.name;
|
|
return;
|
|
}
|
|
applySelection(BrushRaster::pixelToDocument(layer.maskTransform(), mask.width(), mask.height()).map(*traced), mode,
|
|
QStringLiteral("Load Mask Selection"));
|
|
}
|
|
|
|
void EditorSession::loadLayerSelection(QUuid layerID, SelectionMode mode)
|
|
{
|
|
const int index = canEditSelection() ? indexOf(m_document->layers, layerID) : -1;
|
|
// Swift beeps: no pixels, or none opaque.
|
|
if (index < 0 || m_document->layers[size_t(index)].isGroup || !m_document->layers[size_t(index)].asset) {
|
|
qCWarning(lcApp) << "no pixels to select for layer" << layerID.toString();
|
|
return;
|
|
}
|
|
const ImageLayer &layer = m_document->layers[size_t(index)];
|
|
QImage image;
|
|
try {
|
|
image = layer.asset->image();
|
|
} catch (const ExportError &error) {
|
|
qCWarning(lcApp).noquote() << "cannot read the pixels of" << layer.name << ":" << error.what();
|
|
return;
|
|
}
|
|
const std::optional<QPainterPath> traced = MaskTracing::opaquePixels(image);
|
|
if (!traced) {
|
|
qCWarning(lcApp).noquote() << "no opaque pixels to select in" << layer.name;
|
|
return;
|
|
}
|
|
applySelection(BrushRaster::pixelToDocument(layer.transform, image.width(), image.height()).map(*traced), mode,
|
|
QStringLiteral("Load Layer Selection"));
|
|
}
|