#include "Document/MagicWand.h" #include "Document/BrushStroke.h" #include "Document/EditorSession.h" #include "IO/ImageExporter.h" #include "Logging.h" #include "Rendering/LayerRenderer.h" #include #include #include #include extern "C" { #include "WandPixels.h" } QString title(WandSampleSize size) { switch (size) { case WandSampleSize::point: return QStringLiteral("Point Sample"); case WandSampleSize::threeByThree: return QStringLiteral("3 by 3 Average"); case WandSampleSize::fiveByFive: return QStringLiteral("5 by 5 Average"); } throw std::logic_error("unknown sample size"); } int radius(WandSampleSize size) { return int(size); } MagicWandError::MagicWandError(Kind kind) : std::runtime_error(kind == Kind::tooDetailed ? "That selection is too detailed to outline. Try a different Tolerance, or turn on Contiguous." : "There isn’t enough memory to make that selection."), kind(kind) { } std::optional MagicWand::select(const QImage &image, QPointF point, const WandSettings &settings) { const int width = image.width(), height = image.height(); if (!std::isfinite(point.x()) || !std::isfinite(point.y())) return std::nullopt; const double fx = std::floor(point.x()), fy = std::floor(point.y()); if (fx < 0 || fy < 0 || fx >= width || fy >= height) return std::nullopt; // Premultiplied RGBA, rows top down, as the kernel reads. QImage rgba = BrushRaster::context(width, height, false); QPainter painter(&rgba); BrushRaster::draw(image, QRectF(0, 0, width, height), painter); painter.end(); std::vector selected(size_t(width) * size_t(height)); const long count = wand_mask(rgba.constBits(), size_t(width), size_t(height), size_t(rgba.bytesPerLine()), size_t(fx), size_t(fy), size_t(radius(settings.sampleSize)), std::clamp(settings.tolerance, 0, 255), settings.contiguous ? 1 : 0, selected.data()); if (count < 0) throw MagicWandError(MagicWandError::Kind::memory); if (count == 0) return std::nullopt; return outline(selected, width, height); } std::optional MagicWand::outline(const std::vector &mask, int width, int height) { if (width <= 0 || height <= 0 || mask.size() != size_t(width) * size_t(height)) return std::nullopt; int32_t *points = nullptr, *loops = nullptr; size_t pointCount = 0, loopCount = 0; const int status = wand_trace(mask.data(), size_t(width), size_t(height), &points, &pointCount, &loops, &loopCount); const std::unique_ptr ownedPoints(points, &std::free), ownedLoops(loops, &std::free); if (status == -2) throw MagicWandError(MagicWandError::Kind::tooDetailed); if (status != 0) throw MagicWandError(MagicWandError::Kind::memory); if (loopCount == 0) return std::nullopt; // Outer loops clockwise, holes the other way: winding fills them. QPainterPath path; path.setFillRule(Qt::WindingFill); size_t index = 0; for (size_t loop = 0; loop < loopCount; ++loop) { const size_t length = size_t(loops[loop]); path.moveTo(points[index * 2], points[index * 2 + 1]); for (size_t corner = index + 1; corner < index + length; ++corner) path.lineTo(points[corner * 2], points[corner * 2 + 1]); path.closeSubpath(); index += length; } return path; } void EditorSession::setWandSettings(const WandSettings &settings) { m_wandSettings = settings; notify(); } // What the wand reads: composite, or the active layer's pixels. std::optional 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 (sampleAllLayers) { drawLiveComposite(document, painter); } else if (const std::optional layer = activeLayer(); layer && !layer->isGroup && layer->asset) { const LayerTransform transform = displayedTransform(*layer); LayerRenderer::draw(layer->asset->image(), transform, transform.center(), painter, {}); } painter.end(); return context; } catch (const ExportError &error) { qCWarning(lcApp) << "a selection tool cannot sample the canvas:" << error.what(); return std::nullopt; } } void EditorSession::magicWand(QPointF point, SelectionMode mode, std::function 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 &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; if (!canEditSelection() || m_isProjectBusy || m_selectionMoveOrigin || !inside) { finish(); return; } const std::optional sample = selectionSample(*document, m_wandSettings.sampleAllLayers); if (!sample) { finish(); return; } setIsProjectBusy(true); 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}; } catch (const MagicWandError &error) { return Wanded{std::nullopt, QString::fromUtf8(error.what())}; } catch (const ExportError &error) { return Wanded{std::nullopt, QString::fromUtf8(error.what())}; } })); } void EditorSession::finishWand() { const Wanded result = m_wand.result(); const Wanding pending = std::exchange(m_wanding, std::nullopt).value(); setIsProjectBusy(false); if (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) { // Nothing matched: New clears, as a lasso click enclosing nothing. if (pending.mode == SelectionMode::replace) deselect(); } else if (pending.mode == SelectionMode::replace) { // A traced outline lies on the canvas: no costly clip. setSelection(DocumentSelection{*result.path, m_selectionAntialiased}, pending.name); } else { applySelection(*result.path, pending.mode, pending.name); } } if (pending.done) QMetaObject::invokeMethod(this, pending.done, Qt::QueuedConnection); }