From 8fc7ef12dc90906c1962eff79acda6f734073351 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=BCller?= <d_muel20@uni-muenster.de> Date: Mon, 28 Oct 2019 15:18:30 +0100 Subject: [PATCH] * Image editor tools can now work on the entire image pixels --- Grinder/Version.h | 4 +- Grinder/controller/ImageEditorController.cpp | 78 +- Grinder/controller/ImageEditorController.h | 2 + Grinder/cv/Pixel.h | 468 +++++------ Grinder/cv/algorithms/FloodFill.cpp | 66 +- Grinder/cv/algorithms/FloodFill.h | 60 +- Grinder/cv/algorithms/SeedFill.cpp | 228 +++--- Grinder/cv/algorithms/SeedFill.h | 97 +-- .../wand/ColorDistanceWandAlgorithm.cpp | 12 +- .../wand/ColorDistanceWandAlgorithm.h | 2 +- .../cv/algorithms/wand/EdgesWandAlgorithm.cpp | 11 +- .../cv/algorithms/wand/EdgesWandAlgorithm.h | 76 +- Grinder/cv/algorithms/wand/WandAlgorithm.h | 68 +- .../wand/WatershedWandAlgorithm.cpp | 12 +- .../algorithms/wand/WatershedWandAlgorithm.h | 56 +- Grinder/image/ImageBuild.cpp | 4 +- Grinder/image/ImageBuild.h | 2 +- Grinder/image/Layer.cpp | 4 +- Grinder/image/Layer.h | 2 +- Grinder/image/LayerPixelsData.cpp | 28 +- Grinder/image/LayerPixelsData.h | 5 +- Grinder/res/Grinder.qrc | 1 + Grinder/res/Resources.h | 1 + Grinder/res/icons/sampling-image.png | Bin 0 -> 57530 bytes Grinder/ui/image/ImageEditor.cpp | 152 ++-- Grinder/ui/image/ImageEditor.h | 121 +-- Grinder/ui/image/ImageEditorEnvironment.cpp | 164 ++-- Grinder/ui/image/ImageEditorEnvironment.h | 158 ++-- Grinder/ui/image/ImageEditorSelection.cpp | 10 +- Grinder/ui/image/ImageEditorTool.cpp | 174 ++-- Grinder/ui/image/ImageEditorTool.h | 168 ++-- Grinder/ui/image/ImageEditorToolList.cpp | 1 + Grinder/ui/image/ImageEditorView.cpp | 752 ++++++++++-------- Grinder/ui/image/ImageEditorView.h | 197 ++--- Grinder/ui/image/tools/ColorPickerTool.cpp | 124 +-- Grinder/ui/image/tools/EraserTool.cpp | 144 ++-- Grinder/ui/image/tools/EraserTool.h | 72 +- Grinder/ui/image/tools/SelectionTool.cpp | 7 +- Grinder/ui/image/tools/WandSelectionTool.cpp | 7 +- 39 files changed, 1908 insertions(+), 1630 deletions(-) create mode 100644 Grinder/res/icons/sampling-image.png diff --git a/Grinder/Version.h b/Grinder/Version.h index bca1fcc..b68f9ad 100644 --- a/Grinder/Version.h +++ b/Grinder/Version.h @@ -10,14 +10,14 @@ #define GRNDR_INFO_TITLE "Grinder" #define GRNDR_INFO_COPYRIGHT "Copyright (c) WWU Muenster" -#define GRNDR_INFO_DATE "14.10.2019" +#define GRNDR_INFO_DATE "28.10.2019" #define GRNDR_INFO_COMPANY "WWU Muenster" #define GRNDR_INFO_WEBSITE "http://www.uni-muenster.de" #define GRNDR_VERSION_MAJOR 0 #define GRNDR_VERSION_MINOR 16 #define GRNDR_VERSION_REVISION 0 -#define GRNDR_VERSION_BUILD 404 +#define GRNDR_VERSION_BUILD 407 namespace grndr { diff --git a/Grinder/controller/ImageEditorController.cpp b/Grinder/controller/ImageEditorController.cpp index 1be8674..d837f73 100644 --- a/Grinder/controller/ImageEditorController.cpp +++ b/Grinder/controller/ImageEditorController.cpp @@ -9,6 +9,7 @@ #include "image/ImageTags.h" #include "image/ImageExceptions.h" #include "image/draftitems/PixelsDraftItem.h" +#include "cv/Pixel.h" #include "ui/image/ImageEditor.h" #include "ui/image/DraftItemNode.h" #include "ui/image/LayersListWidget.h" @@ -547,11 +548,43 @@ void ImageEditorController::convertSelectedPixelsToItem(Layer* layer, const Laye callControllerFunction("Converting selected pixels to an item", [this](Layer* layer, const LayerPixelsData::Selection& selection) { LongOperation opConvertPixels{"Converting selected pixels to an item"}; - auto selectedPixels = layer->layerPixels().data().getSelectedPixels(selection); + LayerPixelsData selectedPixels = layer->layerPixels().data().getSelectedPixels(selection); + + switch (_imageEditor->environment().getSamplingMode()) + { + case ImageEditorEnvironment::SamplingMode::Layer: + selectedPixels = layer->layerPixels().data().getSelectedPixels(selection); + break; + + case ImageEditorEnvironment::SamplingMode::Image: + selectedPixels = getSelectedImagePixels(selection); + break; + } if (!selectedPixels.empty()) { - clearSelectedPixels(layer, selection); + std::vector<Layer*> layers; + + switch (_imageEditor->environment().getSamplingMode()) + { + case ImageEditorEnvironment::SamplingMode::Layer: + layers.push_back(layer); + break; + + case ImageEditorEnvironment::SamplingMode::Image: + // Add all visible and editable layers + for (auto layer : _activeImageBuild->layers()) + { + if (layer->isVisible() && checkLayerEditability(layer.get(), false)) + layers.push_back(layer.get()); + } + + break; + } + + for (auto layer : layers) + clearSelectedPixels(layer, selection); + activateDefaultEditorTool(); if (auto draftItem = createDraftItem(DraftItemType::Pixels, layer)) @@ -643,9 +676,20 @@ void ImageEditorController::floodFillPixels(Layer* layer, QPoint seedPos, QColor if (layer && checkLayerEditability(layer)) { - callControllerFunction("Filling pixels", [](Layer* layer, QPoint seedPos, QColor color, float tolerance, bool perceivedDifference) { + callControllerFunction("Filling pixels", [this](Layer* layer, QPoint seedPos, QColor color, float tolerance, bool perceivedDifference) { layer->layerPixels().data().beginPainting(); - layer->layerPixels().data().floodFill(seedPos, color, tolerance, perceivedDifference); + + switch (_imageEditor->environment().getSamplingMode()) + { + case ImageEditorEnvironment::SamplingMode::Layer: + layer->layerPixels().data().floodFill(seedPos, color, tolerance, perceivedDifference); + break; + + case ImageEditorEnvironment::SamplingMode::Image: + layer->layerPixels().data().floodFill(seedPos, color, tolerance, perceivedDifference, _imageEditor->getDisplayedImage()); + break; + } + layer->layerPixels().data().endPainting(); return true; }, layer, seedPos, color, tolerance, perceivedDifference); @@ -879,6 +923,32 @@ void ImageEditorController::connectLayerSignals(Layer* layer, bool connectSignal disconnect(layer, nullptr, this, nullptr); } +LayerPixelsData ImageEditorController::getSelectedImagePixels(const LayerPixelsData::Selection& selection) const +{ + auto imageData = _imageEditor->getDisplayedImage(); + ConstPixelAccessor pixels{imageData}; + auto bounds = selection.bounds(); + auto checkBounds = [&imageData](int x, int y) { return x >= 0 && x < imageData.cols && y >= 0 && y < imageData.rows; }; + + if (checkBounds(bounds.left(), bounds.top()) && checkBounds(bounds.right(), bounds.bottom())) + { + LayerPixelsData selectedPixels{QSize{bounds.right() - bounds.left() + 1, bounds.bottom() - bounds.top() + 1}}; + + for (int y = bounds.top(); y <= bounds.bottom(); ++y) + { + for (int x = bounds.left(); x <= bounds.right(); ++x) + { + if (selection.isSet(x, y) && pixels.at(y, x).get().rgba() != 0) + selectedPixels.set(x - bounds.left(), y - bounds.top(), pixels.at(y, x)); + } + } + + return selectedPixels; + } + else + return {}; +} + void ImageEditorController::autoGenerateImageTag(ImageBuild* imageBuild, QColor color) const { if (!imageBuild) diff --git a/Grinder/controller/ImageEditorController.h b/Grinder/controller/ImageEditorController.h index 5ba2ac7..e21183b 100644 --- a/Grinder/controller/ImageEditorController.h +++ b/Grinder/controller/ImageEditorController.h @@ -119,6 +119,8 @@ namespace grndr void connectLayerSignals(Layer* layer, bool connectSignals = true) const; private: + LayerPixelsData getSelectedImagePixels(const LayerPixelsData::Selection& selection) const; + void autoGenerateImageTag(ImageBuild* imageBuild, QColor color) const; private slots: diff --git a/Grinder/cv/Pixel.h b/Grinder/cv/Pixel.h index 9abb995..a72ac3f 100644 --- a/Grinder/cv/Pixel.h +++ b/Grinder/cv/Pixel.h @@ -1,234 +1,234 @@ -/****************************************************************************** - * File: Pixel.h - * Date: 19.6.2018 - *****************************************************************************/ - -#ifndef PIXEL_H -#define PIXEL_H - -#include <QColor> -#include <QPoint> -#include <opencv2/core.hpp> - -namespace grndr -{ - class ColorPixel - { - public: - ColorPixel() { } - ColorPixel(uint8_t r, uint8_t g, uint8_t b) : _b{b}, _g{g}, _r{r} { } - ColorPixel(const QColor& color) { setColor(color); } - ColorPixel(const ColorPixel& other) = default; - ColorPixel(ColorPixel&& other) = default; - - ColorPixel& operator =(const QColor& color) { setColor(color); return *this; } - - public: - QColor getColor() const; - void setColor(const QColor& color); - - operator QColor() const { return getColor(); } - - public: - bool operator ==(const ColorPixel& other) const; - bool operator ==(const QColor& other) const { return ColorPixel{other} == *this; } - - private: - uint8_t _b{0}; - uint8_t _g{0}; - uint8_t _r{0}; - }; - - class ColorAlphaPixel - { - public: - ColorAlphaPixel() { } - ColorAlphaPixel(uint8_t r, uint8_t g, uint8_t b, uint8_t a) : _b{b}, _g{g}, _r{r}, _a{a} { } - ColorAlphaPixel(const QColor& color) { setColor(color); } - ColorAlphaPixel(const ColorAlphaPixel& other) = default; - ColorAlphaPixel(ColorAlphaPixel&& other) = default; - - ColorAlphaPixel& operator =(const QColor& color) { setColor(color); return *this; } - - public: - QColor getColor() const; - void setColor(const QColor& color); - - operator QColor() const { return getColor(); } - - public: - bool operator ==(const ColorAlphaPixel& other) const; - bool operator ==(const QColor& other) const { return ColorAlphaPixel{other} == *this; } - - private: - uint8_t _b{0}; - uint8_t _g{0}; - uint8_t _r{0}; - uint8_t _a{0}; - }; - - class GrayscalePixel - { - public: - GrayscalePixel() { } - GrayscalePixel(uint8_t p) : _p{p} { } - GrayscalePixel(const QColor& color) { setColor(color); } - GrayscalePixel(const GrayscalePixel& other) = default; - GrayscalePixel(GrayscalePixel&& other) = default; - - GrayscalePixel& operator =(uint8_t p) { _p = p; return *this; } - GrayscalePixel& operator =(const QColor& color) { setColor(color); return *this; } - GrayscalePixel& operator =(const GrayscalePixel& other) = default; - GrayscalePixel& operator =(GrayscalePixel&& other) = default; - - public: - QColor getColor() const; - void setColor(const QColor& color); - - operator uint8_t() const { return _p; } - operator QColor() const { return getColor(); } - - public: - bool operator ==(const GrayscalePixel& other) const { return _p == other._p; } - bool operator ==(const QColor& other) const { return GrayscalePixel{other} == *this; } - - private: - uint8_t _p{0}; - }; - - template<bool ConstAccess> - class PixelRef - { - public: - using color_pixel_type = std::conditional_t<ConstAccess, const ColorPixel, ColorPixel>; - using coloralpha_pixel_type = std::conditional_t<ConstAccess, const ColorAlphaPixel, ColorAlphaPixel>; - using grayscale_pixel_type = std::conditional_t<ConstAccess, const GrayscalePixel, GrayscalePixel>; - - public: - PixelRef(color_pixel_type* pixel) : _pixel{pixel} { } - PixelRef(coloralpha_pixel_type* pixel) : _alphaPixel{pixel} { } - PixelRef(grayscale_pixel_type* grayscalePixel) : _grayscalePixel{grayscalePixel} { } - - public: - template<bool hasWriteAccess = !ConstAccess> - std::enable_if_t<hasWriteAccess, PixelRef&> operator =(const QColor& color) { set(color); return *this; } - - public: - QColor get() const; - template<bool hasWriteAccess = !ConstAccess> - std::enable_if_t<hasWriteAccess, void> set(const QColor& color); - - operator QColor() const { return get(); } - - private: - color_pixel_type* _pixel{nullptr}; - coloralpha_pixel_type* _alphaPixel{nullptr}; - grayscale_pixel_type* _grayscalePixel{nullptr}; - }; - - template<bool ConstAccess> - class GenericPixelAccessor - { - public: - using matrix_type = std::conditional_t<ConstAccess, const cv::Mat, cv::Mat>; - using pixel_ref_type = PixelRef<ConstAccess>; - - public: - GenericPixelAccessor(matrix_type& matrix); - - public: - pixel_ref_type at(int r, int c); - pixel_ref_type at(QPoint pos) { return at(pos.y(), pos.x()); } - void forEach(std::function<void(pixel_ref_type, QPoint)> callback); - - auto operator [](QPoint pos) { return at(pos); } - - private: - void verifyMatrix(); - - private: - matrix_type& _matrix; - }; - - using PixelAccessor = GenericPixelAccessor<false>; - using ConstPixelAccessor = GenericPixelAccessor<true>; -} - -namespace cv -{ - template<> - class DataType<grndr::ColorPixel> - { - public: - using value_type = grndr::ColorPixel; - using work_type = typename cv::DataType<uint8_t>::work_type; - using channel_type = uint8_t; - - enum - { - generic_type = 0, - channels = 3, - fmt = cv::traits::SafeFmt<channel_type>::fmt + ((channels - 1) << 8) - }; - - using vec_type = Vec<channel_type, channels>; - }; - - template<> - class DataType<grndr::ColorAlphaPixel> - { - public: - using value_type = grndr::ColorAlphaPixel; - using work_type = typename cv::DataType<uint8_t>::work_type; - using channel_type = uint8_t; - - enum - { - generic_type = 0, - channels = 4, - fmt = cv::traits::SafeFmt<channel_type>::fmt + ((channels - 1) << 8) - }; - - using vec_type = Vec<channel_type, channels>; - }; - - template<> - class DataType<grndr::GrayscalePixel> - { - public: - using value_type = grndr::GrayscalePixel; - using work_type = typename cv::DataType<uint8_t>::work_type; - using channel_type = uint8_t; - - enum - { - generic_type = 0, - channels = 1, - fmt = cv::traits::SafeFmt<channel_type>::fmt + ((channels - 1) << 8) - }; - - using vec_type = Vec<channel_type, channels>; - }; - - namespace traits - { - template<> - struct Depth<grndr::ColorPixel> { enum { value = Depth<uint8_t>::value }; }; - template<> - struct Type<grndr::ColorPixel> { enum { value = CV_MAKETYPE(Depth<uint8_t>::value, 3) }; }; - - template<> - struct Depth<grndr::ColorAlphaPixel> { enum { value = Depth<uint8_t>::value }; }; - template<> - struct Type<grndr::ColorAlphaPixel> { enum { value = CV_MAKETYPE(Depth<uint8_t>::value, 4) }; }; - - template<> - struct Depth<grndr::GrayscalePixel> { enum { value = Depth<uint8_t>::value }; }; - template<> - struct Type<grndr::GrayscalePixel> { enum { value = CV_MAKETYPE(Depth<uint8_t>::value, 1) }; }; - } -} - -#include "Pixel.impl.h" - -#endif +/****************************************************************************** + * File: Pixel.h + * Date: 19.6.2018 + *****************************************************************************/ + +#ifndef PIXEL_H +#define PIXEL_H + +#include <QColor> +#include <QPoint> +#include <opencv2/core.hpp> + +namespace grndr +{ + class ColorPixel + { + public: + ColorPixel() { } + ColorPixel(uint8_t r, uint8_t g, uint8_t b) : _b{b}, _g{g}, _r{r} { } + ColorPixel(const QColor& color) { setColor(color); } + ColorPixel(const ColorPixel& other) = default; + ColorPixel(ColorPixel&& other) = default; + + ColorPixel& operator =(const QColor& color) { setColor(color); return *this; } + + public: + QColor getColor() const; + void setColor(const QColor& color); + + operator QColor() const { return getColor(); } + + public: + bool operator ==(const ColorPixel& other) const; + bool operator ==(const QColor& other) const { return ColorPixel{other} == *this; } + + private: + uint8_t _b{0}; + uint8_t _g{0}; + uint8_t _r{0}; + }; + + class ColorAlphaPixel + { + public: + ColorAlphaPixel() { } + ColorAlphaPixel(uint8_t r, uint8_t g, uint8_t b, uint8_t a) : _b{b}, _g{g}, _r{r}, _a{a} { } + ColorAlphaPixel(const QColor& color) { setColor(color); } + ColorAlphaPixel(const ColorAlphaPixel& other) = default; + ColorAlphaPixel(ColorAlphaPixel&& other) = default; + + ColorAlphaPixel& operator =(const QColor& color) { setColor(color); return *this; } + + public: + QColor getColor() const; + void setColor(const QColor& color); + + operator QColor() const { return getColor(); } + + public: + bool operator ==(const ColorAlphaPixel& other) const; + bool operator ==(const QColor& other) const { return ColorAlphaPixel{other} == *this; } + + private: + uint8_t _b{0}; + uint8_t _g{0}; + uint8_t _r{0}; + uint8_t _a{0}; + }; + + class GrayscalePixel + { + public: + GrayscalePixel() { } + GrayscalePixel(uint8_t p) : _p{p} { } + GrayscalePixel(const QColor& color) { setColor(color); } + GrayscalePixel(const GrayscalePixel& other) = default; + GrayscalePixel(GrayscalePixel&& other) = default; + + GrayscalePixel& operator =(uint8_t p) { _p = p; return *this; } + GrayscalePixel& operator =(const QColor& color) { setColor(color); return *this; } + GrayscalePixel& operator =(const GrayscalePixel& other) = default; + GrayscalePixel& operator =(GrayscalePixel&& other) = default; + + public: + QColor getColor() const; + void setColor(const QColor& color); + + operator uint8_t() const { return _p; } + operator QColor() const { return getColor(); } + + public: + bool operator ==(const GrayscalePixel& other) const { return _p == other._p; } + bool operator ==(const QColor& other) const { return GrayscalePixel{other} == *this; } + + private: + uint8_t _p{0}; + }; + + template<bool ConstAccess> + class PixelRef + { + public: + using color_pixel_type = std::conditional_t<ConstAccess, const ColorPixel, ColorPixel>; + using coloralpha_pixel_type = std::conditional_t<ConstAccess, const ColorAlphaPixel, ColorAlphaPixel>; + using grayscale_pixel_type = std::conditional_t<ConstAccess, const GrayscalePixel, GrayscalePixel>; + + public: + PixelRef(color_pixel_type* pixel) : _pixel{pixel} { } + PixelRef(coloralpha_pixel_type* pixel) : _alphaPixel{pixel} { } + PixelRef(grayscale_pixel_type* grayscalePixel) : _grayscalePixel{grayscalePixel} { } + + public: + template<bool hasWriteAccess = !ConstAccess> + std::enable_if_t<hasWriteAccess, PixelRef&> operator =(const QColor& color) { set(color); return *this; } + + public: + QColor get() const; + template<bool hasWriteAccess = !ConstAccess> + std::enable_if_t<hasWriteAccess, void> set(const QColor& color); + + operator QColor() const { return get(); } + + private: + color_pixel_type* _pixel{nullptr}; + coloralpha_pixel_type* _alphaPixel{nullptr}; + grayscale_pixel_type* _grayscalePixel{nullptr}; + }; + + template<bool ConstAccess> + class GenericPixelAccessor + { + public: + using matrix_type = std::conditional_t<ConstAccess, const cv::Mat, cv::Mat>; + using pixel_ref_type = PixelRef<ConstAccess>; + + public: + GenericPixelAccessor(matrix_type& matrix); + + public: + pixel_ref_type at(int r, int c); + pixel_ref_type at(QPoint pos) { return at(pos.y(), pos.x()); } + void forEach(std::function<void(pixel_ref_type, QPoint)> callback); + + auto operator [](QPoint pos) { return at(pos); } + + private: + void verifyMatrix(); + + private: + matrix_type& _matrix; + }; + + using PixelAccessor = GenericPixelAccessor<false>; + using ConstPixelAccessor = GenericPixelAccessor<true>; +} + +namespace cv +{ + template<> + class DataType<grndr::ColorPixel> + { + public: + using value_type = grndr::ColorPixel; + using work_type = typename cv::DataType<uint8_t>::work_type; + using channel_type = uint8_t; + + enum + { + generic_type = 0, + channels = 3, + fmt = cv::traits::SafeFmt<channel_type>::fmt + ((channels - 1) << 8) + }; + + using vec_type = Vec<channel_type, channels>; + }; + + template<> + class DataType<grndr::ColorAlphaPixel> + { + public: + using value_type = grndr::ColorAlphaPixel; + using work_type = typename cv::DataType<uint8_t>::work_type; + using channel_type = uint8_t; + + enum + { + generic_type = 0, + channels = 4, + fmt = cv::traits::SafeFmt<channel_type>::fmt + ((channels - 1) << 8) + }; + + using vec_type = Vec<channel_type, channels>; + }; + + template<> + class DataType<grndr::GrayscalePixel> + { + public: + using value_type = grndr::GrayscalePixel; + using work_type = typename cv::DataType<uint8_t>::work_type; + using channel_type = uint8_t; + + enum + { + generic_type = 0, + channels = 1, + fmt = cv::traits::SafeFmt<channel_type>::fmt + ((channels - 1) << 8) + }; + + using vec_type = Vec<channel_type, channels>; + }; + + namespace traits + { + template<> + struct Depth<grndr::ColorPixel> { enum { value = Depth<uint8_t>::value }; }; + template<> + struct Type<grndr::ColorPixel> { enum { value = CV_MAKETYPE(Depth<uint8_t>::value, 3) }; }; + + template<> + struct Depth<grndr::ColorAlphaPixel> { enum { value = Depth<uint8_t>::value }; }; + template<> + struct Type<grndr::ColorAlphaPixel> { enum { value = CV_MAKETYPE(Depth<uint8_t>::value, 4) }; }; + + template<> + struct Depth<grndr::GrayscalePixel> { enum { value = Depth<uint8_t>::value }; }; + template<> + struct Type<grndr::GrayscalePixel> { enum { value = CV_MAKETYPE(Depth<uint8_t>::value, 1) }; }; + } +} + +#include "Pixel.impl.h" + +#endif diff --git a/Grinder/cv/algorithms/FloodFill.cpp b/Grinder/cv/algorithms/FloodFill.cpp index 08c4d74..6d2b717 100644 --- a/Grinder/cv/algorithms/FloodFill.cpp +++ b/Grinder/cv/algorithms/FloodFill.cpp @@ -1,34 +1,32 @@ -/****************************************************************************** - * File: FloodFill.cpp - * Date: 13.7.2018 - *****************************************************************************/ - -#include "Grinder.h" -#include "FloodFill.h" -#include "cv/CVUtils.h" - -#include <stack> - -FloodFill::FloodFill(cv::Mat& matrix, QPoint pos, QColor fromColor, QColor toColor, float tolerance, bool perceivedDifference) : SeedFill(matrix, pos, fromColor, tolerance, perceivedDifference), - _toColor{toColor} -{ - -} - -void FloodFill::execute() -{ - if (_color == _toColor) - return; - - seedFill(_seedPos.x(), _seedPos.y()); - - // Colorize all filled pixels - for (int x = 0; x < _filledPixels.size().width(); ++x) - { - for (int y = 0; y < _filledPixels.size().height(); ++y) - { - if (_filledPixels.getFlag(x, y)) - _pixels.at(y, x) = _toColor.isValid() ? _toColor : QColor{0, 0, 0, 0}; - } - } -} +/****************************************************************************** + * File: FloodFill.cpp + * Date: 13.7.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "FloodFill.h" +#include "cv/CVUtils.h" + +FloodFill::FloodFill(const cv::Mat& sourceMatrix, cv::Mat& targetMatrix, QPoint pos, QColor fromColor, QColor toColor, float tolerance, bool perceivedDifference) : SeedFill(sourceMatrix, targetMatrix, pos, fromColor, tolerance, perceivedDifference), + _toColor{toColor} +{ + +} + +void FloodFill::execute() +{ + if (_color == _toColor) + return; + + seedFill(_seedPos.x(), _seedPos.y()); + + // Colorize all filled pixels + for (int x = 0; x < _filledPixels.size().width(); ++x) + { + for (int y = 0; y < _filledPixels.size().height(); ++y) + { + if (_filledPixels.getFlag(x, y)) + _targetPixels.at(y, x) = _toColor.isValid() ? _toColor : QColor{0, 0, 0, 0}; + } + } +} diff --git a/Grinder/cv/algorithms/FloodFill.h b/Grinder/cv/algorithms/FloodFill.h index cc0b41c..3fbbbb4 100644 --- a/Grinder/cv/algorithms/FloodFill.h +++ b/Grinder/cv/algorithms/FloodFill.h @@ -1,30 +1,30 @@ -/****************************************************************************** - * File: FloodFill.h - * Date: 13.7.2018 - *****************************************************************************/ - -#ifndef FLOODFILL_H -#define FLOODFILL_H - -#include <QColor> -#include <QPoint> - -#include "SeedFill.h" -#include "cv/Pixel.h" - -namespace grndr -{ - class FloodFill : public SeedFill - { - public: - FloodFill(cv::Mat& matrix, QPoint pos, QColor fromColor, QColor toColor, float tolerance, bool perceivedDifference = true); - - public: - virtual void execute() override; - - private: - QColor _toColor; - }; -} - -#endif +/****************************************************************************** + * File: FloodFill.h + * Date: 13.7.2018 + *****************************************************************************/ + +#ifndef FLOODFILL_H +#define FLOODFILL_H + +#include <QColor> +#include <QPoint> + +#include "SeedFill.h" +#include "cv/Pixel.h" + +namespace grndr +{ + class FloodFill : public SeedFill + { + public: + FloodFill(const cv::Mat& sourceMatrix, cv::Mat& targetMatrix, QPoint pos, QColor fromColor, QColor toColor, float tolerance, bool perceivedDifference = true); + + public: + virtual void execute() override; + + private: + QColor _toColor; + }; +} + +#endif diff --git a/Grinder/cv/algorithms/SeedFill.cpp b/Grinder/cv/algorithms/SeedFill.cpp index 873469d..5c8ec45 100644 --- a/Grinder/cv/algorithms/SeedFill.cpp +++ b/Grinder/cv/algorithms/SeedFill.cpp @@ -1,114 +1,114 @@ -/****************************************************************************** - * File: SeedFill.cpp - * Date: 08.8.2018 - *****************************************************************************/ - -#include "Grinder.h" -#include "SeedFill.h" -#include "cv/CVUtils.h" - -#include <stack> - -SeedFill::SeedFill(cv::Mat& matrix, QPoint pos, QColor color, float tolerance, bool perceivedDifference) : CVAlgorithm(matrix), - _seedPos{pos}, _color{color}, _tolerance{tolerance}, _perceivedDifference{perceivedDifference}, _pixels{matrix} -{ - -} - -void SeedFill::execute() -{ - seedFill(_seedPos.x(), _seedPos.y()); -} - -void SeedFill::seedFill(int x, int y) -{ - _filledPixels.reset(QSize{_matrix.cols, _matrix.rows}); - - // Based on Paul Heckbert's Seed Fill Algorithm (crude & ugly, but reasonably fast) - std::stack<LineSegment> stack; - - auto push = [&stack, this](int y, int xl, int xr, int dy) { - if (y + dy >= 0 && y + dy < _matrix.rows) - stack.push(LineSegment{y, xl, xr, dy}); - }; - - auto checkColor = [this](int x, int y, QColor pixelColor) { - if (_filledPixels.isSet(x, y)) - return false; - - return ((_color.isValid() && CVUtils::compareColors(pixelColor, _color, _tolerance, _perceivedDifference)) || (!_color.isValid() && pixelColor.alpha() == 0)); - }; - - push(y, x, x, 1); - push(y + 1, x, x, -1); - - while (!stack.empty()) - { - auto lineSegment = stack.top(); - stack.pop(); - - int y = lineSegment.y + lineSegment.dy; - int l = 0; - - for (x = lineSegment.xl; x >= 0; --x) - { - auto pixel = _pixels.at(y, x); - auto pixelColor = pixel.get(); - - if (checkColor(x, y, pixelColor)) - _filledPixels.set(x, y); - else - break; - } - - bool skip = false; - - if (x >= lineSegment.xl) - skip = true; - - if (!skip) - { - l = x + 1; - - if (l < lineSegment.xl) - push(y, l, lineSegment.xl - 1, -lineSegment.dy); - - x = lineSegment.xl + 1; - } - - do - { - if (!skip) - { - for (; x < _matrix.cols; ++x) - { - auto pixel = _pixels.at(y, x); - auto pixelColor = pixel.get(); - - if (checkColor(x, y, pixelColor)) - _filledPixels.set(x, y); - else - break; - } - - push(y, l, x - 1, lineSegment.dy); - - if (x > lineSegment.xr + 1) - push(y, lineSegment.xr + 1, x - 1, -lineSegment.dy); - } - - skip = false; - - for (x++; x <= lineSegment.xr; ++x) - { - auto pixel = _pixels.at(y, x); - auto pixelColor = pixel.get(); - - if (checkColor(x, y, pixelColor)) - break; - } - - l = x; - } while (x <= lineSegment.xr); - } -} +/****************************************************************************** + * File: SeedFill.cpp + * Date: 08.8.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "SeedFill.h" +#include "cv/CVUtils.h" + +#include <stack> + +SeedFill::SeedFill(const cv::Mat& sourceMatrix, cv::Mat& targetMatrix, QPoint pos, QColor color, float tolerance, bool perceivedDifference) : CVAlgorithm(targetMatrix), + _sourceMatrix{sourceMatrix}, _sourcePixels{sourceMatrix}, _targetMatrix{targetMatrix}, _targetPixels{targetMatrix}, _seedPos{pos}, _color{color}, _tolerance{tolerance}, _perceivedDifference{perceivedDifference} +{ + +} + +void SeedFill::execute() +{ + seedFill(_seedPos.x(), _seedPos.y()); +} + +void SeedFill::seedFill(int x, int y) +{ + _filledPixels.reset(QSize{_targetMatrix.cols, _targetMatrix.rows}); + + // Based on Paul Heckbert's Seed Fill Algorithm (crude & ugly, but reasonably fast) + std::stack<LineSegment> stack; + + auto push = [&stack, this](int y, int xl, int xr, int dy) { + if (y + dy >= 0 && y + dy < _targetMatrix.rows) + stack.push(LineSegment{y, xl, xr, dy}); + }; + + auto checkColor = [this](int x, int y, QColor pixelColor) { + if (_filledPixels.isSet(x, y)) + return false; + + return ((_color.isValid() && CVUtils::compareColors(pixelColor, _color, _tolerance, _perceivedDifference)) || (!_color.isValid() && pixelColor.alpha() == 0)); + }; + + push(y, x, x, 1); + push(y + 1, x, x, -1); + + while (!stack.empty()) + { + auto lineSegment = stack.top(); + stack.pop(); + + int y = lineSegment.y + lineSegment.dy; + int l = 0; + + for (x = lineSegment.xl; x >= 0; --x) + { + auto pixel = _sourcePixels.at(y, x); + auto pixelColor = pixel.get(); + + if (checkColor(x, y, pixelColor)) + _filledPixels.set(x, y); + else + break; + } + + bool skip = false; + + if (x >= lineSegment.xl) + skip = true; + + if (!skip) + { + l = x + 1; + + if (l < lineSegment.xl) + push(y, l, lineSegment.xl - 1, -lineSegment.dy); + + x = lineSegment.xl + 1; + } + + do + { + if (!skip) + { + for (; x < _targetMatrix.cols; ++x) + { + auto pixel = _sourcePixels.at(y, x); + auto pixelColor = pixel.get(); + + if (checkColor(x, y, pixelColor)) + _filledPixels.set(x, y); + else + break; + } + + push(y, l, x - 1, lineSegment.dy); + + if (x > lineSegment.xr + 1) + push(y, lineSegment.xr + 1, x - 1, -lineSegment.dy); + } + + skip = false; + + for (x++; x <= lineSegment.xr; ++x) + { + auto pixel = _sourcePixels.at(y, x); + auto pixelColor = pixel.get(); + + if (checkColor(x, y, pixelColor)) + break; + } + + l = x; + } while (x <= lineSegment.xr); + } +} diff --git a/Grinder/cv/algorithms/SeedFill.h b/Grinder/cv/algorithms/SeedFill.h index afd0332..52aac4d 100644 --- a/Grinder/cv/algorithms/SeedFill.h +++ b/Grinder/cv/algorithms/SeedFill.h @@ -1,47 +1,50 @@ -/****************************************************************************** - * File: SeedFill.h - * Date: 08.8.2018 - *****************************************************************************/ - -#ifndef SEEDFILL_H -#define SEEDFILL_H - -#include "CVAlgorithm.h" -#include "common/RangeMap.h" -#include "common/FlagMatrix.h" -#include "cv/Pixel.h" - -namespace grndr -{ - class SeedFill : public CVAlgorithm - { - public: - SeedFill(cv::Mat& matrix, QPoint pos, QColor color, float tolerance, bool perceivedDifference = true); - - public: - virtual void execute() override; - - public: - const FlagMatrix<>& filledPixels() const { return _filledPixels; } - - protected: - struct LineSegment - { - int y, xl, xr, dy; - }; - - void seedFill(int x, int y); - - protected: - QPoint _seedPos{0, 0}; - QColor _color; - float _tolerance{0.0f}; - bool _perceivedDifference{true}; - - PixelAccessor _pixels; - - FlagMatrix<> _filledPixels; - }; -} - -#endif +/****************************************************************************** + * File: SeedFill.h + * Date: 08.8.2018 + *****************************************************************************/ + +#ifndef SEEDFILL_H +#define SEEDFILL_H + +#include "CVAlgorithm.h" +#include "common/RangeMap.h" +#include "common/FlagMatrix.h" +#include "cv/Pixel.h" + +namespace grndr +{ + class SeedFill : public CVAlgorithm + { + public: + SeedFill(const cv::Mat& sourceMatrix, cv::Mat& targetMatrix, QPoint pos, QColor color, float tolerance, bool perceivedDifference = true); + + public: + virtual void execute() override; + + public: + const FlagMatrix<>& filledPixels() const { return _filledPixels; } + + protected: + struct LineSegment + { + int y, xl, xr, dy; + }; + + void seedFill(int x, int y); + + protected: + const cv::Mat& _sourceMatrix; + ConstPixelAccessor _sourcePixels; + cv::Mat& _targetMatrix; + PixelAccessor _targetPixels; + + QPoint _seedPos{0, 0}; + QColor _color; + float _tolerance{0.0f}; + bool _perceivedDifference{true}; + + FlagMatrix<> _filledPixels; + }; +} + +#endif diff --git a/Grinder/cv/algorithms/wand/ColorDistanceWandAlgorithm.cpp b/Grinder/cv/algorithms/wand/ColorDistanceWandAlgorithm.cpp index 9dd2855..b4b8edc 100644 --- a/Grinder/cv/algorithms/wand/ColorDistanceWandAlgorithm.cpp +++ b/Grinder/cv/algorithms/wand/ColorDistanceWandAlgorithm.cpp @@ -10,13 +10,21 @@ const WandAlgorithm::AlgorithmType ColorDistanceWandAlgorithm::type_value = WandAlgorithm::AlgorithmType::ColorDistance; -LayerPixelsData::Selection ColorDistanceWandAlgorithm::execute(QPoint seed, const Layer& layer) +LayerPixelsData::Selection ColorDistanceWandAlgorithm::execute(QPoint seed, const Layer& layer, const cv::Mat& sourceImage) { const auto& layerPixels = layer.layerPixels().data(); auto pixelsMatrix = layerPixels.toMatrix(true); // Use the seed fill algorithm to get all pixels in the region around the seed position - SeedFill seedFill{pixelsMatrix, seed, layerPixels.get(seed), tolerance()->getRelativeValue(), *usePerceivedDistance()}; + QColor sourceColor = layerPixels.get(seed); + + if (!sourceImage.empty()) + { + ConstPixelAccessor pixels{sourceImage}; + sourceColor = pixels.at(seed); + } + + SeedFill seedFill{!sourceImage.empty() ? sourceImage : pixelsMatrix, pixelsMatrix, seed, sourceColor, tolerance()->getRelativeValue(), *usePerceivedDistance()}; seedFill.execute(); return seedFill.filledPixels(); } diff --git a/Grinder/cv/algorithms/wand/ColorDistanceWandAlgorithm.h b/Grinder/cv/algorithms/wand/ColorDistanceWandAlgorithm.h index eb7e560..7b6b48a 100644 --- a/Grinder/cv/algorithms/wand/ColorDistanceWandAlgorithm.h +++ b/Grinder/cv/algorithms/wand/ColorDistanceWandAlgorithm.h @@ -18,7 +18,7 @@ namespace grndr static const WandAlgorithm::AlgorithmType type_value; public: - virtual LayerPixelsData::Selection execute(QPoint seed, const Layer& layer) override; + virtual LayerPixelsData::Selection execute(QPoint seed, const Layer& layer, const cv::Mat& sourceImage) override; public: auto tolerance() { return dynamic_cast<PercentProperty*>(_tolerance.get()); } diff --git a/Grinder/cv/algorithms/wand/EdgesWandAlgorithm.cpp b/Grinder/cv/algorithms/wand/EdgesWandAlgorithm.cpp index b4debf2..f4b8bea 100644 --- a/Grinder/cv/algorithms/wand/EdgesWandAlgorithm.cpp +++ b/Grinder/cv/algorithms/wand/EdgesWandAlgorithm.cpp @@ -11,10 +11,15 @@ const WandAlgorithm::AlgorithmType EdgesWandAlgorithm::type_value = WandAlgorithm::AlgorithmType::Edges; -LayerPixelsData::Selection EdgesWandAlgorithm::execute(QPoint seed, const Layer& layer) +LayerPixelsData::Selection EdgesWandAlgorithm::execute(QPoint seed, const Layer& layer, const cv::Mat& sourceImage) { const auto& layerPixels = layer.layerPixels().data(); - auto pixelsMatrix = layerPixels.toMatrix(true); + cv::Mat pixelsMatrix; + + if (!sourceImage.empty()) + pixelsMatrix = sourceImage.clone(); + else + pixelsMatrix = layerPixels.toMatrix(true); // Use the Canny algorithm to detect edges cv::cvtColor(pixelsMatrix, pixelsMatrix, cv::COLOR_BGR2GRAY); @@ -38,7 +43,7 @@ LayerPixelsData::Selection EdgesWandAlgorithm::execute(QPoint seed, const Layer& // Get the contour under the seed position uint8_t edgeValue = contourRegions.at<uint8_t>(seed.y(), seed.x()); - LayerPixelsData::Selection selection{layer.getLayerSize()}; + LayerPixelsData::Selection selection{pixelsMatrix.cols, pixelsMatrix.rows}; for (int x = 0; x < contourRegions.cols; ++x) { diff --git a/Grinder/cv/algorithms/wand/EdgesWandAlgorithm.h b/Grinder/cv/algorithms/wand/EdgesWandAlgorithm.h index b8906e5..2f501e0 100644 --- a/Grinder/cv/algorithms/wand/EdgesWandAlgorithm.h +++ b/Grinder/cv/algorithms/wand/EdgesWandAlgorithm.h @@ -1,38 +1,38 @@ -/****************************************************************************** - * File: EdgesWandAlgorithm.h - * Date: 08.8.2018 - *****************************************************************************/ - -#ifndef EDGESWANDALGORITHM_H -#define EDGESWANDALGORITHM_H - -#include "WandAlgorithm.h" - -namespace grndr -{ - class EdgesWandAlgorithm : public WandAlgorithm - { - Q_OBJECT - - public: - static const WandAlgorithm::AlgorithmType type_value; - - public: - virtual LayerPixelsData::Selection execute(QPoint seed, const Layer& layer) override; - - public: - auto thresholdLow() { return dynamic_cast<UIntProperty*>(_thresholdLow.get()); } - auto thresholdLow() const { return dynamic_cast<const UIntProperty*>(_thresholdLow.get()); } - auto thresholdHigh() { return dynamic_cast<UIntProperty*>(_thresholdHigh.get()); } - auto thresholdHigh() const { return dynamic_cast<const UIntProperty*>(_thresholdHigh.get()); } - - protected: - virtual void createProperties() override; - - private: - std::shared_ptr<PropertyBase> _thresholdLow; - std::shared_ptr<PropertyBase> _thresholdHigh; - }; -} - -#endif +/****************************************************************************** + * File: EdgesWandAlgorithm.h + * Date: 08.8.2018 + *****************************************************************************/ + +#ifndef EDGESWANDALGORITHM_H +#define EDGESWANDALGORITHM_H + +#include "WandAlgorithm.h" + +namespace grndr +{ + class EdgesWandAlgorithm : public WandAlgorithm + { + Q_OBJECT + + public: + static const WandAlgorithm::AlgorithmType type_value; + + public: + virtual LayerPixelsData::Selection execute(QPoint seed, const Layer& layer, const cv::Mat& sourceImage) override; + + public: + auto thresholdLow() { return dynamic_cast<UIntProperty*>(_thresholdLow.get()); } + auto thresholdLow() const { return dynamic_cast<const UIntProperty*>(_thresholdLow.get()); } + auto thresholdHigh() { return dynamic_cast<UIntProperty*>(_thresholdHigh.get()); } + auto thresholdHigh() const { return dynamic_cast<const UIntProperty*>(_thresholdHigh.get()); } + + protected: + virtual void createProperties() override; + + private: + std::shared_ptr<PropertyBase> _thresholdLow; + std::shared_ptr<PropertyBase> _thresholdHigh; + }; +} + +#endif diff --git a/Grinder/cv/algorithms/wand/WandAlgorithm.h b/Grinder/cv/algorithms/wand/WandAlgorithm.h index b720ee4..72009df 100644 --- a/Grinder/cv/algorithms/wand/WandAlgorithm.h +++ b/Grinder/cv/algorithms/wand/WandAlgorithm.h @@ -1,34 +1,34 @@ -/****************************************************************************** - * File: WandAlgorithm.h - * Date: 07.8.2018 - *****************************************************************************/ - -#ifndef WANDALGORITHM_H -#define WANDALGORITHM_H - -#include "common/properties/PropertyObject.h" -#include "image/Layer.h" - -namespace grndr -{ - class WandAlgorithm : public PropertyObject - { - Q_OBJECT - - public: - enum class AlgorithmType - { - ColorDistance, - Edges, - Watershed, - }; - - public: - virtual void initWandAlgorithm(); - - public: - virtual LayerPixelsData::Selection execute(QPoint seed, const Layer& layer) = 0; - }; -} - -#endif +/****************************************************************************** + * File: WandAlgorithm.h + * Date: 07.8.2018 + *****************************************************************************/ + +#ifndef WANDALGORITHM_H +#define WANDALGORITHM_H + +#include "common/properties/PropertyObject.h" +#include "image/Layer.h" + +namespace grndr +{ + class WandAlgorithm : public PropertyObject + { + Q_OBJECT + + public: + enum class AlgorithmType + { + ColorDistance, + Edges, + Watershed, + }; + + public: + virtual void initWandAlgorithm(); + + public: + virtual LayerPixelsData::Selection execute(QPoint seed, const Layer& layer, const cv::Mat& sourceImage) = 0; + }; +} + +#endif diff --git a/Grinder/cv/algorithms/wand/WatershedWandAlgorithm.cpp b/Grinder/cv/algorithms/wand/WatershedWandAlgorithm.cpp index fbc61e0..b68f78c 100644 --- a/Grinder/cv/algorithms/wand/WatershedWandAlgorithm.cpp +++ b/Grinder/cv/algorithms/wand/WatershedWandAlgorithm.cpp @@ -11,9 +11,9 @@ const WandAlgorithm::AlgorithmType WatershedWandAlgorithm::type_value = WandAlgorithm::AlgorithmType::Watershed; -LayerPixelsData::Selection WatershedWandAlgorithm::execute(QPoint seed, const Layer& layer) +LayerPixelsData::Selection WatershedWandAlgorithm::execute(QPoint seed, const Layer& layer, const cv::Mat& sourceImage) { - auto layerSize = layer.getLayerSize(); + QSize layerSize = !sourceImage.empty() ? QSize{sourceImage.cols, sourceImage.rows} : layer.getLayerSize(); // Create markers for the Watershed algorithm cv::Mat markers = cv::Mat::zeros(layerSize.height(), layerSize.width(), CV_32SC1); @@ -21,7 +21,13 @@ LayerPixelsData::Selection WatershedWandAlgorithm::execute(QPoint seed, const La cv::circle(markers, cv::Point{0, 0}, 3, cv::Scalar::all(1), -1); // Extra marker to make Watershed work // Use the Watershed algorithm to extract the clicked region - auto pixelsMatrix = layer.layerPixels().data().toMatrix(); + cv::Mat pixelsMatrix; + + if (!sourceImage.empty()) + pixelsMatrix = sourceImage.clone(); + else + pixelsMatrix = layer.layerPixels().data().toMatrix(); + cv::watershed(pixelsMatrix, markers); // Get the correct marker diff --git a/Grinder/cv/algorithms/wand/WatershedWandAlgorithm.h b/Grinder/cv/algorithms/wand/WatershedWandAlgorithm.h index 775fba8..eedb076 100644 --- a/Grinder/cv/algorithms/wand/WatershedWandAlgorithm.h +++ b/Grinder/cv/algorithms/wand/WatershedWandAlgorithm.h @@ -1,28 +1,28 @@ -/****************************************************************************** - * File: WatershedWandAlgorithm.h - * Date: 08.8.2018 - *****************************************************************************/ - -#ifndef WATERSHEDWANDALGORITHM_H -#define WATERSHEDWANDALGORITHM_H - -#include "WandAlgorithm.h" - -namespace grndr -{ - class WatershedWandAlgorithm : public WandAlgorithm - { - Q_OBJECT - - public: - static const WandAlgorithm::AlgorithmType type_value; - - public: - virtual LayerPixelsData::Selection execute(QPoint seed, const Layer& layer) override; - - protected: - virtual void createProperties() override; - }; -} - -#endif +/****************************************************************************** + * File: WatershedWandAlgorithm.h + * Date: 08.8.2018 + *****************************************************************************/ + +#ifndef WATERSHEDWANDALGORITHM_H +#define WATERSHEDWANDALGORITHM_H + +#include "WandAlgorithm.h" + +namespace grndr +{ + class WatershedWandAlgorithm : public WandAlgorithm + { + Q_OBJECT + + public: + static const WandAlgorithm::AlgorithmType type_value; + + public: + virtual LayerPixelsData::Selection execute(QPoint seed, const Layer& layer, const cv::Mat& sourceImage) override; + + protected: + virtual void createProperties() override; + }; +} + +#endif diff --git a/Grinder/image/ImageBuild.cpp b/Grinder/image/ImageBuild.cpp index 173af86..401f635 100644 --- a/Grinder/image/ImageBuild.cpp +++ b/Grinder/image/ImageBuild.cpp @@ -153,7 +153,7 @@ void ImageBuild::autoGenerateImageTag(QColor color) } } -cv::Mat ImageBuild::renderImageBuild(std::vector<const Layer*> allowedLayers, bool renderableOnly) const +cv::Mat ImageBuild::renderImageBuild(std::vector<const Layer*> allowedLayers, bool renderableOnly, bool ignoreAlpha) const { // Make sure that the rendered image is an 8-bit color one cv::Mat renderedImage; @@ -168,7 +168,7 @@ cv::Mat ImageBuild::renderImageBuild(std::vector<const Layer*> allowedLayers, bo if (allowedLayers.empty() || std::find(allowedLayers.cbegin(), allowedLayers.cend(), layer.get()) != allowedLayers.cend()) { if (layer->hasFlag(Layer::Flag::Renderable) || !renderableOnly) - layer->renderLayer(renderedImage); + layer->renderLayer(renderedImage, true, ignoreAlpha); } } diff --git a/Grinder/image/ImageBuild.h b/Grinder/image/ImageBuild.h index b7e41dc..7f28e94 100644 --- a/Grinder/image/ImageBuild.h +++ b/Grinder/image/ImageBuild.h @@ -48,7 +48,7 @@ namespace grndr void autoGenerateImageTag(QColor color); public: - cv::Mat renderImageBuild(std::vector<const Layer*> allowedLayers = {}, bool renderableOnly = true) const; + cv::Mat renderImageBuild(std::vector<const Layer*> allowedLayers = {}, bool renderableOnly = true, bool ignoreAlpha = true) const; cv::Mat renderImageBuildItems(QColor backgroundColor, std::vector<const Layer*> allowedLayers = {}, bool renderableOnly = true) const; ImageTagsBitmap renderImageTagsBitmap(bool renderableOnly = true) const; diff --git a/Grinder/image/Layer.cpp b/Grinder/image/Layer.cpp index d22f0fc..80d2af0 100644 --- a/Grinder/image/Layer.cpp +++ b/Grinder/image/Layer.cpp @@ -94,7 +94,7 @@ void Layer::removeDraftItem(const DraftItem* item) } } -void Layer::renderLayer(cv::Mat& image, bool renderBackground) const +void Layer::renderLayer(cv::Mat& image, bool renderBackground, bool ignoreAlpha) const { LayerPixels renderedPixels = _layerPixels; @@ -120,7 +120,7 @@ void Layer::renderLayer(cv::Mat& image, bool renderBackground) const } // Render the pixels to the target image - renderedPixels.data().renderPixels(image, 1.0f); + renderedPixels.data().renderPixels(image, ignoreAlpha ? 1.0f : static_cast<float>(_alpha) / 100.0f); } void Layer::renderImageTag(const ImageTag* imageTag, ImageTagsBitmap& imageTagsBitmap) const diff --git a/Grinder/image/Layer.h b/Grinder/image/Layer.h index 4db5c89..892bd26 100644 --- a/Grinder/image/Layer.h +++ b/Grinder/image/Layer.h @@ -56,7 +56,7 @@ namespace grndr void removeDraftItem(const DraftItem* item); public: - void renderLayer(cv::Mat& image, bool renderBackground = true) const; + void renderLayer(cv::Mat& image, bool renderBackground = true, bool ignoreAlpha = true) const; void renderImageTag(const ImageTag* imageTag, ImageTagsBitmap& imageTagsBitmap) const; public: diff --git a/Grinder/image/LayerPixelsData.cpp b/Grinder/image/LayerPixelsData.cpp index 8cd690b..efdc5ce 100644 --- a/Grinder/image/LayerPixelsData.cpp +++ b/Grinder/image/LayerPixelsData.cpp @@ -65,17 +65,40 @@ void LayerPixelsData::fill(const LayerPixelsData::Selection& selection, QColor c emitDataModified(); } -void LayerPixelsData::floodFill(int x, int y, QColor color, float tolerance, bool perceivedDifference) +void LayerPixelsData::floodFill(int x, int y, QColor color, float tolerance, bool perceivedDifference, const cv::Mat& sourceImage) { // Perform the flood fill on a Matrix for simplicity & efficiency + QColor sourceColor = get(x, y); + + if (!sourceImage.empty()) + { + ConstPixelAccessor pixels{sourceImage}; + sourceColor = pixels.at(y, x); + } + cv::Mat matrix = toMatrix(true); - FloodFill floodFill{matrix, QPoint{x, y}, get(x, y), color, tolerance, perceivedDifference}; + FloodFill floodFill{!sourceImage.empty() ? sourceImage : matrix, matrix, QPoint{x, y}, sourceColor, color, tolerance, perceivedDifference}; floodFill.execute(); // Clear the original pixels data and draw the modified image fromMatrix(matrix); } +void LayerPixelsData::copy(const LayerPixelsData& source) +{ + for (int y = 0; y < std::min(_pixels->height(), source._pixels->height()); ++y) + { + auto rgbIn = reinterpret_cast<const QRgb*>(source._pixels->constScanLine(y)); + auto rgbOut = reinterpret_cast<QRgb*>(_pixels->scanLine(y)); + + for (int x = 0; x < std::min(_pixels->width(), source._pixels->width()); ++x) + { + if (rgbIn[x] != 0) + rgbOut[x] = rgbIn[x]; + } + } +} + void LayerPixelsData::clear(int x, int y) { if (checkBounds(x, y)) @@ -99,7 +122,6 @@ void LayerPixelsData::resize(QSize newSize) createPixels(newSize); emitDataModified(); } - } } diff --git a/Grinder/image/LayerPixelsData.h b/Grinder/image/LayerPixelsData.h index 6eac07c..625ca62 100644 --- a/Grinder/image/LayerPixelsData.h +++ b/Grinder/image/LayerPixelsData.h @@ -44,8 +44,9 @@ namespace grndr void set(int x, int y, QColor color); void set(QPoint pos, QColor color) { set(pos.x(), pos.y(), color); } void fill(const Selection& selection, QColor color); - void floodFill(int x, int y, QColor color, float tolerance, bool perceivedDifference); - void floodFill(QPoint seedPos, QColor color, float tolerance, bool perceivedDifference) { floodFill(seedPos.x(), seedPos.y(), color, tolerance, perceivedDifference); } + void floodFill(int x, int y, QColor color, float tolerance, bool perceivedDifference, const cv::Mat& sourceImage = {}); + void floodFill(QPoint seedPos, QColor color, float tolerance, bool perceivedDifference, const cv::Mat& inputImage = {}) { floodFill(seedPos.x(), seedPos.y(), color, tolerance, perceivedDifference, inputImage); } + void copy(const LayerPixelsData& source); void clear(int x, int y); void clear(QPoint pos) { clear(pos.x(), pos.y()); } void clear(const Selection& selection) { fill(selection, QColor{}); } diff --git a/Grinder/res/Grinder.qrc b/Grinder/res/Grinder.qrc index 2be16db..e5b1be0 100644 --- a/Grinder/res/Grinder.qrc +++ b/Grinder/res/Grinder.qrc @@ -76,6 +76,7 @@ <file>icons/overlay-tagged.png</file> <file>icons/batch.png</file> <file>icons/folder-out-interface-symbol.png</file> + <file>icons/sampling-image.png</file> </qresource> <qresource prefix="/"> <file>css/global.css</file> diff --git a/Grinder/res/Resources.h b/Grinder/res/Resources.h index c1f1539..d6e7f6d 100644 --- a/Grinder/res/Resources.h +++ b/Grinder/res/Resources.h @@ -88,6 +88,7 @@ #define FILE_ICON_EDITOR_LASSO ":/icons/icons/lasso.png" #define FILE_ICON_EDITOR_GRABCUT ":/icons/icons/painting/scissors.png" #define FILE_ICON_EDITOR_MORPH ":/icons/icons/arrows-group-interface-symbol-to-expand.png" +#define FILE_ICON_EDITOR_SAMPLING_IMAGE ":/icons/icons/sampling-image.png" /* Cursors */ diff --git a/Grinder/res/icons/sampling-image.png b/Grinder/res/icons/sampling-image.png new file mode 100644 index 0000000000000000000000000000000000000000..b42bac8735d1d09fabc9ada69323c4c1c3080899 GIT binary patch literal 57530 zcmb?@Wn5H!*Y40D9n#VQ3P^+G&>;ef0@5KV-OW(aDbgh%CDPp>NQX%00Mgw<&)I|b z{k+dP@7MFeAIi+$d;M3hYhBy0S1R(j4=5jiKp<R21sOFE2pRY*G6)kL_;Kn!eg*tM zHJ4PD1c54Iuy2gefPZ7zD`+`^K-m3=KS-vp%Zh+sQaH=LbADrI=ImzVXbQ47vb1%6 zW^3w9$M=lu84vdnon-<Dv?Qn~Bl*@{f7b^i*+lkCEZ%8YXhT$NVN7U)+T4|!?df_b z9rMdjhS2aFR<o`k<WDk^JPHJH>?t1~cUHDnS}(!D6Jxw>i`n_mw3)4~%uKJ=11=>u zsOH(X?FFxI`X3qZpzv!*7r(vlwa!%sCkGEn3Ouog?0L`DxjqHX$lecgc#HxQQCNul zy6UoKneko1rIvT?s#}L|CWJYh5hlHoZ4zZ&(UFbhJGOTQe@q@N8VKJIc*hW@z{H$2 zh|ddYen}q7N}#wOqLc9*i9>+dfX-cmj>LlqzAhkv_*OptavAM0MklzdO_2aI_Ig=^ z$%^TFg@j^eBziRp@U3TvZ`soP$bwR3&Yk1lq;<9PID+S&nnd~YhZ{aWe#G~wK<hm{ zQJ9Re8Byz<<sl#N6;SUjSA5^o40Ye{1*+25G7NF`Xni-}mG-)$0}eLydRj9EEwrj| zvrftH$6>&y3MMkT^=Oc~bb=+{<R>NxeNPjH2ylZp^i3Wr1IL$Ph+$XhDq-z)=Wf5X zuZmZvE|z$^3VoM{_$aQPlEGIuw2guTwVu{iqcYt1A^Jl%q2l}F5#fh~vcS8bktE3T z_<~egKg%D4tCQ5-U#FnUMrd#s-A3^kaUpYMKc?(D=ezGrhyxSF|C}!Nk0d|>u0qj5 zlUc9zd6aPU!h4MX@x<hp!9Sdu7*6Kef+2Pv=-Y3(GWs4+->A`%-$9K{@4r$%Dnso_ z01@1?LfQ}6dZE$FSCxgZ<d#fVUc`~xSQrTSwVX!z5>|)(>da4M7KBLLdgOCj=cx8$ zn23vHG?FW3lIErU`6$%=4F6_<gu(CHNI3Q(U2pAv-tE=>`7J~1&sgvBZ|8w~7BRC` zAI{f;IZ2rj#|TS{3Vpw^VmBL(_>76|yWw+3A8&-U0KHLAV9MIIL#s)EQ@)YZkf5VF zhIm&jg#w=`%<szR&V{vE!e01YFU!S0?4Pe?gtWYShxm-ijFRzWZloJpO^DFXr|(>w zK48St>W~Q!?)=&;>5&12l4#NYA_616Zu>Zx;UsCvu3r`e>x`mnx|Co&F1wJrm%@RK z;OM~YM!5(Ru)K9;TK5q2kpcPrQ*0ljOD&5W37R5~%+qH}+5vC9kfKA#?!d+~Nud&k zxQ4wlt!t?A(WiPeNrb1YXuRKj+ks042o^8t>V$cR-vi~l*F@K*0w^Me>M0;wso;$A z^7?*Eo7TOp!{G8=eO^ayoic!YXG~2g(=r(QxWot@Dd^IztnFPi(E~Cpd&VuGlHhWj zF(O2G%|s|@*8%++0VsAyGSOppH3XsWt_fZshN<qD)lu!iqPmqY$Ie^tPg9!FC$5rw zy}TT2nuvFPfA*upltGlBdRiL1xYw?$^f0-5%`=sz8m#=B>wFCBtRYG<iNAAYA9()v z1pGt}E-ZJY*Q|h?HL<48$fI>>vn>CfAw)IQkLu7(5253UPo_^r>-JQSQXc=cw6E@g zqW?U;rjkkM;`RN(lD$jkyaCaR>3KRAufeVCf%j4pO{Ij^?Mk$`P@paOvL><<cO1@x zS9s+Ss%7BzkeP46f#B-^M<Y=(Sqi#%NqDcMd4kOLh=F2ym^8P{iuELXY=B4R?-hMG z!5f)9&`wIk?PL-ySa~3d-e!N#X-oay!F>o&Y&Jn0-O$xvH$T%o`38s;7TPua9e$l; z>W5IO%nCa3(FOgD-WcNKGLeIJ@Y!RW`#4j0iOc9T*E$?ACAI2XPZakG!YW#4<+1Yj zG;caznfMCcm3or+UDL<xt|+VqT*%yu%#@!QIzqN`@C~rJG>wB%>c#yTjjtTliIE#< zpRqjacfGm3t_=DsYd+R{2kleD)7QGtieRb;p^UHI-2cXX#rQi6K4OaXk3hT(`UYWb zIdVw#wB5vHIg9t95tbHm|C?t-WJbJjPJLISzh<+1(X|(GsA&%fxXj>yw0(PTo%&1G z=MR8X7x#(~x}y8}UI)gM#7Ea@&Gj^@`yr9_ptKv<%5lWKc@<vTb+5Snm;%k?Qy3|9 z!tc*+tIzC>+kQ(Rx7*bQ;cO!K>p{v=T}SYxgYaD7yG9Z%kb5Bn!J5>7eaOQHvj!V5 z|M_B<q5D3Ge!98$7zWoSelUYbl0nd4TdW%c1%wjPP=>U4isTDCPm$s2ibLHLq;!h* zMwW|Oh=Zr?*6JcXK@hl=?Gqk&&kfy4$YBJ;_>M5k$%o~ryw3;sFdzRFJ#?9zShMey zyKP<14qWWc2f2QFQ+99UC7@lMs6lY|Wj}E;97lHD?GIF!R*6J4wD{n-|0WjRfj_eg z5zuXK!F-)e@M>zA6U_@ai0DfmaR1Gdo9(l~HliuH&rsfr>-=QrUicCQyS)Ra#LGc@ zlRMFyymo~3c3!S)ra(SsiDg~ttfXG#Pk};iRNZao6sZ}Fk=2#}XY7iiKdBmocH=cI zUzspuvA1$^jz~WcAPAqYn-A?}>EE{f=T=lN*WFU!QI|%gG0ZeCK2M-rcquf8bH1Xy zBU5B8aym*vMtDGe>>z%RoKZt->IE~+F(q9qogxU6izEet2ATQp```{RS~9xHUGn8x zGGzVAc7^B?xt$g#tK*WJyBo-tOm}~U;$u1)shHW<rB(4BfS+JUn|CM8sCCe{i#6W# zbfM)~({vmBy;rx1FXi1;wmV$;2ApF(ZKkADVO{ad;sS!EHiD=`g7_(Ic)^z7G*n$r z-12vZOrYn0rVW1ny}UlG_r{e~6z13rW_fn>3dv6braT-IItO+9VJwLVdWRE&yENfi zYKBFKRYhbD+`;P>vV3dL##>t*<caXWdaS-1K@LK1-US-yWrS(<MQf0mN;h|Td4tT4 zB~>~C%%JhWjV!Xh8+SUAJG*8GbX%qwLJG+}iV9Y|@E*(T8&LfBOMm)qE_vW8kw2e5 zO*aLE<m@)+$c7;MOVE9#5rtE$u8%BFafZT9WQag)ughU03jbc1MC{uF7Ok@x;W21S zXrKdqiBwtlZg@-~@<DC93;gd52HyidYtt|S{U$RH7hsEL6m4x6YNoY*9lm|bON5Dv zOn<g?X-<;@F<3vSV;9qhA84geFFSjct=<|I6K)?0-#l#DjgbHzT7)qIKIy#?xw)x1 z@`V%=Ilw<?Fw3KMI&mKRmZLps{4dco8BR_HZtfCIfOLfoxz_Txhm<D(5qQvTFxmo< z`&YJRqLi!{G_WoEwDAoQ7U<WX6S)7>JEFLMpT?5YLw3QziDxbO2^8uu;dMv+kGvZ) zCU?FmaOgKCB!WNR7UBOn$bWyphy?;3G##{bxgitOR-o)ps(?&K9=_WJRaXX?FcDy) zN^N(Xj7T{*iV3pEpy|Xif#&t$4bA8bGXDz5E^~1UK3eG@1?~m(mZW9=nEw$Ddz1|G zHi!(rmRu2pDiwEdOc_dmg^KJS>v?!nMX;so$EpaDS+9dcO-Tg($7UaUZ^Suithqmd zK$<Pce*bf`xXW8p2Sc_`s2~MoKj8cVPu%acLI)2xBT#hWm_QOlAmG;JiU@g4d?8xY z)8R}=pidx#FDO6Agpe|vTo0_YYcOGfVo^bW?^vrLq)34pkYdy_D1!W$K!ECu;sZ9a z01@sgpO-&WL<TV-AvB#vA_`}hS%)S4^e<5$0@3|d+gHFV=y}@O*zv4o0cjOMfWCe( zqm6*E|Nn|wUK2MpIy&=Za<lR9;KMn5R23@?`F{yyWW_+XOHN6F`$RpJ*8R4l4G%ZQ zn@Y;i*4f*b9QsfmBCBBazYZM45fl&*@I5DIWp+JrvTjhp9Af2GPlFk}aXx4xa*P}5 zrx}>t``)$Sx-zV<h|_#ymHY(z!BcGPh)U!2AU>4;b=GOh-Zw=x3-M#07;pW5WpN1f zF#5k*1@*AVY&m)v@TRiEC$@3G8OiCsO+94fo%~NFiBK$25!TsGLIq(VJc0l-B`Ur7 z=EgU7=9n9(Dp&z`f2hIb41voHQ@0(5!>>&41Zw198HtILn58p#X7Xh!3iqy1s7VB3 z0_7k~X2SeVwM9{Y4IVkRcdH4c^$4fA==qT&q?}%T!X-*CT^gv$VfRs3QQ6AXEghCW z3Bu~3SHPvnbT0r$l0wfeDG@6uEp0Wka5te5I~RFAa?@B@23gw8Y`vQ~4iA6vwU3$f zH(LNMDxd?xGdW~GREf`k3k40~PhxY@Qc|YG4l01Zy|pZriMgfQ@KAjq*4j=p^L@oJ zV#vW;-n^h9ucqYT<&((u$k?|CySz4M0x5uG6an9!o@YJS+EUMj-I|7s`DFBLK*CPa z?J%X|E?YFx{?If?U3xW0oh=7<%LyH8*2F4H@w5X@FQXR+qC+Bh3^=FJ34K#xeSKv^ zgDgKE-_66^8OLHUf#=7Tf(_9>RQ=Ps{<m?LHf4q5sdIxGb8ajJOmT~gt+uOadmIu@ z=#zv9=Nc!*0ug~;BLYYOztYjkiOR;cz{}hCSckc4jrsHnW}mrJoinyJW8_pnO#6rm ze)HL+TVGFa!QU(U5ylkK{XMY$=N=dzaG3{mhBU56Mmt-fJNWz2gLbMOF$V0wD1qmB z>rV#Q<PmAwZ43cwRtQ1tQmZ4WZA>}oy=rLxqZ$RfRe`$d<d<eUd6+jNbenD69?^oh zGpjQ5ojY+2HcisY4f0kWK8$pVrAkr~zC%Hzh(e72_{CTH%Bm`*>nn_xjvu#mQ3t;c zZ-$gluN_^{uK3gunmD05d;S^vLmW?=nwt1a2(=i5Fs`Y}y>YD`f~vbhB;G+*VW;Oc zf&%=HwwyhD51hR+*F~##h9y`XjeMkYDg7^U(Q_|1p2Zww0v%M|;rvaw4jN0uvpN_O z;I;AlnTvI^yTdA<6)6;mF*NTVu=eur1BgIlpHfu&*Iroq`brAT(%IZd(ov&7c<|s@ z>BS3Abo7wL62a7Gj$S@gL>Cl<r}(pvN}+Uzn&r)JH8j*?R9_A`yTQEGawqi2F+b8~ z&DHwA<x07Lp%!+HoNW%M`GAI!=90O~kAsu5tR^StlMSCtC?bRjP5&bRWr=Ud!d9zJ z=_?;J{6gi@w=T={9N{&J$o_Q5LPA1$92_N`#PM$qKJ}Z8xIJH>Y7K^xVeap3?h511 zJkjf&pf5~d`)udn;cZSiG`T~N?1nBCx7p|4Z1p(s<rfQ=;EV?eSJofZe8yL+O==73 z%CJn6Xb1izq9deOd-1Ot&Tw%zxV<1;H>pF*jTm?<)R+M=pA!=lN13;7ANj|U=c&|b zmQ8L(OBy!)+Bz2PLtdyfcajFWRDD^ATcYF7n++VCY@A7{gOjkQ>Z!c*`cPc|SMFp7 zo_!3&k7&x^9*MP!hF966)*fWXKQ#!;HBog)5&rs_<=$UQ6+zXtH4u?IVQURe>gjm1 z;yvxcvx98CO05<?RX>U6Br}^fBU0=TvX(6fMyJ!r7x|WT6wk>(1<2H|Uxz)hg4`ok zl5d=xs-%iOQ7I?F23EtJkUI|dqo@fDWg1!@h?{YxlnH2k2eUtUa>Qsifvq*KjVLBL zIEtWHP^hCa@)wBV;cdDVH@!U_bqe$kesGpweACz3+Tn@W+3s58JVntUj)?sH{G0CX zeD1)}o4{A@`-Gw_WtD68&E}i-SVj)^{JB`YBPa3kV|MZavYefrP8nk2;;*OYe`|*< zUYIB;?Swsz+J`83bqYK-&SBiPTd(DYoO~YGYw-nwoN?{pLz`aX4k;C0+I!8={;Qe7 zaZ26|?T}cpFYa7!RxZJ9HPy9`5`f2$SyiF_(2(@Cv`iss8IMo59g%it#$&3gv#t@M zNli`{#)o9B_>3?jZ;rjFnT=?s84eZ|%FD~c^|?hkIKB)dd#;y>L63fZ^jnEkt0CaJ z;4v@0qWKy$EB#Y_Tv`6dUDIcXXlCWSk7nI?(BV}ev3t8LCt;;fdf9ZwzD^|b08F^4 zan-}a!}<)g2a@ZRF?mQrk|=rlt;Vx6=DTYT+<m0WGZszkGynz!k5}xZ;LZ-!TLpvY z#lx0n=n3XE3qd(QJ9KoOR=ocT1Kr&GYgHEIBdRtAf^g9gK7kwl5Fi;Zt{v(9vG-81 zw6#o_nw&ISRbaYzU+I6{*Tv<mrBdr>Zu-t+CMVQZiNh2i>J2&+>*r=gW|vrJwvjw# zC8ozE@Sm4uXII!-drO7X-ZI8;9-5f=>Zl>pB7^6Y4<2G3HQ_R!zzFB_ceZ!Ppv%Sd z^|Ii!<p!G=$-$8>>nY*}Sy|bKH8n1zp<kev$k`$i%;@?Ug4RD6sUDUK=>qvc8Lm+> z`mw3W>xz<+YNo3r@|{{QqEIx%A_~P?DyoEwi*lN7c4LZ@*lUvm)*+3#G3|^yRRSW~ z^iP9>>#M8dxnl7Ibk+jpMt7wYGvD3ho?g=g9H3y{D9%Z+5Ig|y;RoBv2p5b`O%=!8 z9lOC)M0G|Etv&d3tl<fR3O&ZPeNrk$Mx#t!qXm&>I|sV92R7CwepnRxnV)1YGHxkL z?)r-?`^b8FSi}y+P<pATqUNi8LlCvNk`7Ue`}wi_y7TirGZyW+H$02;=XSihmmLlA zURkP~UEJ77r<pZD;s62BPqXXbnOYx@HG$~1J?)LJR0R_cVqT~=iMA>cwu{?T%xEo{ z`|l`k?(IM##`ZiC+#b=eFc&-vrV#n&#@If&@20?{mZo7RiZ^ndb&5WA+wA=~9xrKw z|BwbK!Owof8T#83GU7Yev8@}Ghwl9l<zGMai+@r#lhL-MYo}}?9RL6(wx{W?OO0g8 zie}>ewL{T3d3xwrTcH}K#%-(4UYzpf3;v7NzwtJWdT#OXa1D4wh0kj#imVGODxNGi zx`oE#b9J;gk;c@o+Zj$a8@3qr+8%7OYZi_#EQqzB2abBDo5)>p*7ejSCH0uZ4rNIQ z?O_9h+u;Q=EO~J9q`!Y)#<4f*Swtr?!Yg5_|9YixO9g?-0m<+gUPOhWGKT#g+2^Nc zF1NQRSxNn7Kpf`jcvj&{Y17pKX_?9fev5en4|-?11I4ct7_b@$QNoPe`22JT+Su3# zO-V|c!V90deAcse^r)w4@x|LBersrSD%weuysPUup{(rBpDr#gfh|5h2V6RP8-RC) z1QMwCJ)K*N4D&~^1u`&ZfXM9Ztg1g9x3qi3Yyav@Hbf88B^LjWYqqaeGpXi{`Yw`T zGTRj4Q#}O1JbnD-ff!C{Xz<W+1w{SePd5>Le)l9|!WTCH4f~~{mz#}SV`kMXH3Z13 z3;LP{(9l)QD((Dsi^}xDru`Q(BQVtIqgo^tG{@jz`Tk<74`vA;h8!p}?s)7w>*UQC zQ2^sPX*8Yu?#PZ)r8jtEsJ+keS1h0nx{nx8?A+v}YxTmir@zzC*I&!d-~u%(&a%Qn zZ#yq}c_*XSuk|f08!tGdn6tlp0kIG6>~GnWloX3RBX|If%3N;5KdH)OVWc{fYWGkq zQiJE^Rt>1@s{Kg(UV@fa->ubLt-*kLE^m|+-J&aiUzlCmDjBhOf$#1A3lDXD>Rpdt z0>y#;CrM{V{gVr1d?nK=h=ql<vxoOxIKxF|W#uXP=T)GLth6fJmyr@=I*eh5;k6_2 z;RdtcmRu<Zl+=`W7rmR)^*_zZWPLXc34VCe6|}VEdRu!Nmjm4`<0jH#Co*0cZYjo0 zX-anQJYM`yPa|bzj=9JU`?`_RPYR}^PQqPWJZ(`!Ego)z0vR3a$TbraQ;^>#wfewJ zOlwqTNQWul4M8dcm%n#+U(M}g%zZ<^jO*ibYfa(Qzy9<H=ye_bHgpXdNU>2E?b~M< zg5Kx{3@F*X`ni3MX<&k@<!Tc%Ta8k3xmF7WzjEf)_4Ua!e4OpI2WuPb8rKOHb7hCK z3!eArNwZQh@6Fo+!Dh5RLBAbBnQdm~g3{%@zTAm@cZUz!JY-c1IEJQO`VkkgFuQVk zPf<Aj#f{`>G0kM)p;*~YRDM%jNEM%O=QWU=S7&BsihcB|o*|0y3eA7Cs8sq&an$RA zb0d{X1xV9h#?~&2-!g+s%gbZ;g5~1Tv5Y&`6Wo`6wY{&MzWdmoHU4J*mxi7E(pBP5 zGdDUtfj(=oq-Fu>WayAmlKjUPJ_fH}bBokl@#f&bgrBCV4|b6GjG0vETRJ^Az_eQ` z3g4ZG0+aiy6^|JdjBY~;)vTa_-WAck!7XabfM5&<_w6vCJ^P@At^E*Cy#ph(r=)Az zYy5T-s~ObeiT(MKBL2<T<neL%3zLLeYG!u%^XUtBQyZHMWrny+EQ~uaYP4oN_x#8# zv&_$8AOHluI_1a}kI~ty0~M|GTAvIvD1(4(<m9*`SHrx%sJhx&z^qj(ZtnN2#?1UY z54W)RT`4dTRsl1K`E+`eNo#8u3-Kds-vCYm&)@s@dX}}bZF+Cs6c-njykFrdy1UQm zqW8hAUT*M*pMaicd~%{jS?Md8&DRxDrBzGLuD-zdH=nA2l4~dHdM%mCBGJ1IM9*7R zbxF)=19~oSD2?v##5AFWMNR5wu&4|@Jp)_2gIGqN{dKV&w5kb#053ux23pUdB=xr% zl{8lQetY55)?6vLfzq7@l|s4@*afnP2Q2fYl=rSbLA{Ou8(616kXl_|Up&k^K?ay& z5LRc1`K#P@LeLi@XX_g>%-S606#%{P!R&^-wX6oGs<R5192>gZ`qI9=#J0zr>X`8% z_DJ+&Lak+fFv>O;>}QWOyF%<VSIc69#g-X{;+VI6)v+9hUsqd$OTUMk!YUxnT@(T$ zXwmLiKEBDWs0e$Mm1f!7KQzR#yR|3HV%l9iLDh!BZBI*8xsf2v(<}3MwnO~uW#y=K z1Mc3wrkGgE2~a5$bOjN!Q5^Wb42e=Bz4{okGiR}sL;b{yZppn3zv-s(__#ECIj$_* ztr{ik03*J&Iz;ToHs+uxF0LZ$A?ZWq@hQPoCG)i>X)Q_$;y_BQO4ZR0508i#U54F+ zoDI6${ITzwwf&BJpjlMoR##oyf+1#A+b|qlTeiQSdD(Eem*X~ujoJzBaH=mwdZHXB zA|Rlzqpo){=?~y@k~e#Lr$Uj<m0EU{&UBdiAIa-Qz->!!)YT_!2v{4mGnUI|F%c!| zlK#HbcM^ggKdo7RhdMbWG_^*AcfnGw<IU2&=zpF~=2$tm(?<E=(!slpzRF|GaV_Hg zAL6!2ipbXGxY5NQKNxB~{nj6tV1D>u718MBDYEt;&rT!z1|`V+_v}OB_T`34^Agt{ zezloh^EmJR+HzZ4%iF@~4Z=1rM)FLNp}n(5`@cJff&=E~b#uYwcVgbdZFB*yIn?K0 zL<LUO^ZSf_-U47k=EVMKw5rN^Vv=V0lyq^X=zTYJb?-19&yndy%-mWr`ZZ2t89j|4 z!FoRfF6lQ@(^GvTw2Q9^X}aNEwW}|$P`0pxTss^LcmxEFgc9VeGE)=FecBZ<t@TL~ z5)-4JeBOpxodttCI=~5oG&Hj#xp)QFqPa6yMqgX9tJ*vYi|sCiky8pa9a+=F2;>bU zW88~di3V&etf}MK-X2KO-4Sjd$zlH7a5qhO$uN}k3+rlb`eId+{#^?(@f)9BDlb!} z2Z)aTtI~aA5W)zGmWajc><G_t22wa$Rl=gs`rEf}$2&cWyP26|UbCtv0fqPLvp!=J zqyUavshA~|q@L@6*1NkpQ&ZC#2Joq;;fY^ZZ68I^Zj}W%sk2D@V{J;cvN6Gh+ZA#L zbXdW>j#1h5lfE|UXlm+g)$|RDerD3xvLkqEZVu*<%mG$$FZN%6h#{Ac8}dO=J1(qC zh7ak=20tlYG$t(v(v*wnkx6{^5ny4F5A?>`xB7u-VLksU{T;HQyUDmQpO&kA=|G-# zZc}^;$Gft~-Sl*C)Mdg3HJ9p%{Tp8DD@K|!@RI{Ovc4b?<8LqI=%pB-m!kbe74@eV zkE()>JJz@NQ~&DPn=xMA1u?N6i|R&5W6Pap{OBL^sUWAt2-gc%WVpdw%DuU340Ni9 z2&LcO^yutw-|XHtiM95G1j|o9^Rx7s9+JzcDalZ#u`Vnva-bSYj0ZZ&1OhMB@;>io z9$mX<S9EC$m){mG8r6#h)5W>Co^KqXO&5aZ=2n&Wq7Yqg^?k`XE=L8?#r2!nt?DKz zMGW2TE-l6Kn6aT>z9){)sB+9}sw;!10d67ud@0FeN?)~7pCq7fY*TiTL9?)xUx>qj zk#na;hiaKCE-6VfXerOh%ZH^?61sVO@u?OG$4d}L-WytNvhI9(aVu9<6K`rn&tmz} z8Z`!fxbH7ti|Xk3K^RBGbM4Hme51L!smYw=%=hcUmUyR0#bWhqDZ@BmW9>3Nma72s zocVs!bsnvPfBaMb&9gmv1oqlhb3el#+oMTvdP<W4c6scSe`nZBTBV2UcnpBvay{Mh zhY~cVPW84+dXSdB@#iw_Y?wUw*##cHz^ot=M<yJ{<<g6u4@?VoOXLS~x8*dD-|OM5 zpT9kNRc`>GJBj>!0+m{~Tk|@lT_-Lq2jcEHK@4tBK7U}_RTyWMP*k*BjI^$8Y-xFq zBX)8*I$8myjLawVFimCW0h8d2wCn<Xid@g?1LvG*`AN?Xb_D!b3IJ~af)~OD+Rl#D zbh%3IFfpS#p?;Q(TpOwS@{Ty#S{jB_H=BVk$j8_EDBvr(2i-;kcW*F??e5;rHDQ^` zWc<rl?%w_z80aCsLC!PRw?k?@KA1$Xqx~@X)2DW_5^uV!u~s@}rQxgNr>gYz`slC< z&=A`n6DJds(*;CCPXBE(mml~slyTY<Z8Wl}@i2;!y6)8AKE?i_5$ad<(rkbO7;rA5 zDD%^j(;JD!{L3g`k`65WdPRIArJYIE8WuM<KCdRok;1KDT2*j?oDBe6_#Zc1bKP8; zUX<L@{MHCeD-ln)GrTZbY~f&G>jA)2^v_!|gNfw=&Nz6uv^KS~z6F!p`~C+rXp-ed zEF{rUs;wUZ*TmHMTW53;O~<k@DbhRVTv|#Rp+{yrVxP5NX=prqMDgf4S;Ou*ZS>*m z{6UYcGM1Un=xQ3C)IoD#7HuuAsIWx!nE9~tZHh_@n9w;`RpH-tJ(xZJz=9Z@as=zN zIDqy$0y9`p+QQq?_=6`eEz{b`RiNtOzJriM8%DM8Ep%2y7~Qxws3tH9bKG=ewNRaM zk0s;T$VZDRn6&O=m)|@<)AtI|U>g0C+&Dnm+S<_fJUX&vS$nkPqMR%&ELsEk$2xPf z+U8#?Jw&O&@^;ZS@IHY{g>BL=ukNmwy|dJ<oRdTrGDoJy$NNL%<$!TL)$lRR_a3O4 z0hB<kh`-I*qgjNYP{xj@hB61k8{|?;6@BITQSD49?5#lPsbG`6NdMj$c%TWo25i62 z`_0zcLU7Z5DZFDLdH8Tcx?TY21e#17f7Suxea#@}my!}M5!d?prU~r@Rzy8K|C`&U zk-rob2^m(*(yr*HDaHSEz5Xo2;F%lIDZybONIqmxGn0{&TX4t33GVv>v<7{~RawHq z!lbOvo*2cuSiChfC3&l^j(-ZnZ`QNpgR2$uhU#lHYX2alb#81?WjHi2s}PFLN>s>G z05fjqXJ;Rs)R!f>!&=e_G`v1P#hpute<c4>0dIPz&$9NjR_++w+bED1>a=r4iH*Ep z`t92{TSVCG{tfnWGzoe0q{Zdq`QZkEB%t>J&yJ1h(DFht-@bhLQsu*0Fie6U-fvwa z<`=@fb+9EB*C3{nv>_g)0XGL)erp2*INx#QgrHuS{CWG*FD|c-Uj@lWs)Tg$t7ssV zizm5tr_BIj_vn*%j8tA`VR13pyoo6<lFc;+!Sfvbge5o5+(Ffk6eSjl;>SBwl-g%< z@ve^A$Ar}<RYwOQ!1!czNmsoAnVb^jJ3+X|-=R2!10*Nc!`ytg#Gh{EgvHWU1%RR% z{dIA@QsqxR_nT1#%ge20=q*4`F*^I8ruM!fBA`{5`4en{M=(Bq{;HI<Tx_k`->l?7 z9MCXq4foCq)CR-PF#E$bgF}=~!(lj4k#`MM#REZ0Ba^RyljN5y_R3P+QLiYDH<Nwl zpF_SLC|BwytU-NNX=Sw&USR$;$_E9d3MfHYZeBiC;tvy)MR#|1zpVp}>5t6&{RqAe z2$=F+pJzn~O|A0)u$1Q8x76V&cF(^R7ss%2a151}7Fi~L8cZk0k>8h(RoGsa`mQ;E z+2i$V>%95*uW5HYk;Vj54BfJagj><Nq2=eL`}_DgEzP0F&&%IEYvvK5u5=7;_X04$ z?cdGKqIQIox{8)^E*D9;$gpdX&v}LVk9}dtGS>x$@=c(8(8GiTc>tL;g`MGlUR~Q5 z8A#Y}yT0it@NlUidJTal4fRKO0H9qt%ik7{6a@5bF2XHG>4vNfA96A{31&00vPRNX z8^5$P<gJMbgzUt)7t+Y8-qx6$$^F3!Y0)YAt;;3Ef9rqe-w~I;Zbq8TV{70KU_ze~ zCu<!(zHo_L*lI-T09*22%_vB*^1wgq7G3TAI#fXIvrC}gCp~2zA!7t3FG*edPSWD| z(3vSIh&?z=r|vA~q)BO)?OC%rRKG-IU){ie^$Su5X4V2m_wd<R41YU29i3|m?m1^k zS95bs6wk=S=;+}$92(ap1Wy&Aj>%%Z3LCPuY*cCdk?p1u`rGX<36Ou*)YS0S7Z4L% z*!fGm)b<!2vIG0kj0aXZ{?vMqFdI;<MPZu|_b}M^^N}g$egH!{7ARz_CAYTJ)@v{^ z7Tv=7!yQRkt2>ZpIHMw?eX6swxnmIBfz@B!&J)w`chQqR&92rrF=rBY=uOW041&V^ z9GBspYYoUAJ`Y}8tCN2BfK}<6tOa0P3vGSBiOgISr~Pf-6jj0dL+}mVKuMWh;okn% zYm4fu_dp(On)W%`huMei(8y#o<1BlSdA6Ag3f4`>-DzZ)2;q);;`YmY1_7WVtTQ-) zj9&TGn>W|e`aVaS&R%Y=E--4WD(d?hDtTW+cj!S6MhC8+2dwg@?>JF}4_3VQAM<e+ zsQdoi8`!n9;CJlG1B)KB({VwQw*WRHy6@~Y43yTMvom~hM@PrsK5zZP8WKk<9O`tl zZ%B_JZ&*c>s-u!$$$0@>0^B7xCSEl+Q}qdrtf5l=WcIpH!lIX;FE$HERI}*MqkyjJ z0w0hKbM(6Xu$PtCe&)ZdJ0%TTO=YjG3)!d*%N*T4!LSQnFq`?vUY1=^WMkppWyq%! zT``lP*)eL{^(+uW3Yc!=7L|nqk5~Xi%euGr1HF>v$4}d-shG$|BS7VW0836KW8_io z%`-IpBn4CULs~Fng5}$%xFXpO19r7Rm9WN^*6Xgw+H$D7H`4hH7R`Y2G}mg*-CYmh zfTJ^4oWgoM)1yWbR>11PUJSLJ&?1)RIqB=`Sl-6@#U{git@1^_UW%;rr(3(bYO~a( zkfVJz3KJ<hQNcEhjswNdCvDgf;fn^)U+w3w6)cx__I5bG6@IUj{5rpa9h8X~%wXN~ z(*^&A-LRt12N;Ci++5rsg+&c{O1TpiSn)brk04TC^agWxX$hZzrXPe^8BJCiDl4Gy zmFA;-Y;5hL#W~}Z{%_kdfKJilXUH%Y*}9RqE0qll6D#xlLc)*$TvF_yRUp9ryo^#g z5dut=2FZzo<@%sG2kYE+eQ7?x)%^ObZ`r5`MWO{Q*_e^D4-vq_RqLI<YU1b5wG1}e zwdZE5Ir8vl;~xt_KTtt3b6~@y(H#as3MPVIIf+P>8$(;?x<*{7IdjhuS=`~#Uj&v6 z3G@;enXHzBWH``d@K=TMUYx`w@0oQ5X%xU!Z*G@bImaj>4Z5X0P>@geFAhE_#*Dzf z^cml30X3_tzMjd_9Dj9VV`F?`N^rQ>#Ev(WgKUXL)<&p_lm{aOboM}|TU~8ycT^$M zChw&`#FQkZ<UA$^<C3IIG$|engjpP6nB<_OBFOE1C$TB3@58~e?E#;@=o&HX)Q33S z{*R!XNWVg8mVaUf84EIaJRu18rg2Rht^k(mgWvdP^I$HR8W_M5RPpiS$7n#++2u-e zTe`D7zCsouV1;poP(coMl$jf2L&zGNyh@Xk(>*be%?)(Cyc7ip29CAk)OB<;e@&0i z3-EKKWF@e12A+64p@0QiRpR=eUk7HJd;2d%0gJT*%MZ4;GBmX}*FJ=-Q=6wfxg}x3 zv24JW$v{`__)jtiJ#K7#{0V(luj#n^B5X*(6dI)UxM~f7hzbA2M)Z-tj4nG@W%PF0 z%3f6hlL3XRb;?{mavF|Bjfm^B#fYK(`H9KJ3h(#1H@|=Xj@8W;9@{!!j0D|&f9K%* z++-L4KIQ>bOPp6&pcLrm%{teM#XRD$Lc%GwcBjY3PemN?HzdEebSv43VZN@IqQwED zb~rCDE&U3?e(+$LUIq22Fh!lU&0m<P28V2|1z*5Gm+SFYl;GeHt=q6Y(U#H)isZI1 zn!;|xJmT^f#%n>92;(FWj^|fVp|oR2|3wqnSNTMhQCf8%>RQ*a7U)L$fGrKIt2G*f z%D4n*X2+zNsCUjUhIvy5V}1x&9t@K832;}=X4Dmq&(h9N59b1@swb9=Tk6Z#ni4%B zA$}kB{T+tTMs*w2M#>y&yepJ!47ym~?B5npAz|TRjgP;K6pQ5p0r1}E^BP;TqrM7r zy`j^1z!N~>_((Ur`g%v98|(Z@bJuv&vhqEM9Ya0d->q8`s5kh%5N_0)ae=Z;!nWw8 zQ~Bu%SW_C7kZ0i5!lor>%LdH-79ST6mnHRAGC!ZFnmCzLY&2lie?K(4L0I95Q9 z>k<4_?EdiVIB4cWn{MtzR%mniA3KjSy=2}vS63Iv+`cn(=?E>;zoqb!+G_c*ce;wQ zWS_6<=lUnMw|fdrbg9@sw0{Y33!OQYfmyd_q+9m=qntWE$D$W@^SaTLmAa2pM2?)n z@ZF7TbrBUc<i4U#DS@a*r>C89JbQ9H>33TiUgTcO%bKp440>06As9ZFL7m8>cPNB4 z{X*gGZY7fvn5#ygdc{Bo0eljIDJg8Zvrg9MIrrTbSo-f}WavRd47pulr<K{~j?X{$ z)t;ZnpUREx0Xv0<=Q{Qa@=xruyrRqo0(0O00kb}NqMp5P#hjmS@dRr0!?*_K?&E*L z;4~*wLtkB^X;eFt+4NE_q{E~&QBzY?Md_=Xxv6Q<`~ORNWe`Z7p*J-3(J(TOOgqP> zrR}#C5J7W4FppdkfRtL^-&`{nFH~rU^vj`zclgIu9N~Wv^cL@b&*)0w%}A0tH!!)E z#y;r;Y#OvKcAt1BR-?rJVm=3QNE34bQ^EN7_=oTaC4q(J80X+AT7Us&*YsKQTy$l| zi^9yB7E8+0P$qA5v9p+s20*{TMxFXwviD>5KW;6C=7%J?>!CuayHe+t1N#xoAUO2$ zpwALZ<mv+I04v8Ay9C$N*3|v<tt2hix6c^0=07_<&-l0qqViR}3hFS4UqG2_E^I1* z?L;XLy`+<6;UJuRs3qL;ng}zvY<&VLh5*!LB+tlci2%pY0<Nl>8&_VNbKT*Y?bc}B z^P>K>^BWKj!}D-){B~MQF(%EzX@An<jJ!;q@rel?Aorj|zz8zape2*rt^Q@xls9Km z5GSR9t@`so))BIfsd#RtES$<OF*o~iW$<NS$u#D`Sv{xg827VQ=G^^O@MJqszHSQs z7SSMK1~I7Ui<QWP4BUz(b6is0*QdYR*PSPzZv=&zt6w`FQ;)_IzZs(qSdiN{aeKG9 zxqE|}ZsYv<{W5>|JTG&^na{<C78q~u-u6CC;4D}JKx9HH8}Xy1#&5#k#-iN;ld)yU zfK%S0g?L44w*THM1*?nrm|dVN5efLA*X+4m@$K984<KEh_5R*GJy@9_j&bkC`%>Bl zYRWT@xNq0)`8aEy5B#k2UNBzikp}!kz_BA8wpGo<&$zX$!JX3Nk21}X+t<z)=N1=@ zqJ6S(%4d#RK2cb{vg7ih$`m)%At;kUg^LL^os<D#9G{!}hy5@~)=6a=bM+J-2faif zWYg*Shn?-=&>14NnBBgjEzZO3FnLjPPFy@ZA1*U01RafVis8QfJ|h9ilaP{z=?@cO zeuSlz)V?uw{F(o}zoxw0zJJ|buPzya=@+<}Vh~l<Nah>&b`@ApY64A}LQ89V0RVnM zf$D_aEB!lh`>`>=sX`*XP$u0m)rTAwW-hkUpUNo3s?O@WzK@$*7&{S&pLy!p7M#DH zIMQv6RV2R;jCC;{XI;57#&bY6;G?aIy60S3{Z&%q1JDvxI-iCK9m%Co0Os5=|3aJ< zpAM7^Vu*p=+Iwwxv>;bzG*WD5K~7FX;e&zNcR-K2Pu7HfV;^=kdvJb5lLasgQ~^x? zfn*#0Wj2=S6Deuw_lp-@s~F#1LpG<R0l?2LDuq}rGI4V923R^n-2q_$s!LT%ZYL3M z%zq2xiS6g+`1@TPUefgqpwY}^8{xnVKR02|Rxi?%Xqv;;%F0T&?p9T_V}PUJQc_}r z{&_j+=s+5eRa>=k>XAwvEG4XH%K&zM7V@<C+w^LyGOq{k8>5O@Y=8v9Z4r?JivuRK z&G+j;B%N#Lq#qfals3XKNB>~YbMc8Xgr&&;#nJw$TWIa<EKv_F`Ls&+c4*&ekEQ1K z^62b2Lb$vcv4bK!dfH~9KWrVhrYTE(e?&<m{@ajmq2sur_u^O|muwF~uBxtc)BW{( zHeZU8Z!k-aoZ=Cii`T8e+5~OPck$Z}f;q>OSoC(@37n&IOdy8?{91T^%p;U$%=@sC z{oB%WfW8S?epjfG^TH2^5H(9nv;cO}uURK(&n^R%tXw*s=J0?fu9CRnH{tH*@;YTD zvuCGhQuv*g&CS~Me$cjkfMR88e)mTkFE;v*vNWGy-FQy4Z*Q?QjhHx`CSszN&`AT6 z;gz=bxAv0;=kj+9JXwE^FYZh#-{tmzIPZ<K^G`To`ymLrzquL+fZ|q#5*V1DY^iN* z?VYn=UGa#g;dOzaxW1MaKlQa@V~&}<#dIE!argz>d@N5GxN%TBr(9fJZ94gL<fDmx z6HoB-+dPdrcWV}2buj+0ZJW%``HomEDj?toBT9C8;m{a>4P?fRR?xP8|Ip!i&uP<* z`CHZWK#`GA&|$?8CBC~`jV4CSs%mPD;StW>t=;WKv)Wm_nI(my$?}tN!A=o-jnV|n z%`xT(<x`DLAeJ@XjC~s75^_??1x6x?r)e$enjxY9P+sOw@u)7Hxp!ayytsiM?)ePh zJC;7_8`Lz^Xf$jA-5W&yuR)%HJg=`t*1lFT)ChNm7$qhabktN=yE2e3Qi6k`4X;p- zcf)T@#t=NB;R{DqnU;>zDPSv;w<m{1h3Cr7Ge=iknOjwiG~6bgHyH^)e>=0YB0r*{ zlxexhl?`AQH*K{d3ps*NwL-p)<<y-9tH;Y+begP?min?#0)UEX4v+_W3^nTimskM2 ztBMedr66C`UrSgx54kULcjw3>YS0d9=Tz@IH#Z0A^{OB8TaJ4@IuJ$j^b8kM^p^XL z)Nwnq&!gVNH!lE64m)Yu`(fiM2x#~Yd-KRgvuN@LX>b1nYB&TzbkU<ZV6+Gvks@gx zE^d<HBOe0G3Oh4Pd(z>bc~X>Ry5o%4c+G!sDu0|FGT4LIn6CU?7c}f6`za^?0fZYJ z6D2d-B7ao^Ec-_<nxB6D)&s1K=J)m<rP-#<6>X(t{e&tp3@e!8^VL;<z21%-3flBn zrPKNG5CqQWC7-GM4Rvq6!zTCzx8%{W<JZ~4|55-WLGC;OHcd%~2b$7x4L-#gT@6GT zww!78FNlT5P;fh5nec}^7YnFeikf-mk0=oTcyuE+2*S0x0f^Pq*l71rs}WbQ?uGeU zQjduKxm}bB{OtSp@5&zOB`<RICLFn6QSd$vm612s)Oh{+wXCaaEy>=h*pk~3Tr-D# z#$P3pJ^4W15A4^5@u$ApIzCR5Wv2sJ)Oy7gjI6D$;%KOAj-jg*{VePut548W9SA#j zd{r{B5bT;4{f|&qe^F>aC{)F&xgAJCJZaJ=ikTCj^V+HwXn1MI=R+0kZW;OzE?y{@ zT>8z<B02ijY^-DJ+?%U){^RDc`<SPT+@}sX_i3h)oST~fH3Pc0Li`+-a(=WQEUeQ@ zN{cGUhbBWix9+}36^Uc{pQVph98tX_-EnHJ6s6|bpan?q*nkCb;l5}#M~_!Gv@#Yi zVt=j^+}?pRcU!7d>@E=7ivrT(+s%$_qxWvAcHq(>{ht+XgU+#Uj*XlfMRSH#i~?R` zz){kU(8l2R6vwH1Zg0N}83+vxji^SB<;wokJA37%MKf8T4Uu0m{8M)$wt0p@8{Fvw z|8>}#FVtW!1nim55kS=s!atF3yIq9_PnV6)@iNg`m`_U^O{<Qv>ZS^0Ucnf?&`!)v z!DlivTMyci;ES|4daL#xrP8r&*fvHibG{Td?-x9GcD3~l3}XiNd?t7BMJX|5_az(Q z!30g7*4`_1pD4C0H_>QfJVjrab5$nrp5OWV^SV!I<I=O^P45o(#O_Zh9HaxI@(AnO z5BU87XjHXCOdKqmwys}z9lnn3`9x~641{^k*j3DM`e{uCoWbuM>_4{j%gM<BMs#-1 z1tIKm#@zSx$@$Jt42_J8C_E((fNBC;@sF@A=}+hBYTrNV>l-Wn_D$zKc;T~!rHu>u z%XR5QUgi~`fF~Kc_D23uMx9K2$0<N|mI!7{<FK`~wE156of@Fryarkl4?;ue-tmQa z_5_s*KoW)c>)+&U;zB<PL;b&8yyX&w1pv$jiZjT&4fZ8%3`Xn_mWpM_Ii+D4;A)D+ zyrPj;7j*#XliGMiRpG!KpyEOc%j_;*0b?#^i(|r~;qDlf@{ksg-|NY0cTBtwnoZnI zPMP#mtsYw))Nd^8-qAm}>+Juz?@T*3Hn$@=gPW`T*4fpCdsL<jACH*8BMW$W%*$UB zAEB*~+AFH$3Rio~`&o<m@2u4XVr65qGg65H{N7?7L!_w@j5qdKC;>pcb2>V*V`q%7 z$<m=}oD?1+hBYG7ztTwN$90w_C#H&{m<Mi;OtV5CW$1xJDCVvNGdcnY)8adr<6H~* z8wsEHQ#44K%9eRhc~;Q|)!zgFZ*DyEJa3abxK%sNvExrI5z!h+a)Vl}&DZW(r!7no z%x48!O#Ax*WQ|++3)AD`(KK=VtxVI}Srxf*FwLciupnp5*$V#>T`%?Z2YH=-2S|?U zCux0sfDQ&HL%_ctq8LckF)$Gt`nRrM2B-UCz+Elg)IZwu#;90Q({!furP9=y@`)}j z7!86T9$}h`17XmX=<`!)vpT~&$UO-r_3y~PkNl;gs3_pf;<=?mSof_Q5Hoc~so!AH zXeBm#Cjcv5_<65%q6RS4Kvb2Lc#PTR0eBXxc;U&1--L|Ys}GX%){Y0aw<)QRFED_L z&&^zQCX=A;g&y$xAj9?`0n=HKzJB2&9jv@_&0}`!xcP_Ya<i8%8<6Q~qBLN@TIr(h z=XyPkWkx4;wMGG$u;n;LRq3mpJU%UL$oS8nKZ$#Jb=G_EUKxw9l}@3nsp~C>L6+8H z<WF~L10*~#j`x694z7Y04Mi7>s|<p@+{vL~L?})tbNIk+djkOR#N4~f0=O<3I@iP( zfh|7DWeV!pnsubJ{KL$jZ$>p_Ggd;+o6I#zFSG~G2@t#s2jzS4sU@K72zaZ7xWkaI z>Z}C3k=MgW3=#-AF^)Z37ofZZHsrJwBD(Mh;BAU7tkl6oxC`ubcG`~Ax3OV#3JMKe zJOckY;3i!Dk|`aN!l9?cW@2t0!(brzWkEDVABJro%_&*m5)Nu6+q2a}5qqy{0DTWo z?BUepl)_3pjJKkmM|KlixU~Sh`*^met9SDqEviDq2<>-@M^xrWnNg|bu=PIDsG7Y; z8IX;&6#39Ncy*0id0#DpJNg*F;%nFsT6}I_e0*_R3ydbTHEzzK;wjMy{Mauqt{M6v zEVGksfy=#6de+t$zG+moS$hl9>TR6vtcv45344w?Hf@o@UIzcSN_1g_AUcee9=k8s zj8_X!(tB%OB$!Eu5D{JsjG|l#)BF%rUJoGmz-bw@S5#E&Zr=IKrjBdRw9x}KXUI`G zpP??To)c{S_U&rHHb!;)*VYRh`E`8vTYpUgk}UinhFzET&LtmUvP=3gn)yy!<2<^t z(Gv~XSK3tF#S+*nzE*c}L7L?kfq=a{&6tbpzcS^O1;s*{4<#3Gk3#8gIWb8D9ynmW zP*SR*GjsfA_2I#<MWwXVRF;VFh?pLqJK{Z4jQ%Zvw=b$wTfY({f36p4t;ndee)06K zmGAlUn*v~AQYXMCP@$YRZH1Tms@@MNv=q!q>!2HUu@F(dyf5C5K%MyS7h^mW$aGB& z4Qs^u5PLxuv1iLca9oUP`i^{vVR_-VZ!fYZH+|7rTzb6>&*7VT;y58^G&g*?X6>CR zn(cAvfnFs1P8mYSK;D-grcp&9$tmc|xQzaCg1&DD4p`Z_fMqt=j6QZk@Pu>iYXx(A zT(E)`>~1;^VTT8t0Q0$(YU(|ssC8$qpmOO8w<Y~FAVb7_l1<Tl5A>%(3UZ+Dx3B=h zmJa8f8DkO6yvao*$wP+z3x2L_*3bgAJYOTZr%gm7^Ok^%9CTEb25(wCV`n!h{=l;> zM2v#T9X7FE|G<>>L)U{gFFdn1*pnfDr>Of5!c+}a!dw%kvt%J#7IqGhQy-EgmAmEj z$q0Bl?AaS<)dFGBA??x|3#MM(**sdr#CX`(KRjH&r<Rtg^8(=Jwo+k+%}J3C-%7lH zbCTvtqr^S?@Wolw(WQ)-IQF7Z#Z1}W?)H_K%s|m*i?4RcsI7cCkDCi@uss^rr%IjL zI#m2&S?c7Y4<)5!HX7qRJfwv{HUa-?ow|z792zNNd-{|{md*`7Ceqi}_`}r*K=yU5 zn}RWwa(t-fq)q{EI(oTDcZnIgHJ18gUK|0W$!iDBT?KpBVX*;&?lP`J3@PF9jLihK zi@i5o#Rlc#f3cAG`{^K_9#pwR_Z=Wms;GGC{)iLRfAz{RdZtY9n1CdefMoYY!n!8# zHjSqD$j4i%mOq+4{!0&e8h(Uj8s+1?Aw4@k^TGS^a(wH(y)eeNVk>ciDrM~I>go^G zo_~J#ssV4`Di;W5&VFPC3y8l?PDuC}AcC#I9%{X<P9-J5=}7rQew;VBQFMB8(bpBA z<9e2C_m&oZXMf7U@u+uX1Y)J6Gynre`o{p~i6^Q$LT}I%XZC|5a>ng@R0Xu5<&F#> zw3NOo;y1`jO&qMNabq<zsmcICD}svH^b>0@H~A3FMS7FR+7HIr%lGAf5|QpPhQAG# z38o+8uj^Fc<sM=&(J-2>((~xyx}Sqn*zDw|bjIcMgR~W4zRlIZTa?3E4J%1z5kr1Z z^RA$_hp}K$VNbsFzKCYWcPgZO{R3?EyfJS+c62O{bZv}oor?#xgG0)Aax~j0onfbx zz-lHBx0omnO<UBUpp#vTQHT9Ge_c%t6+jgHDwwJ&!wDb3bQD}U2)wwa<TMb~-iuB; zY`*LHgMDLZ;eJX5w6xbts^8ToCKwe0m0v9Emf<`w;klgix%*-PfBu~BW+Ztp?VkJl zPb5D<2C00(e_(Km`dFZ^ci9>8)XcZuP}1Aj@xvA4)0%TPOm`T@=o{Tk=JX9Dfa4hf z>>*!C8qs@ArF=6DrTVCQ`t3)(-Rm?YZX_<ogj)f#G`wE_WSv<9WSGM*i%Q)YFjw^! z%;Fuk_S~#@hrP_Wl>jq{j=UAGDvEr;c;t10cJV>o^zIAM&=Zxq4JW`|3pOtot5X{! z%RL^pL8zPc%(?V~b~3n&U(vkLRa=pve6(anO~Fb*m36}!6vukhjY(DeG8OxNi2O@- zQ6vNbm><WRPZXm{!{iJ4@GvgD_{Nc;A+enA%{k<dA<UaLz|Gyz0+!7Qm=;fMKjDf6 zU@RpMf3UKn-P+z)gbmqFVryA{M(L0kFXv`s!7OO&e5RMa`<9Nx*O(iE>FY)E;!F!@ zxNRy9>PRT5(iYLPIb8Y7;q6OH++1^;gwL1<k8zouikvm{!Q$LPUq-iF0Ngf<nkwyr z%IhrHwOa$1DIKS0mO&Wn`mjlpelrUoapHHL3t(Qf0hJQ+<;xfQF978#3fTso{#`W9 zE!mz*odrEAm4H#g|8r6aU@6>d_k62F;dxkAhl(|qnPd?qwPZ}Y7>}@Pj+}WtH}F3u z$gZ=xPnoNr;yKj;WU>eNOLN7=R>`-C$o=N%Yjhu0$Qjdj&$!+bZ>U8BYv8iZOU_3; zAH6(=*X<vLlnLxz;)Db?HeTUhBGbQ?MD=%q0$U(2qeQ&Ujn}V#&CjcGGbWtz@d*S2 zvu0aJt?MJt8`vL;hd#07v+YRJEy&`zZ>?<oMlKU4)-eI9NCnnU>)*PwQQH8&n{pNA zJRVl~kR+1peLPdcf3;>Q>`1S~g*b##;uph#N=nvg>mWpmeYm@9ipQo}`syLjnYxjZ zC6SN*@#6G6fL4p>Zah)n_VT$i{GqkSepuD+zqm3n_K8BEC~8PxF;Y4tBnseEzRu`y zrz%T#mr>nuO?KgYS3ojosXw&+X)WSbc5z=`^*8Oc??85A{(6-@<kh$w4{O;}9*+mA z+16ERL>RKbvK^)G(jEY^rnzgtgF$ef0g-^b&Bq~;1ef?}B?7g(C)EJPQm3Jz5nWqb zld!(A!3+8H`!#aO@GqR@b0z&&O=DZz{qX<8)m1Q5)pYAa2}rkeNhs3N-6$oBbaxy| z=`QIM0SQ4sMd|KCBO%?L(%lVrfcJj){q7G?&fa@w&6=myAD&zBK;?aBPA><+&wf6k zWH+D{gR<=GYg(pi7GNZT3-eO~0C(C{-<U&Wqj42R&m7JnX6o@dLJdxzn&5CG-@{$1 z$%JVl6k!YJYFkm;#nmwb;bc$v{`m~Ng|&j(cZ)be#mM@x`!GmSQgS!S=?U`#IMsTa zf~YV34a+vr7Y+_iUTD6OQ-pOW8G!qQFX?8J>}gnjI<(-rv$eJrhK`AOg)OwAwR_Wr zylF^FQpq5u+z@~uIgtO#*aq3-4;RpL%<8W^YN$Vyz#nRE!#~DAXHzaUSmNO49x*Eu zU|m>BkU6ap!ro?DzT^2f?hnvnMA1lawL@coeTzZ*01RGB0Fy=q(y+zd)m5dhr_Jqu zR9)LxA4Cjjob|dawPg-3kD-zh^1cr7;N3G)u9suTDykyK{%p2T!_0Hxjl+o5e-u2s zEj6cY|Hw7ghy45-1rV-aF6{8EPV2!@zUSe8pp-6ANNO#9s61*e5G8B+t)5#0N}0g) zDHQ;wrTx$?#)6;vLR;T@Vtr5&he*S37M8-=dh)kPjecm~HN`r2O|g7Dgty7Oc<$^l zj3h&lj6Y?2MTcT`%MLz)s(WWXUD(aT@U`zVwjeQHd;-lc*U7c-{`SwmtDgdtA+u@n z)8{!{)tvKl2{RZaF;pJgTBmMuG{{SPIj7Yx0px#>(P*9;Ic^F+7AV&2-%M+hGrfoN z;kDz_>+25@xSyHJzpS2xO&QpFiA<d0F{Xihlit)gCMC8cX4^K$vDC*<hhLxQ%2Um) zt=|A|AhOHZd=bFNQa(Q+y3+~VyO3k)A?u_hZ(ogOB*2H#Nw!+OPQ3I6mQk9T!K*Ec zjxJ@;f4XdwHw(c$H`UcAY&}fyMcF)))ba(J#3)*JMDR-#kcy?-+P;IE(M#_6u%!@; z3!nqbRVz)hw&uj3tMfU<<7xZNuw$}v)&}Lo_#jH|ws*xjpuz0XpfyKZ2guQz$7<P< z7nH}<4MZqK{t*;NJ8VgLD`NPU(aa3eBqejzDGopT*R5TCNLv4H4L+>L;as|hN$(fW z0HUb9{oZH!{L0AK<<J&{Q#VkP*&n4EP1bPG-F$8}t<M6S&j5e~ce*<XD2LY3kAMHB ze_>+smPfb%fI9lk4O;JB&yqwh?Vry0?-O_u3cJbb1?XfiEB;kblq+ve7iotV`Ga*a zIX?VFl@$LwWEdPv3rj%vgB%oC@Ij&1&qCV&%vU?u_al(~I*D4cB~~n6D*+~+=RtGK zmuvmcFc#UdYYkHWo$;vp0w5YP>%ZO(5UI|FWgcjefBE9|;pOz?)Kqc&DAKrMg|k-? zb6C2f?cY@R%6nw`UgGxx53;nhqyv^wZ_0BuV=Jr4VTJB)nWn}`pzL~Vngf&j-p{?X zyj(mCC+}N=Y*Qj{{8A1G<B$nn4wuyKui!YgJ4MTNyskNpjC=>2)o-I%3!h)wI#=rk zN{qI66~FC=mI>sC7Gz110UscQTkBmtqL0Ep6VP8mNgk1@00})=-Y}_`v2!&mPtr3i zcQt!U3o86^guPud$rk^YWy`F<4jjf0GpNh1PGT0oKdAnS(mTUmYgN#q&-igy5cBRp z{^wylkosO(l?YTZROGb)dz8rUF4Py|_Vz`nLZ97TmwGcDRIiage>Z0J0aUeaa06QW z(h7;kaOrLsbC?Y<dB69ldQ9WiQtyM|E+nlEA$?;0GdDL$Y`hi;e(eyz*aFyddS<YL z;ln;4wafwEOLy$L*_``^Q9xia5EE_EC}33i7YS+FDclafU=nlrio5%T1H0Cp@3NBn z4j-XcfXK_4hVI&Vm~dge0otf~5Crfz9T-F&2z}iEzj3kU@6a&YVya|3w6v^33rI8< z!(0sxAo8Y6q}Q{>B{xQo`ia9cuwoOzuKdugn#xHXuvuKZ?>A6f+*n&%8>ObERw9Hl z_OaOvs07uP%dNe?lnGJm1@2%;FIVgU_@{TCgvi0kiF|l?nsZ=ervEt~myWKw$`Csn zuLEjtyv&yw7gi{4>c_i#*u5H=!vY>6RHh9;@FxYVROeLKf}%<d4mkzGN>2f<a{)X> zA8yxz_;y`r=)>}$Ch1Ihp!c`*&`n{%O1|1r2PH@%V`N>Dk{+RG*LAG#o%)_WEh4nJ zu<tPZFbD8NK(lV=2+%E2u*)WZBife=yKF!-Uj-IyrEOHKUe77ae&!k{?E#zTgHiYi z%}Z<FRh#7d+X6krCj~evoD=i7`-5~vg-YHRRGjbHy!O>~gtcuD!2;05oD*K>t%W7p zJ_5@k=<mZNYO^2BsIxu=<(IFx-QqL_)#NT{P{3kVeFr^KjxF<ZlvPhoE*~Qkt`gsS z0T6g(kl}!f;_p6|h+oU0r^xLFNSX2^9OZ;IX9H_%9qZ>rOg?v{<()^zgab+Rc&^W` zZ`&bnL8ZaEx5>t|F(n!u2;&#PA#`C#j^lMQk*i&lp4|TT?_Xo!h=;vD(>(zI`ld=L zvk2<Y1{QySX>l7~@9?C7bgUT`CDiz9Wp;M86L6a8J3s+E&nsfgmRQf47^53z_92f< zAD@dqI(YL8NM^l+gxNJJqo3b7kG0e8-_a9D@sA&L^oQIx%`X!EyfWUF8ly`&ddVvY z0FA#&3cVi0Ftj3}1aMWoO8<No__Zc0t83ftio${wi7%n8rUE$a9$j?AOH<+RwT>#& zJn)<MWU1g8PUbV=_{{jTqr02jU;jwrnYU)%@D(>%Vtw}_njn07dA9Bi4Zw%$!0vg| zzl>LhdnbfBzeB|m<$)Ml(Kf}mq&hy9CE;}I51SG4G4{tG!NYtJ8|UiQRJ!_t?`21e z?2{N`78YyI_nq4xSAddh)!EreVk9y?z@`PYjbcrf99lnA0=!g};g5j7ei6m^!0a}X z%~kE2zikr)AgPhH<4g7L{kj{#;XXf%69p*OxF5^QM)t4(WZpYNo#*|*n+*|-^1@y} z;ZUsBKKGv&Vw+q~u6<0(C(%s118jl<E{=sMP>E0H=MT6K>kJCVHYp{0*P{JxfU(p4 zV1Iv2i2LiSY$?jp4>PL>D371N`$#iJmY)k++9m)X$IsN&%?6WW_^^6Vz;2x9aiIYQ zQg_GxfBrgz|2G4@_sRisYpUk&_=yzC61vC8<8KCWMb`g%*Yc|?a!*#}*IhfaPky5n z5vhktKNa}fA|9vt9%w5^RW=#4$Tx>4Mi>4t;2b>|+_K&x?*s(9ln<mG^jQLGxeyRc zlIr)$jFty=#pd6Cxg28tsY|xkCrM|bgBfSfj!d$l3dLz?P>!Owl;58MbJ<Ck{F#}t z*9Gaj5BvN8-AvU6_Olm2ClY4AwgZ9;L_jqH*+_62K>C@%Gsd9>9R*08>42DMGA>wB zbi39W71C|Cly9=1`{8BWb)~}h^-v3<!lbfpi`X2alTtb>Ne>x|)GRd8y21U3czB(K zqb^1HB6M~JgdHMn{51rgBxR2Y-&d#huzm-8GIQnIM;-?Tc<*4=OvzFKodgUFEVFg+ zS05}b5p1oZgrs5v@A6K32f?Of6UgZST-HBp4+{6nkWE7vFSILc!aWrgQXtacmaH{D z_wkbf*kbL_vMU4_y0Tn{-+a}D0e7YT7nRcVJi)5ZECFqBAcAD6Z#Niywx6RM=`XlW zU((ij;r4?BNfS*Pg~~*9>NCb?&px@-dMOu7KLU;hmfr3^Rw0Q6_b2bYdb)!UQe`K) z<4-%=@!~?k7^oQbW*!QV6w9;le=i=Nr7aBOM`@)*7kZFvtn~QLI_0BFHpz~G4Vq)y z%#M8_54DP2JoxCrnSNI-kE_RqY-DF;jm9>SK*kVodFfHpyyoSnCL@R$6@LBD6qN_g z@V}qk4l(6a3ujEKo40Le1~`PEm5HXz3`7wCSdl+0m+I~Yc4#QtOa;qldJIRqXm4rK zG0KE(aJR&hS*Qu%Os~K-=5WpL(Z?+dh{ahtdxIi``|61_xN~SRb6?2S@-NtHa=gdw z>LkilyUV5+y%#WlL-X<T6VK@|tl{LS*RgYcMi1GI7DOy*@Z+L>@e?qSvwpk+##yj9 z;I(mTT>gwS_^q*J0@Vs3Z0+1X-TUi`By5y-j;}L}=tan?o$K*!Mh~)yEHd2pN5GRY zXy`FV14wNd1Jy!(7{?te@y{yLfj~+C;idPUhEybFa?s_~%-z<K0#*Gz<I4B(<B(wf z@u1mMf)X9)Px0{tTGq#lRI;gh@*ozp9@x1q$H$aV*q?KA4QB{Tc2C<?3)X-(t^Q#N zMq<MYb(|{D3nYW5#_63PHNSaZh?jD5XR{)?NB?o)^&vtrOfCG(m)jcNREO+i{l9!V zB%Hv3hx4iIXS(sFjW>{9Ta7KTelB1IWm*Eblf+ScR=sd!cQB$@?aVwSnq@Z`0v^45 z`7)?gSPrD4oB`Hv@s`(Cki~iF=+?~Y>e!Omqwiq=3|+V-3U@1Q;64N(0vmz>N*+9+ zB=1^%2#9%M1)LabL%z%Hkp!p}XHd);Gzyo7B?L95(=mHLB+h;NHk?sWATp`{ydJ?- zga3q5=qqTi$0<Ql%D_Oq2_qvTTQC6xTUkj#QK2N=*e(8bm?jA4*#YlBnS&OVQ&?yY z5E|^;L;Bmjs+sgw)$A@P1B#H$vfcox{H&kOkslxR3p;W1Y1Y>eGVo3lnoOq?VhIvH zfdGVKy|yefU`GeGlf!u{&GQO$fS?`x#8NI8ytQ-xlJ~;z!cAqAk>%F}j^&-dpsa&z zl>Ltb1L>cw#czvS%!9iMV-o;rf1UkYBmf1w?C-NjsqO^zH#nO~grGI_YXS#@+BXPq zFqJ0;bi`d?25^MeKI4X-&xM7;hMoB8taMIMsVS<eswfGVAn@t!{+1C=0$t{2-2h9j zD3Hk<X70Ad$>=xctI5vRRJm9I1%?LW?kt%?fwPl*@uI^Xh+0h)&w*reME4i;xPom% zyO91dsy~I^96dWc6^Q!#+Gu>u13g6hu@QK_7t)a?O2&d=u{i4v+!Mk9JGiNS<!@tv zqLix$9%ABJ&|zpq`$Q%F3F71c4}L1%(L`YX;z8QL^fYZHV)Pwr`TSn^B7q3mC3LDL zV7TG|>#tsgzP_-s(gg_k)kW~Bd{h9}==hwQ8#w-9C!u6;E(k{_3ti!xYQRPdhf+E- zFu+l+!<npIJ~H<)V#6Hty9j$0)L()C%5O0~@lf>YN>cs{yLjc*=mu7jfpo#^*TH}Q zVJq;uRSyURo6Q|KWoZosXx(<_1NR2i2wcbni1c})c&^>ls1r8x$N1JZWlY^pq^hEF zOh7;|?&;gt26R$bw-$6RE$fW<n><PSf!_>S+W<+u95ycZdXdQ)5ZxmxVPu3lKtP8Q zUBLm=`6}90y$g=xvJ#Gt817~Y>Ts@SXHfAx#{R(lS`0WTJWjfAD1c^5yFK~=rt+bu zCqL7G-jV?S7I$5{Hw8$e`q=Q&DA&Mo(45YOQ?*92eNX+fuKb*Xe}s>pdpowPU9mzy zhbQq_`S+`%(PXqtgmfZsDX;I(6v?WoS&;xY=|$jvHz~Tb>mQj(=hlJ-ZARp)Smp1m zujfZy1vL0JnpZ1?-!6Wvb&MZYT3VB1d=CTgGwRakqY?6P`<Ly6CPdiUm#u?w2ky4b zy$iXBEAc;UXJRI1tfZ5VZflYHXb^G?@l=z{j$Ow!6KO%&YHzX+e({o<zjfSxH zK-R|5a8opaj!i2ruCnu)x@%Rtvq)1{P`k~FC~h+HL>|pBt1-^bP7liAji^eqtRK{! za_EI%_g@ga<kG5QFV}x5O8_q41q<MPVGlS*D9Q8d(|4ZSHcBz~idvgb5dCDBcJGTc zIUp*eqJS^QBLph0N1Ntj#}*y(z$<eJPSEK;OuZ_~+&L~ohGjPZ(5O}5W-@!v{~5de zW0$NyxYAkZIiUDnf%_c?$_M3Rp&E90qG3iRWpNVtIY$uoWHxrl`Wg2%BK<`CaV0iQ zz;49TGHEyg<=#8M{GSgOi0Ts>+9@&O#*opxG{eXH{E*_avaOwo3az;`6mwJqTmFQK zEjZPrHb%1U7)d-PXj*rc@<@8{;we+k(CG<v);P7g5Jt={`{6ZS9XAzOKLLnow(}Uz zlf1Mk=+WmV<t9h_5rCgg-0QQdW(SI=+MKJYxH|sr=^}ft(B+Yb3=NHVCB_OT06k1A z5c4TD$nUa+GY69$o|=GC4n^#dnOoJHZa26t!&S7iW#p(B1ZuSpmn_x~KX_A?PLLBh zJ37AE(R+l)*xa`8$B`~WXY*Fz=&P^@^vzPQ7WfFtmbe4<g}-YaWG%<X=YP(ly88iP zcg;Qc0YGO{7Y2TSoi6eoTS~CLm{#NO^l#rvUvua1zWg;X72$J!Zi;fbXMCpfcfsuE z1`#<+Z(B)m@v37KTPcv%n0X}TOO{5dZDEd{=u5~weG^6tR7yIv{F;g&M=IM`+KnD6 zndZ}RYaZ6TKh+H2_03%*|Ge;)66Z?G9ws%Hb%6_x(ztpVcg|2-tckYh&+hUHaTdRJ z)L+<pSA0sZxyc$AaZW7SB}pw3=m*@0;gh~RQCfj~!*;QWGV}eSCi90FUB>B&ilwSq zMupg2F=f6lrfhJ4NODuf6HKw$>8){htGL`X0bwERosB>^-hC_`oEl$2SX>;{Wjncp z7N9F0_17LOptO<l@_L`yP0B|eJ42-{1+TKZ33GIO8`!Oj`2N~S|M;~=yffFgTQfN^ zSuZTi?wUGES&MMyg6^%?qF3HY^%T>^A~e0#)VmN!RdvqJ&P>W29gAw{bA2k(uUk^* zDh2QpFL0seL=E^&Yx|chprzU5W9$X~4I_i!$Aj2Ncg62cJIXJA%B72dv!JQZmZ-JM z_N@DoFaxAo+~yZCnAQ=*L|oXCQ;=UVx?yJodM4n(-9Y>fZ4mt#sDIi>zPgdna6QYi zw=U=Q$P<#_W`7TV`4ceudBD>@vUnsy_OKTQbkg*l{jKk9CXaazA1q2>6Hz9APO^lW zLo-|<OkaOip3-7NGPv!VN??B|(Tg&DP=84Xr-q>IU4|rm4dZvm?H#z#XE=9!bXyWF z?v;Xp5fBI$ZRR46{eHuD=^P-gs!l>m$_w6{n5dp9+6#%*p-B5l>z=>~Ty#dx)z`mx zJ}dg8xUlB)VdkIM+3AXjHO}p}uf~9sBIBHC`@}XP7qUp`Fca?BPrq@?3r(P5S0(gB zrB=(X3x9{}_#==N+oRmuZr2}(*)01|_PqE~^%_e^$2cR+j&U43%3GmQcjw#T{rN_V zBsyLyc}}EAQTd7KlXHARLXKg%)<ck;Lk=zkB6gL6N;IEC`A<z^ZX<=pbP#3s8#|ta zlxAf3ZRI9x>T;2q=981ArKG%NoXzWBy8(tjN!Up^!~S)ys_i9LJ{LLcDK&)#3hwNK zgmOhiNnkACg)S4xqdsX0p4_NW&V>|?e{mie8PPN>&$wl}{xQ{8#lU}+&FjC8jzwlM zTMGRGig5Yot#~m-8yhPv_Y2m!20igzqUi&f-Kh<<02>e<G|P|}a~u&KUPORTDx5Mi zHl#8Jd?>$ol4!80ZbYMwhKHw+y>61^>FX<hR(_69$G*loTc!3=%7xguUtu)|Zh<a{ zywoX6?l5K&6rcxVvV`)j|L;{rPS2l{F<@!aeBA=#L}2fU_tDJUNCcwa$+hFUCZ^c9 zN9a-%>znJ>58P%YD&?y;zeK;$&=?lteeRJ0&`t??R`VG7u6BaX5)h^*Sr9Y>&j>Pf z%reOI7URa>xm`707-Z@K7YRIwSu#jyJBgNsvx8*Z>~ujJHOcO3)xGeO2ef)Ppu~zP zQmB5L+%T$vvx(E_`hJDr?``b+I=ormcxM7Qd1sB^eR#sk%E~<2zp}hcD6go%1jNGL z?<%J<{D^2LIw@d{lBDC8l<Q<Hy#>lE0NpD~0apN<u|7V?6U2$#i@j%II^|Wflqgq7 zra2Q8|9%}3NTOn5VtvL%p~9i6Sv<uWUBvZ~Rb_dw!u<Sgd|qp1xR0oah<<9|>*)dN zCpWt-`YAwZp5JoavnyD2g%5oXaQ$Xp0Q9=dXi>{icoladm%EKpnZDql(~=L=%}Im7 zDsYOzJ)EML%~IAgOt9X@l3E@g>CRc-y=zS=?pxEJ5Qqa|vczFg1BljmQus@oty><@ z1}3-c>U?3^LR{m!rgSo(*B98t9IbG3ztA*F7=LE2lFg;}8x}gz6VJ|3ZynUVefp?H z!gZ5Gi@;O5<f!1Mx3as~*ArJXsH82Ugu1iJBl-A(ObW>@_(zdlJ8!dxpew)M{`-Yw zA~ig6b2zcvZ(KTUd7O-s+ELT8B=^0<Mvuw?%?;uwx3Zh;4>~7Il+36Nj33>Emqf}R z++8Yw2yz(cY}dQRH>UvqIwxpoWlJQO&H4En2d8Qje&S*BtN8lc_dr7B<c(6qWGNga zf<1wh{FZg{HfM=O=)Sm{!}Tp3AxrHb0r_@U52vjjTeo0oX*p&N*Tj2?c|K*%8UQqu zk&}Ocnx2^MV5|T;JhADw;`#^8IE~H|jA=kVe2@eHraZwXcG4ieF^uB-Pa#4oNPgHj z&TD_g;EUM@;!Ru0AgzRudmHJ^-tATR|NizX{Ln7F#t!5J6x@opZ{MD@0zTm_5EvJn z`5QD&<0KC9B<59skUnCg@<gAMqzO$7w-=+k+pc^#eKU_9k)1!?EdPrd^LBWc<V&Li z(}ds8Vf@o+PH-2=%ColC11`}kl$OET*WbCn!Xki?MNIo+1l6$LcPjE4SWln%Q8T#9 z?cl^*xHPbC2aIPY)a~E4S^un$QV1_R=8bj{)(7D9H7F6_v=?$w)Y7Vv$>9l3!b*Q_ zMJcsAi&-Q8eh8`Lp$P<*R&(n>w<*}c`SbzJNz0-}pZiI!BIv+){3?FZnS^gTGmc*A z>dl#WvmJVX+vj7+#>~@8uc%({i*OaE^w;0dzJt3=>(&uvl-Yi!ECV`;=y6w>4-}ro zy1VuP@AM4urxT3Y>Uo@{8<Z|pq1ljI$n&3i+>9h@Z9ScsEAyx0PjYb3f(7D>@_4?? z6l^kE7&uq)PZ&BAfwoOJJ~lQsMurx|;Sd7du(SB4P*cr`OYv83Cbi;Gaj0{%yd-z~ za_wpZ)~8SLsWwI9H<6-VoPIaW=BydZ@+dKJwE2z@;~>5*gz^3X_dKVS62RrNqxfA! z&se)C7Q}XUvbM7_@)De#VXq=0i}D+=u@@6OEh9E@5h%EHl{^!9RH8%h?}DTa`I@fR z1QtMg`{e;h5ViA^kf4?yM`T;3;;DJmzP`HYMw+r&QtPh!xN{AIcTUJMV!H+OoJYcg zU*ZfRjiojFW@eP9f$`3;OrYA~!J_(2!`B?Z!fb(a2*~e5e5v|}K>+c<#E2><2fwO{ znp&^hw+$WS;r2fv*iwFSh<jIJzvO$|a&no!31lp3Yip;3Q8)|lcPl#OLM<)x!Nr4| z@gO}PWCm5ApWA|7Q#YF@Ns^gZj*D6rGdwb4OSI;abpPrKbcRvr7*V1kmB@$E@7=*I zNkU7@y&C}0#1pS@;0?TNY*<KPhSm&tsRq=4K`uuiXoeAL_ZN#Yiua?~C;wd(I0)WD zSXTb}%ON(5f<C$mL=r2}nAe%unYXjVGMQov;-ozo$EMR-ZgHbcHZA$OzMjM#+j{Oi zbH(xzsZo^ivRE_cMnTDza>G84sQCTCezyC?xs1I|eZTRUpxaZB?)MnW%>!=RnC{jy zgCtC&#>hIjj-dyHhN10$_d@`YaD43|<Zm~7divM))rucX`_dGdq(Q!mMW?}ZD#6#$ z&HoqqmJ{|;wz2p_4+i)}v%VT(B)B~+Yy=(}-e6(k_a)I~S}4gx)DyNK+9%;r__E%d z#+4~p_8NlqJ0HeE*wTG2(#pokL2lL{IU{R%Kc0K!xHh6{U?LUf{(}b(X3@kB9|ICU z_t!6>C#E$||6RFDA?AQ0zghT8OAQ2=R)D8`a(PARhQ~t-xqJiC9x$8cjN>#9o@7nF z|63|)*?Oe$N}qJG(AmP`$-}-$VM3wl0OX~@d5uF%TPW0$yV)2vb3rsZ%46Qf@T1N* z^*YW@G3qNrTcDQ|EKomy)_ZzsZ)^ATa#TF{%f!e`&pyNX<~umHHW7pOytYf<$(26! z>ee(n(tD+arklqr9rOP{C4(ELFB!HfABz-E^q>%`9|V4Dy?*HeX`$J`uQ%A+YkYcC zk1emf=s%XRhYi=EWvtpKw7FAuE{uT8-C=ALZ*pSdtMR{|{u-kMF20J~X+0q4{`@R* zVFB04m@7GV44uQikXYIyV|9D&iO+97!rstTW>_*%#fE&zxx@U{?%POdG#_Q&wi>=O zb*_2+@g9wW`JF!~D6AmFx6O1Zl(bLoDy(_w8m{ib(n{;X9fS&MgOJ>hzbsyCghS(* z(~QLYkDkAtf0eZKa=5wnB7_0{-~h<I&Gs&iQ1Z`KT`>TV;YHzlV+wG!hn@0=b(ex1 zK|P(iw$G<O%sJ5nd_WhfIYRlZ0gADhf5)XD>$FtyVSl8}6Bd@#i19oI^&0(*9NF*v z@wBqTj!uqPqa(w_R+d&#pF01hN$cN~1D|u;>%21ow-V>Mn=~=fsSocw>F^UL|41vi zoPl`-BbbRl!-d)kzE0_iPuJc0KVM}J@qnXCj_RS;*73Pw^NC)3SGzE<8}O-VX_+;Z z2=TG=4gcWzoDI@^5=Heh!i7dR-x|D>!J*QBxr^?Nc0#MEjfII(VDSO#4xsqo*9c*D z+s|<J70^z~D_?oWeR1=(#nvaYs>Qtg`l0VSDX3ggP4-=ldym}R+{f3npq{Q6pSvR7 z6uVs@3u7QB*ug)(plIB}y-dZ3qF@=byAgn0HxR7u?JR$HuDn%slP_#oMksna7#R(^ z$@ef(kf-q?9F;AcThK#1uIC1Swka*1be$CtJnJi&U+Dq48buZ%Dn`X)@Yta;QQ_fV ze9F(!5K|9u<_g4jjxZ5D5A{7Wj-gcftQucxre+eTQ7AsF);4b)=>>7Wdvbk^(|d>o zAg+3j4=aE(f+0B?Q<A2=%FrFR45WyLnT{1I2h`PFyH&KE;LUjEN}gtO!PXc5>*Su} zNn%EIckZBhPi6P$V!q2+;)@7cTOC50P%!e2-YT&k1o?&&_`F^^EA3=O_hPpPQY9{` zvu5=#IJoDVoXsuS#-MytYD_HZ_QK2|LNFT(Pd!uv_u?yY3knu{Pt|-Z46G<Ggcp?M zc4ARlp}KQlYD)=2&fUo~mU}_e74)+@y1G!YfxBu!V~0gYRv)I~Z>}D{M}kV<M|Ay* zt4T*afw#m$kE#?6KBhZb$#NVAD+>E7GPRwageNTE+~a}cF|XSUmey8}K-|-AE=|~; zILPAU4rs!9RWaMQ7SWb@J^oP>uU&V(u<F^yd39l3T{@B1wui2lD}c&-!<7_^_!RE< zn#G-JVJYw-1ZXgI1;1Kw33dirHMTCja-Wyf4disi^mcAC5X427+D=SK62=<8LHvn4 zF59T-cOVGgg(nVYBF%sCXKHGyd3vp&)v)9S|K#Z5z-08k>6)l0%x~_>{hPPCz%fWz zBRo99N@;>;V`Y|Bjb!XamXU5-)-U!*Z80pY@NLWBX`kts4cZXuS|>RAGtyd8JdU<) zbksWmS3}Rrys%8W9?R?)?p>ZKN8bGYpa2A%tpI3o&>SHuQ%5kocgNPssvgC9No!7- zN-MBqnIiYaBLF!o<_slmxSlQ88@^lrosy!xtfq=_celRROh({C#z8(fuObCEIiOA$ zG_@vK);Bh8;-RY7k-scLeti3846c0{B}GNQRJn5)RdTqVF-j!}EiJ96X<0|^G!oOL zT!Ns2;=wJGOe`yL1%)jfl$0&P<VB!~RCfQ(;J<2rRacrDev~^B{_9ByJgl6-Wi*O` z?9{d|>_p<5XCXiU4*}M|^n>oTGBT41fByq@Y!CmdB~K-J`PlLEckb?O-qe`a1Hk2C zJ@UItHTlod-jlwb-ym$Z22{Kx5X|&+ZxSBXi0`?)SB2vxc8J>m^5C?`iU=P6*9Vq< z=mVdpc%uUzRR};eR)CJfnVOB@t~p>`)ksO_NvZ><!!-~*i(w%)PkyoKRj~WbRCi;h zI5F>_7yeH30M!PhSiD8KW}y1=sn%5E+wk_(;P7;_+nXTHkIG#9++HM|uZMmpw@7qM z#Ew#Wh^4DsO|I=v`QCU3V?>o0ygPP7@W9v#pl8bWZL~8^Q?+%G2mU9&h8rGmcyI@- zp6u<|g5KTB1=w;_P7(W=AkTNh%y+CN7Lsmz#pm|{-Qe#2<kL1KpKH791Bn>V)ka|~ znHq%1gl-;L7e*Aa_6;G}m6hhh(%_Nu;|lPJWL!sQ&D}h%6WZwM>wTbkzswMf9UYt| zxHPX8Yd7%l@U|?{di2jp-7-z%qy_%kRgLevx6SqS2?+4lM3Pew3t$M1&x&Dd`$3$9 z0v2@u;)t5uoM`tcw335b^(6C|^O;o24vtdAsPnUli=<_eJE-$sHfQq<@-vX5E<P5S z28H2D-&$vSUjA#cXyGO0&crX8fZkY+Dnklat?Ni#d=MYyekdKgT0+b5p-$9}GGLJj zrKdL&-PsRIBlmbx#c~VdwBqMml+$V9?=QP&tV%*`n0%4QxffcznvWe^+?8x+?F)qn z7Ur@*V!G1m+BL$cLEBpqvc$=91hH)k#7ZLA^<2)gxuTc{y62Mi0k38Rm=j$YI=vL` zZZ!Aeu_-pVzUk>n6q#ZJIS3EwqyWE?7<-;0vEf_Wn}!{iQ3X02vSYsKRFSCg$fMu; zRZqd!itm2<DedSKo^?Mz4OLR<%BOqYZ3bF4yYbGwK;{(;1#X_mXfc)ngp#y@tE4l( zHieS25~~1Sv)_(XL~!9o({WNyKFgZ8MgBwi!lJFLL@IKmxXRSQZ9D5X77mo3!10I0 z?j71_eoZIYd4^)Bwm@OSnUse`tPsCKjak0=Lnh~YhoMOuqy!;<<Zhb90NtJA8Uqi4 znN{Nt6IlG~i?Tkape%2tp%ZOVA~5@bD$z;nPw@=(ewmSr@u%)wQ_|90!m5YgJA-C+ zg=#l!rGYvvOQ!92L2T#Wda;Jf>`@|KO%B>G8#|P2N;LbKqtj|SZ<<ZNmc011d4!7h zA#5w-86ac4W*ie@<Ahq8>ic6ye?vzRaxupP1qk%FPXDTu3~z77cXu+pYO@T+N8UD~ zP&m5+u5vk$P}&82&S7o8>9_U$0up-j6qB3JusdI4=A+(0`S<E1QhLabxhdYY0@*`G z7QmMeb>&xmKhhZ<=9;xIH?M{F-ACnZoK9~SB=z$U!V3(VXSDS{(0;>ORasftZC<V& zoi9<;J}P#o&k7Qb47HvYkf2<@MvU4RIJze6*bnkx*;cHo%$W`YCRCzPfJejfU`C`9 z2Q8;9#%L4v*1+=EdfKAG4sY(IrpAMo3vCUKhIENXUDy<v_<BrzD6VL>fgKmCqduE` z7Lfuh+N+H+*j!TPgG=4a%)^ZS54_o?gmYd+Iy*Q7uCK3073AigBT4poYY-O|8=Cy? z#O!^p6<=kddsZn_H>qsnnTtINe^;dc^C`E;$qf|GnFat)Zme(jlf{$*i(4yxYrB<f zQl?AwLTL_v{J35%xoe~b*ggf6BlFfG^EY%Kvqm^j!=EYvQT<3Ulm!je4k0tuOrr_X z?Lm`hZr(9E(`i4>?#q5{)hli%yN2muZ~bJV97vLEfLsa1t-2y3?|OO#E7d>ss-u0U zMiir+J4W-*6}_aBQ_q><Bxh>aMVXrfqN!Dqf03AeQ#ygMz=<{Mv#00JQrAkT5U)4) zU05q7$ST$&ejsluj^?)(N7=s-oD`QN*9p4NJ+Mgo?@tVLkCF4=dg<H{kil-PaB#~U zgYAynH$oZm_VzlSFu6t84ikspIs-h2e2-XBKHi8$dmt(<J@H6T6{wxp#R3r<y)<8Q z-DYoq?(cHVqNCz9IazBTwCL)QhHhB<6Q^oHJ@G5+F+s=(nv|%SfV8mxdC2yoUn;*q z-*sgMRF;T{nVEgDj<mMW)pAPXld^a<vK(ne#h#Paa-GtnHsg|g=VqvaT-R&#WBHQ2 z?;2+>0hhhytM4MvvM*zI*wbm?L2u=yI~H!!%D@1bv^2Q>9I+HM{r`S~%t{DGh$ z<mTa3zIBr1Omm0hSpP3b|LX1AD+Ivrf#yB|Bz;CPF*52a>gTC@=xGS^(fuMNPkv+r zwRBQbRrQq5<h(?WyfXA=bBGw+f|0KYE$p3-w>Re4(Yx<W2VzLjY%$EY&!}o^*RG@b z06$tpWl+w5RM%smU#<I9l4GSHbDELm`P^@RFvvZvD|Of^*K3@`#~lo-2;1~{NGOYa zSkUI2@Z?jr9-tQ~^wiWuW(Ox3q#A6S^hV^g1dF-HCDxf}cz8gDMa9qZkh%S#ze7W9 z`thM}@M4h&pCx{M($U*n@XDgp5r9G0BO}~S>VpEEOi%0jzzK^tv_pq&rv^e!wO)Cj zB;_5q^#_hw5g&)w|L!Ri)!8xALZ!5+(PWCbZv63MIZb-UGUBdA|L0AC|HPi1!INc6 z@njGiBGU}bD_Lmu!3Zn&4Fw`T6gT`d$({MfKxpy<gfNv%&sSqcE-3OwAmcnr1wtg& z`Pq%S8Va|HldonSrf>z-$m5eD*rKUON<{L=P2)mlY>Bge&;k?Qi7@WHIZJY`mVV)( zNtU5m#i410pS=QLxWp03P(iEZ&970S(~;G&Q})%jC9wH%W4>d{CwcyK6YJbXw~CZH z6#FyR&v}llf!*lGf<>cjcvJ#UFRcL<s16!y8pKAde!^Z=t5PD8?)VMA^mcCvAeZUa zbplDAX5;A+b)t-G0u~%_Ao<Y_e!txG?go7?gai~~yBO;R%PkvhArjzIi{V7dkxPQ$ zu2;jmImW}z$AxBQV4C%y2OJ)VGM$P+7u00Ulpw!84#bJslngtK32Zm6O?@3%$CrIS z40{>$$MZEKN1B~~I;a-AOpADL^rXv2iv_--3psKdJWGd&UI_-BS5Gh7T_HH02;|M| zUd0p@u>y}e<?}}hI3A`+F&$$#P0)e22<?7X)%96Q^bgU{;uw*YIoGK@bS0t%O!V!{ zyLUcwVnYvowmkbVF+nBlggRw)7?(dVBM8j|thER*LtHxoE#m_#3GCDaaK35nh8KUq zAAO523fZ)>w7KT}Go*PNPa!#RPogQqf5n6q$ndVc5213;lFuIt9{(e*-{^a!K7Mib zvg-BQf<aONYQ^7fZtiiwXoB3*DX@E%K!x42y|z}<YUy0pRg)DL{O&A>72DV<Pc6)Y z_gjg}sJu*5;<gmkInF*T-W394%uEKu&vLr!vPw!e4uBr(U2p{s@78gDzdil(Q!&ET zL06g-bCtJh6+qNst_6KE7X@mUW~T>}uh2?Z0s0>>z9>gp`Zr0qsnx|$sqs&5eY+ne zIdVbVn?bhxuk-rJgSef315*ke7~A2`xX8t9Yf1~jz=(iWba8<xbs(7czVFLKY<TP! zH`mA#)?#a0JVutQmeSL1X>IjqeS@@}nYDmXtT&gQm9-w^`yO}8njL`{FPSI>`Ac^E zME3~isIUOedVGBMZ`OjAwb!B(*4HJt8f@SOgXI^JXRpVcH1AQM^cd1-idikF%WYZu z%k41dLy!Kmma9<RH<aiyUPf$*qpz0(Bw{`sCv)-9b^OZBe){w&OMYi>ukGp%<*J># zCkPq*{-dH`#~QTB^?p>?Tu+GL=S0<<v69JZ{@?PoD<U3FIjjoEtbc%o?CxBV7?Y)9 z5!vEd+fdWStNSot3gR&FWg9I)hbb?lxUyyIs3d)KID<s-Xk8MXO8S^ZB$J&<?XShr zgZ?aXal0=fd|a>}Li8v((&fg|>!JcgOB?F$foyfpO>5}?d*8U!i(uoHg@<8e{o<N9 zAR!?ct>u$WqS6C6FGEUW=m~~$sW(u{NqBlTYjSrGb81=N{MF&nAqIg&ub&P}6kQfP zXkDGZc_Rz?DmY7c;Ym-MRg$B1m`I{pvzP{mm_>Fi@s9@1)sIou1za5)jTkwc>AF;5 zh%R@7t^NtVCZ)N}hE)nUAad+{L@F%t>XNu?Y?&&V*cF^W)**;K0I}xbV&=;N9wR2x z5)$Z3xUaJL5Eu8&lf>|p>ub3FEK3skWi0`I{_z%>rUwniMHtnBGsrHEjhTQ~Kr)4L z4J6&sYSs5U1qYsrH1u@TZYG1iv!Q=yWF39hciJ?q@)L-sg;kyqIIkJ)<N@^=66b5; z$xY+3RlCY5@1@)4c1J3V$HQ79Xt<$v2a)tODefy^%G2!Z)PNy~{rMOb2_&U|z=LLj z{6`{gE3fH0mis?{7F3WDktWwLP4Q+oaCKXhD>O>ZhgyCOvTg8W@SqId8RbVU?A*Xg zZLTrp=RhwqdaiX%71zz(x?fLHilP7y>UF{5#QfL}C$UCl@1l!IL#FsIuk7@6`|hl6 z9My<Gz@^UauKUS^b?@;)!_j`^fH8<bm~wxmmqzhXJv?U#JRszHPYf6g-`FkP@)&o> zW2DuxY-)2{#j&Q78=%%RVvilBA}SYI(%~crU-~36Jn~CST3UU4%opJ_5tsKga9;;W zoe2mD`Hbp&%|QmL^M0p~3`p|h_Y$Gxq<c&m2Rs_ax;neT)#Gsq2^FUExB73%|GWkL zmTiZkElVUM$opO2SH@X0`i2(?wr(nFqw?t$$CNWL7F@#>y_aRuAJPikdJ%G_U0;LI zj27JRV04jTO=_MAEft^Jsq<r8h&iVcVAEC&!O!^GBUH<w9-`|VP2N_!e6@_4Bc2X! z3HSvRF2)6L)=n$PhUNc&iU0dHAaUcCHFPwFww}v#I78b(v?nc49)SlSE~Q{@dV^+M zprL3F)u}#*Vh<RE-&Mkdl~<axl78Npfy_~|Vpx0(xrpT7X0Sd%h?@Wpk8=qtIRS7< zQu_~Yk99L4QvffpnL+%E6}WGl#UNu*%k`)b*BX<-eW~gPj?%wC2Owo^Y7)=tfWnaB zM9tyVR3N-5HcrCa8#re0w*pR)gKftc6m2Dt|5Pi)#AG$RLIIF^HDn2##__#1Q82?x z!op%)<=IBgZIA&Q9(L^p%K#W#o}l-tFka{ZH|=8p#vS$~mAs=%R*{>8v&9B7AUhYE z<kjvZpphbGGpfPPC%NlA|8XM{L0qtBLLo=I3|l-n^#ZHM=e7akn(?9EX-UA<Q_1C| zS{5aB6sQZ^o^$aOd=HA}0X9uXUNU95XK$oRef>ce9&)>Fw|KeQ2i#Bns35n@WmFdg zN>%;L$at4S=EfPYU+b@iDX6HZkcL6(#D-~r07=h@>*>5{%R!?6;k!f)Q09W)mAAr} ze{8(Wko`W+3rNaI?7g-ZSgQ#NjgZTX;X|n)k!BlLt|v9Ju>Q*fumh_<cUwkt(Iz`y zAf&f}dn!=|#%2mwJ64BkLcQr(6&1~f?=MMc3|pDw`s*-xwNulSfRCorecK6uX5<CW zT*xShVd&gnfLg~mv$)vuA<V7+2qYwWadwTAZYKU8`oJ*`|5XLVuyEri>e(}U2qr>& zGV4*CHe2$;MXju~`t+ag%uVmmWc{G40Ac0%7BwT0J^AzrlkO}#)oY@MU3zMa!`?rC z{#5^CDkG!5g-M)m&um3nyTO^^2@9n<={OGz3>QYG!sJp+2(qT`W{z3{0+drM=mSD| z)DCG+Pl>30C|{ZykzkXIumC$(*^dK~J38t=?<^a5Yk1}x@Gxgta22n8CO2-3y0~dy z&Gzy3lp8I9p9Y%7RIti9z3Sp2@j}drs6`l`Tt*e?l+$cM;}z7#KtG!l8(R|pLa~aY zie~iyd1p}v%z27nZRxbl`PvRRabGn;K~3w(6_Bczw5*4%8U;j4ZV7EnZXw$sJ;PQV zdRzv5@cRf9fTX+Xn+S#LdoDzKRpd=D2>~NcJh@DhaeO#`azE-e0|-t)0o=sIq*7b& zDF42)W%<OQ4gRgN(GgU%n&svAvdwE}Nsu#>_%idV7NANY&R0^JxyKj47cInIlIQd6 z(-y0XNh58*m@W#t35cjOo^xfz=<W7SRwH7v2O=X%PQ8CqcCk+(fZxIrh(H&5uFPpj z4bk-*7a+>zL;%7+q+0{*zRMM=o<CKv5YRDJ{}Aoy8=9Kp<D+|#L+1;|X@>YVQi~K< ztUKSJgDEJ0GGBt{{@74+O`}{V(hGY2$V6*Ykm%%1+N3#jJi%B$%B3kZ`Pf3?NK~C1 zAaxJvI0qJWYzlrF`4VesYK{Iq`4^iAej2b=!||s5;az(dGAn{rB0pInND5Q81qyAw z4ABQMr4{A*+y3myT)_VM`*Cud2pv8fk017X_n7aCgzw6^hj^az8o!fjTJmPBxH1ik z_ySk>oxA$iuU}W;tk}3*#lNKu>9c-}KkibfuUV!S^U}L%=yw8f**L3UmYH7Uc>eu( ziu;_a7X%PZnEeBoXn%>p{Lj=tjo%ncPd}}{IDC+AS~Bc<0+ZFFYL9%N^XGNdtxMem zL~rn-{Q49MnOb22I6@YUoYl6yc$W&(+x4A;d`cyh0Ii(toQkOn>)_0jQ~&vZ0%?GV zkBNXvg8DhntpK$tDO!MoyAIG*!d*F76v<ke-gy7bx5obCM2Q+rvtycap)ubIm`>tu zk$pb|+GS1<E{ThY3i_rj@K60tuQU<NMjGby!EwWn)Wl@a%cmh(=sFI@_{!77k@r*| z<bqBjc5vxMWi2r0O%{OE9NV2$^*^|s3K8)m+#>7hL+d2;f<Q4=k699~p%c5@9^YyD z1?bUObUk;?%O`BZ{`-Y>04#ac5fNudGA$xc#RANwNDPL?Nxy##KY|Be0EZa~h8YNh z`Hvi?6@<oI++2<T7AT;Bwas5**|eBL26)ZO$5O?rMWVZ(o&)dmNbO^+GuHm~LlJ!; zHC@dxhbou=Jww)v^6S^tOK#<$73b$3Xl6=SGKT+Jl5^1o5O{g~(Y4@bv8~x4NzP_@ z7&*$tiy}~G-ml8#U;HEe8_3_UqkD>B!z`UI*hfZ&uY^sLL(B^&z9gU`839XYI8?Rg zxJx9LYYV|&;t0g=GGIsV$d^O(eM!W3Y!K;=d+Lz{r)qOx@NzeHO31a>4NFfWRN-Uu zim$y<2XWUJJ9RcOlWj#)bOMSe4rQRWgrH@xFP&#nmdQvJ3cev{(>l^6zB_*YS!s9> zu=A?G1R6Y>0=+}|F<`KXHC#>%&h5$1M@g}7(q*m_S=~D;Xxh0T?7~Eg1G#^2nH|8v z=hsZ_mhMjQ^yvHRL=SgIh3=#k)|PZ*Fhx<#F1X-eKnslZn`8m^I~vKh_^CxOKCm1( zF*=z@DR6@GovL9$oW+vrNWh2YzNei+gspyE(|#*6{%k>*+j@8TxECk@51YPf`ZLKz z5#J+WusO$vr{6nt3BFMdp4oa^>mJ46H~GgYYlHOk$_;&d83P@i3pj`>XdqL~a<+M7 z#KfKs`sS-bfJV#%wEo7v7ctEO*gU5ktT3r!?j;BJZ5t@GxY_v6(KRv1oLCPEl99q< zr){{jGImIb==nxMKsC88FsSypw5OAx9t2Bg@QeEDf6ntU%+XN2R|?YJVV>V^NA@jS z)(kT9h6$ts@6U1{Cke=$nh|A80rcSE2qtKoy#a)x@&5h$(f3<Uv64>(P#fpqf=hHz zz9Fl#5zr2d+4fD&?i?XGHC|JL#ek+=;5A>o;SV@Fim95p7Vb;aw813<<SFP9@c#-3 z)LeHmQBnZ)^%jsxKM^rTbF9FyUb7GSQ&N#$w3Z6=erUkjt#qqpX$XgotiN+_1Jk~$ zf#WF&1PWWe1Ys~>x9$V)QexkvykUWA=J1z5Gy3$;I*-@Qc%YFQYy?<$t<!&=5KI7e z0<mQ7ib>iuKgrlMau?YT<fUQ2ZX1r|s>}BbH_|8yXaui70D!rP$&y<r#3`>F7>853 z=P!S}{$4i*Et+mi;n+XKAMnI<+yjFEt|`+*;EHym#eY@H1I;V3kP^3uFDys9SI}g5 zbQG(hBaIYjBzcX&w6q&!(lx)ecqu7cz53NHQxM$X>3DQh!an)Q;GN|J$W2t8Dtvh4 zEt=QAC8>2$0dx$><CZrV>le=v2U)=k1P^ROeJwTT3WDAbfP6jviorufNLcLeFxC!_ z7vQSTkMiNQU_HZ}m+s%`m3Iq?lLCBn1`MYEU6=77Aa#kh<l$0W%JZs}IHuIx9NX2% zL^F<JfSb+2Wzko@KuKx)1F|sDKPgysIl&rr*-HUrui$zG7bl(Nte_w_Bh1fz4ad9g z+<@cWx2Tp`@tc04Xz=gL21n<7At6s@;OU*jCh0|skS^R&rqJ^nkDoXV^8X0Z3GiG^ zT4=RiYrK7H5;6V*_azu1fyNpY0tUdI(2m^>SxnsrF%ju`Qy*3q34u6>t8<>LbbjzL zXK^nWMUlA+J)^=7mkle^DsK}OFb^UpfTd=<MR9d;5*FsE*BN@JUAl1vlKkH5S7Z|~ z|5GFM!_a)Vkglad#S|1G$Rf{m6(_W5b_*Q~t@v@QO5iYaW&konlC4)*f!1aRfgQ1e z1PALO<BHbw5sWn{Fl{T&${{f7>oX@5q?`83V+6Zx9*Lj1YkynNC4509w7o9ho_UEt z8qf@4wL#y6u;+AB^I}Oq(?~YRw8*tCq#{`qhV>s#jkWyse*AiT7PQc;G4$?7M^C_j zpli^h=*!4tx!T#u@-wMnf^5Q(k#eW<1jOXW9g-}`z3;~02Z>N9kYe_~y{a)aKKs4H z>NB2wT>5`XveO6&^wtLb=aV2Lhbg7B3aCn1Ka9I|(afT^gE4Eb(!*`$;#%J&b&beS zlSKMWXOE(0@r8n2Gh<k+E@l1M@VOR+s8TPoz{CI{+)G<F>y<dXDDd@%fqwtwH+Ue$ ztUvGf4C~bGB%`Om^wjMlf$1Agtu2AQ6BELr#e;fXkQeHXAw7r*y5WSuttybBrx#k< z^8uUpJ)q)9fqAF*XIb^h_?P@(@$cpF@o{-)j%uJM>s#6rVg#B@QF0y3lDB!+cN6mN zgn74*Bt?aWMUu*7GoihX{zI?HhAHn2OG-@V4AL-b<)<({3Eq!2z}_avJ}EDXS*-)c z8M@FWh5l^L0K?FPF>3Mz_ja(UT*h6NT4?<?#VSVY*I$^v5H0Jayj;9}l$INjiT-g% ze98QrtP0ynkV@#eOWDSovTbYW7^v57x|9I!8nb~j7ifDYrK!eE4I5as0UVy(z{CT; zbi~%Vv279Zsf_RVT%Wm5RQArv0}|A20=m2We)0a_X?gjZI*=y1faj4&`wM3&@hdMp zCn$VxyqJ!sCuhnumSCOFTsli`P(xtH`*-gu8lRKX^N|w*i|G(>!>RIWHjYxsL&Tn4 zm4Zu!jv6`C33O<TS&un;4?4qTN=phFpAElf*nhPmCqrB^7wjSA(>rco2{mRY$b-Qk zjrFqWDsf_mZ^57k9*{v)4mOZY&7e?VYIP=%h)DpDZ1z|FuPVrmw@ChhTlhtv?y)~I z4YttUNxf!qEp<0%OjcQW4Gzi)sXck4M<KV(bFH2B&Q9*Of2*PAcre-VeP8o#7T68w z#JE5(H~v;CghEm1Xx+#wg6(m`SAPkP4on;qkJ*fj(zSw{ULKf2oiP)^QLO9-*y5Pa zLezl^3k$#YLSFPAK)X~)lTA|hM`eHZ4id?Ds*{RF8`sPgsR`srIpn_ttH$ZQoSd?l zn1Pv%O+LSXz=iB?B`~>i&Z)699XQerRJJbL%~)}0ITz1d?-y%?+{XLJW(p-utoLtX z1>|F2Ko^|_qGu~w{&|MSw~FrjXIKZeHcq~^cWZm+dLIVNb}B1Kisk-v{_ZB+kaF3T z+Ikc?3!6VaPvcH{mZd0XI%ZrMkuvVV+n$|MU~6WEKSv>H9ogh30YqNG)AJszZvM{? z>(RjpA^|i*WwphN%6UL1T0S_>HEB<n<4N=fFUKfr^1{LXMMsU{Y#z_pL1R2h@!Myy zUv<Ct+khazS)($($hW_^vgCVZy)nEUe@SQ6Bb>RyZyi{8+<36zyP_^#5RK(Js`WJO zPL+q*DJU0IMsF(?h_qjRT~#KzJN0fI06w$4Ew4~s%|?KBEQ99>*u6?iD=Rfd%UbXX z3JR{^m=6K!%Q&EskKq&79nY1#yncJQ>J>55>j1RMn)Up|6vW-u=H_I8fb<SXokcU; zj=OpwRIvgwy`ErWt`++0Or#fQXTQl;`O?cILZN(p<D+rp=lF7urKb7lF?dch#5$cP zNoftKr1Z}cMSn^$d>dcdbBc;kVgz@ZX7dtC<|j`)vFfxAI^sdPAlZ~&(^fqZYSC^u zNNmLKk_^-Nf6rofN^Ti(tO}gcU_?jI!?4i5><8g8Zygo*$an-;4sC1qvE`Q4LCiZH z9$u_(Xy?mvm}PZ0Mmq?{$Q>mPr#Q^-@sA}+*=T6@gLf0}1znNIZ>|S~EK=%`Zjy|j zwlr!v{<4=3#h1mZG|#_tr_}BuT6eDiBM9Kq%oct{>iI?!a4iskab!PLt;Pj0`pvlS zc0A#&vif$%ix*CiHKQnE*$*t?V%?B~HK1i;?M&xJLg}|^uycjAhT5!LYx%zN+P~PH z`T9T-J&@*NPJTx@`*~LS$wJG~pb4Fn;u|o7+?MT2Ktpe!suYGlhxj8HRa}P>^V6s6 zAv$3VVxWM207<Nb1?a9r=0{4$$H#d`p2u#6B=9>utsd#HdPC<*#m1E-G(9u?$cF(m ztgWNXFF}fO!R>spX6aJdJ4F-oEq``fZa=Vb31wnREj!x{EOCH5B|U5FEv<Q#3eDgz zv-~=#GV_O+sKKW<o~!9!fM4a;8;jCeMCUedR!QE(1Zt{npGtj(r;0wmNHFx*Fx_ge zcc^%$eVle_wiOPc&`YnK_V%#~QqtrB>Q~>nsPZ2w>!vNm(MGKrY|G3kAg53J)lQXu zW)-|~N2foS?<!X^Qy=s2J9SeJfxcU489KDpr>)+)9UKz#pLY!34s<qHR8&IUFB0T< zzk9DA{+1`Z){?Z5v)XvPePDPPaMF5m36ws_^XO@B7~i&y3=a>3X2}%r(<2~k&f~Xn zQL#p2hCf5TJ2uUniS|bQs;9TBivzT_yxfLX8d1HEl{oqHDj0?xb#xA2K|AZN?&Wf( z9l{u-=H_;x_~gkGg`krm3#^LKv>mKKVPD8oHwUR4PWiU3Hz9(hQQxN_F*h*Fh{*2p z9raWGOg5hjyd^YcdRtLMZNn0Q@#EHe=Z5Aj$gxWr^ync~O&;tDX5OBG*{I)G2%-m9 z3jct~(fgX3lTei9n-&DGcH$E|^b257ux2c%0AAr@X4!=3v@|^~-sk%UMFJtMv3$68 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≺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 literal 0 HcmV?d00001 diff --git a/Grinder/ui/image/ImageEditor.cpp b/Grinder/ui/image/ImageEditor.cpp index 53cbac6..a64801b 100644 --- a/Grinder/ui/image/ImageEditor.cpp +++ b/Grinder/ui/image/ImageEditor.cpp @@ -1,66 +1,86 @@ -/****************************************************************************** - * File: ImageEditor.cpp - * Date: 27.3.2018 - *****************************************************************************/ - -#include "Grinder.h" -#include "ImageEditor.h" -#include "core/GrinderApplication.h" - -const char* ImageEditor::Serialization_Value_Block = "Block"; - -ImageEditor::ImageEditor() : - _editorController{this}, _editorEnvironment{&_editorController}, _editorTools{this}, _editorDockWidget{new ImageEditorDockWidget{}}, _editorWidget{new ImageEditorWidget{this, _editorDockWidget}} -{ - // Assign the editor widget to the dock widget - _editorDockWidget->setWidget(_editorWidget); -} - -void ImageEditor::setImageBuild(const std::shared_ptr<ImageBuild>& imageBuild) -{ - _editorWidget->setImageBuild(imageBuild); - _editorDockWidget->updateDockName(imageBuild->block()); - _editorEnvironment.selection().setImageBuild(imageBuild); -} - -void ImageEditor::serialize(SerializationContext& ctx) const -{ - // Serialize environment - ctx.beginGroup(ImageEditorEnvironment::Serialization_Group); - _editorEnvironment.serialize(ctx); - ctx.endGroup(); - - // Serialize tools - ctx.beginGroup(ImageEditorToolList::Serialization_Group, true); - _editorTools.serialize(ctx); - ctx.endGroup(); - - // Serialize editor widget - ctx.beginGroup(ImageEditorWidget::Serialization_Group); - _editorWidget->serialize(ctx); - ctx.endGroup(); -} - -void ImageEditor::deserialize(DeserializationContext& ctx) -{ - // Deserialize environment - if (ctx.beginGroup(ImageEditorEnvironment::Serialization_Group)) - { - _editorEnvironment.deserialize(ctx); - ctx.endGroup(); - } - - // Deserialize tools - if (ctx.beginGroup(ImageEditorToolList::Serialization_Group)) - { - _editorTools.deserialize(ctx); - ctx.endGroup(); - } - - // Deserialize editor widget - if (ctx.beginGroup(ImageEditorWidget::Serialization_Group)) - { - _editorWidget->deserialize(ctx); - ctx.endGroup(); - } -} +/****************************************************************************** + * File: ImageEditor.cpp + * Date: 27.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "ImageEditor.h" +#include "core/GrinderApplication.h" + +const char* ImageEditor::Serialization_Value_Block = "Block"; + +ImageEditor::ImageEditor() : + _editorController{this}, _editorEnvironment{&_editorController}, _editorTools{this}, _editorDockWidget{new ImageEditorDockWidget{}}, _editorWidget{new ImageEditorWidget{this, _editorDockWidget}} +{ + // Assign the editor widget to the dock widget + _editorDockWidget->setWidget(_editorWidget); +} + +void ImageEditor::setImageBuild(const std::shared_ptr<ImageBuild>& imageBuild) +{ + _editorWidget->setImageBuild(imageBuild); + _editorDockWidget->updateDockName(imageBuild->block()); + _editorEnvironment.selection().setImageBuild(imageBuild); +} + +cv::Mat ImageEditor::getDisplayedImage(bool ignoreLayersAlpha) +{ + if (auto imageBuild = _editorController.activeImageBuild()) + { + // Only include currently displayed layers + std::vector<const Layer*> visibleLayers; + + for (auto layer : imageBuild->layers()) + { + if (layer->isVisible()) + visibleLayers.push_back(layer.get()); + } + + // Render the current image as it is currently displayed to the user + return imageBuild->renderImageBuild(visibleLayers, false, ignoreLayersAlpha); + } + else + return {}; +} + +void ImageEditor::serialize(SerializationContext& ctx) const +{ + // Serialize environment + ctx.beginGroup(ImageEditorEnvironment::Serialization_Group); + _editorEnvironment.serialize(ctx); + ctx.endGroup(); + + // Serialize tools + ctx.beginGroup(ImageEditorToolList::Serialization_Group, true); + _editorTools.serialize(ctx); + ctx.endGroup(); + + // Serialize editor widget + ctx.beginGroup(ImageEditorWidget::Serialization_Group); + _editorWidget->serialize(ctx); + ctx.endGroup(); +} + +void ImageEditor::deserialize(DeserializationContext& ctx) +{ + // Deserialize environment + if (ctx.beginGroup(ImageEditorEnvironment::Serialization_Group)) + { + _editorEnvironment.deserialize(ctx); + ctx.endGroup(); + } + + // Deserialize tools + if (ctx.beginGroup(ImageEditorToolList::Serialization_Group)) + { + _editorTools.deserialize(ctx); + ctx.endGroup(); + } + + // Deserialize editor widget + if (ctx.beginGroup(ImageEditorWidget::Serialization_Group)) + { + _editorWidget->deserialize(ctx); + ctx.endGroup(); + } +} diff --git a/Grinder/ui/image/ImageEditor.h b/Grinder/ui/image/ImageEditor.h index 614d46a..c50a3b4 100644 --- a/Grinder/ui/image/ImageEditor.h +++ b/Grinder/ui/image/ImageEditor.h @@ -1,59 +1,62 @@ -/****************************************************************************** - * File: ImageEditor.h - * Date: 27.3.2018 - *****************************************************************************/ - -#ifndef IMAGEEDITOR_H -#define IMAGEEDITOR_H - -#include "controller/ImageEditorController.h" -#include "ImageEditorEnvironment.h" -#include "ImageEditorDockWidget.h" -#include "ImageEditorWidget.h" -#include "ImageEditorToolList.h" - -namespace grndr -{ - class Block; - - class ImageEditor : public QObject - { - Q_OBJECT - - public: - static const char* Serialization_Value_Block; - - public: - ImageEditor(); - - public: - void setImageBuild(const std::shared_ptr<ImageBuild>& imageBuild); - - public: - ImageEditorController& controller() { return _editorController; } - const ImageEditorController& controller() const { return _editorController; } - ImageEditorEnvironment& environment() { return _editorEnvironment; } - const ImageEditorEnvironment& environment() const { return _editorEnvironment; } - ImageEditorToolList& editorTools() { return _editorTools; } - const ImageEditorToolList& editorTools() const { return _editorTools; } - - ImageEditorDockWidget* dockWidget() { return _editorDockWidget; } - const ImageEditorDockWidget* dockWidget() const { return _editorDockWidget; } - ImageEditorWidget* editorWidget() { return _editorWidget; } - const ImageEditorWidget* editorWidget() const { return _editorWidget; } - - public: - void serialize(SerializationContext& ctx) const; - void deserialize(DeserializationContext& ctx); - - private: - ImageEditorController _editorController; - ImageEditorEnvironment _editorEnvironment; - ImageEditorToolList _editorTools; - - ImageEditorDockWidget* _editorDockWidget{nullptr}; - ImageEditorWidget* _editorWidget{nullptr}; - }; -} - -#endif +/****************************************************************************** + * File: ImageEditor.h + * Date: 27.3.2018 + *****************************************************************************/ + +#ifndef IMAGEEDITOR_H +#define IMAGEEDITOR_H + +#include "controller/ImageEditorController.h" +#include "ImageEditorEnvironment.h" +#include "ImageEditorDockWidget.h" +#include "ImageEditorWidget.h" +#include "ImageEditorToolList.h" + +namespace grndr +{ + class Block; + + class ImageEditor : public QObject + { + Q_OBJECT + + public: + static const char* Serialization_Value_Block; + + public: + ImageEditor(); + + public: + void setImageBuild(const std::shared_ptr<ImageBuild>& imageBuild); + + public: + cv::Mat getDisplayedImage(bool ignoreLayersAlpha = true); + + public: + ImageEditorController& controller() { return _editorController; } + const ImageEditorController& controller() const { return _editorController; } + ImageEditorEnvironment& environment() { return _editorEnvironment; } + const ImageEditorEnvironment& environment() const { return _editorEnvironment; } + ImageEditorToolList& editorTools() { return _editorTools; } + const ImageEditorToolList& editorTools() const { return _editorTools; } + + ImageEditorDockWidget* dockWidget() { return _editorDockWidget; } + const ImageEditorDockWidget* dockWidget() const { return _editorDockWidget; } + ImageEditorWidget* editorWidget() { return _editorWidget; } + const ImageEditorWidget* editorWidget() const { return _editorWidget; } + + public: + void serialize(SerializationContext& ctx) const; + void deserialize(DeserializationContext& ctx); + + private: + ImageEditorController _editorController; + ImageEditorEnvironment _editorEnvironment; + ImageEditorToolList _editorTools; + + ImageEditorDockWidget* _editorDockWidget{nullptr}; + ImageEditorWidget* _editorWidget{nullptr}; + }; +} + +#endif diff --git a/Grinder/ui/image/ImageEditorEnvironment.cpp b/Grinder/ui/image/ImageEditorEnvironment.cpp index e55b56d..1d8ebb0 100644 --- a/Grinder/ui/image/ImageEditorEnvironment.cpp +++ b/Grinder/ui/image/ImageEditorEnvironment.cpp @@ -1,76 +1,88 @@ -/****************************************************************************** - * File: ImageEditorEnvironment.cpp - * Date: 26.3.2018 - *****************************************************************************/ - -#include "Grinder.h" -#include "ImageEditorEnvironment.h" -#include "controller/ImageEditorController.h" - -const char* ImageEditorEnvironment::Serialization_Group = "Environment"; - -const char* ImageEditorEnvironment::Serialization_Value_PrimaryColor = "PrimaryColor"; -const char* ImageEditorEnvironment::Serialization_Value_ShowDirectionArrows = "ShowDirectionArrows"; -const char* ImageEditorEnvironment::Serialization_Value_ShowTags = "ShowTags"; -const char* ImageEditorEnvironment::Serialization_Value_SnapToEdges = "SnapToEdges"; - -ImageEditorEnvironment::ImageEditorEnvironment(ImageEditorController* controller) : - _editorController{controller}, _selection{controller->imageEditor()} -{ - if (!controller) - throw std::invalid_argument{_EXCPT("controller may not be null")}; -} - -void ImageEditorEnvironment::setPrimaryColor(QColor color) -{ - if (color != _primaryColor) - { - _primaryColor = color; - emit primaryColorChanged(color); - } -} - -void ImageEditorEnvironment::setShowDirectionArrows(bool show) -{ - if (show != _showDirectionArrows) - { - _showDirectionArrows = show; - emit showDirectionArrowsChanged(show); - } -} - -void ImageEditorEnvironment::setShowTags(bool show) -{ - if (show != _showTags) - { - _showTags = show; - emit showTagsChanged(show); - } -} - -void ImageEditorEnvironment::setSnapToEdges(bool snap) -{ - if (snap != _snapToEdges) - { - _snapToEdges = snap; - emit snapToEdgesChanged(snap); - } -} - -void ImageEditorEnvironment::serialize(SerializationContext& ctx) const -{ - // Serialize values - ctx.settings()(Serialization_Value_PrimaryColor) = _primaryColor.name(); - ctx.settings()(Serialization_Value_ShowDirectionArrows) = _showDirectionArrows; - ctx.settings()(Serialization_Value_ShowTags) = _showTags; - ctx.settings()(Serialization_Value_SnapToEdges) = _snapToEdges; -} - -void ImageEditorEnvironment::deserialize(DeserializationContext& ctx) -{ - // Deserialize values - setPrimaryColor(QColor{ctx.settings()(Serialization_Value_PrimaryColor).toString()}); - setShowDirectionArrows(ctx.settings()(Serialization_Value_ShowDirectionArrows, true).toBool()); - setShowTags(ctx.settings()(Serialization_Value_ShowTags, true).toBool()); - setSnapToEdges(ctx.settings()(Serialization_Value_SnapToEdges, true).toBool()); -} +/****************************************************************************** + * File: ImageEditorEnvironment.cpp + * Date: 26.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "ImageEditorEnvironment.h" +#include "controller/ImageEditorController.h" + +const char* ImageEditorEnvironment::Serialization_Group = "Environment"; + +const char* ImageEditorEnvironment::Serialization_Value_PrimaryColor = "PrimaryColor"; +const char* ImageEditorEnvironment::Serialization_Value_ShowDirectionArrows = "ShowDirectionArrows"; +const char* ImageEditorEnvironment::Serialization_Value_ShowTags = "ShowTags"; +const char* ImageEditorEnvironment::Serialization_Value_SnapToEdges = "SnapToEdges"; +const char* ImageEditorEnvironment::Serialization_Value_SamplingMode = "SamplingMode"; + +ImageEditorEnvironment::ImageEditorEnvironment(ImageEditorController* controller) : + _editorController{controller}, _selection{controller->imageEditor()} +{ + if (!controller) + throw std::invalid_argument{_EXCPT("controller may not be null")}; +} + +void ImageEditorEnvironment::setPrimaryColor(QColor color) +{ + if (color != _primaryColor) + { + _primaryColor = color; + emit primaryColorChanged(color); + } +} + +void ImageEditorEnvironment::setShowDirectionArrows(bool show) +{ + if (show != _showDirectionArrows) + { + _showDirectionArrows = show; + emit showDirectionArrowsChanged(show); + } +} + +void ImageEditorEnvironment::setShowTags(bool show) +{ + if (show != _showTags) + { + _showTags = show; + emit showTagsChanged(show); + } +} + +void ImageEditorEnvironment::setSnapToEdges(bool snap) +{ + if (snap != _snapToEdges) + { + _snapToEdges = snap; + emit snapToEdgesChanged(snap); + } +} + +void ImageEditorEnvironment::setSamplingMode(SamplingMode mode) +{ + if (mode != _samplingMode) + { + _samplingMode = mode; + emit samplingModeChanged(mode); + } +} + +void ImageEditorEnvironment::serialize(SerializationContext& ctx) const +{ + // Serialize values + ctx.settings()(Serialization_Value_PrimaryColor) = _primaryColor.name(); + ctx.settings()(Serialization_Value_ShowDirectionArrows) = _showDirectionArrows; + ctx.settings()(Serialization_Value_ShowTags) = _showTags; + ctx.settings()(Serialization_Value_SnapToEdges) = _snapToEdges; + ctx.settings()(Serialization_Value_SamplingMode) = static_cast<int>(_samplingMode); +} + +void ImageEditorEnvironment::deserialize(DeserializationContext& ctx) +{ + // Deserialize values + setPrimaryColor(QColor{ctx.settings()(Serialization_Value_PrimaryColor).toString()}); + setShowDirectionArrows(ctx.settings()(Serialization_Value_ShowDirectionArrows, true).toBool()); + setShowTags(ctx.settings()(Serialization_Value_ShowTags, true).toBool()); + setSnapToEdges(ctx.settings()(Serialization_Value_SnapToEdges, true).toBool()); + setSamplingMode(static_cast<SamplingMode>(ctx.settings()(Serialization_Value_SamplingMode, static_cast<int>(SamplingMode::Layer)).toInt())); +} diff --git a/Grinder/ui/image/ImageEditorEnvironment.h b/Grinder/ui/image/ImageEditorEnvironment.h index 478f5d7..f89e9d4 100644 --- a/Grinder/ui/image/ImageEditorEnvironment.h +++ b/Grinder/ui/image/ImageEditorEnvironment.h @@ -1,72 +1,86 @@ -/****************************************************************************** - * File: ImageEditorEnvironment.h - * Date: 26.3.2018 - *****************************************************************************/ - -#ifndef IMAGEEDITORENVIRONMENT_H -#define IMAGEEDITORENVIRONMENT_H - -#include <QColor> - -#include "ImageEditorSelection.h" -#include "common/serialization/SerializationContext.h" -#include "common/serialization/DeserializationContext.h" - -namespace grndr -{ - class ImageEditorController; - - class ImageEditorEnvironment : public QObject - { - Q_OBJECT - - public: - static const char* Serialization_Group; - - static const char* Serialization_Value_PrimaryColor; - static const char* Serialization_Value_ShowDirectionArrows; - static const char* Serialization_Value_ShowTags; - static const char* Serialization_Value_SnapToEdges; - - public: - ImageEditorEnvironment(ImageEditorController* controller); - - public: - ImageEditorSelection& selection() { return _selection; } - const ImageEditorSelection& selection() const { return _selection; } - - QColor getPrimaryColor() const { return _primaryColor; } - void setPrimaryColor(QColor color); - - bool showDirectionArrows() const { return _showDirectionArrows; } - void setShowDirectionArrows(bool show); - bool showTags() const { return _showTags; } - void setShowTags(bool show); - bool snapToEdges() const { return _snapToEdges; } - void setSnapToEdges(bool snap); - - public: - void serialize(SerializationContext& ctx) const; - void deserialize(DeserializationContext& ctx); - - signals: - void primaryColorChanged(QColor); - void showDirectionArrowsChanged(bool); - void showTagsChanged(bool); - void snapToEdgesChanged(bool); - - private: - ImageEditorController* _editorController{nullptr}; - - private: - ImageEditorSelection _selection; - - QColor _primaryColor{255, 255, 255}; - - bool _showDirectionArrows{true}; - bool _showTags{true}; - bool _snapToEdges{true}; - }; -} - -#endif +/****************************************************************************** + * File: ImageEditorEnvironment.h + * Date: 26.3.2018 + *****************************************************************************/ + +#ifndef IMAGEEDITORENVIRONMENT_H +#define IMAGEEDITORENVIRONMENT_H + +#include <QColor> + +#include "ImageEditorSelection.h" +#include "common/serialization/SerializationContext.h" +#include "common/serialization/DeserializationContext.h" + +namespace grndr +{ + class ImageEditorController; + + class ImageEditorEnvironment : public QObject + { + Q_OBJECT + + public: + static const char* Serialization_Group; + + static const char* Serialization_Value_PrimaryColor; + static const char* Serialization_Value_ShowDirectionArrows; + static const char* Serialization_Value_ShowTags; + static const char* Serialization_Value_SnapToEdges; + static const char* Serialization_Value_SamplingMode; + + public: + enum class SamplingMode + { + Image, + Layer, + }; + + public: + ImageEditorEnvironment(ImageEditorController* controller); + + public: + ImageEditorSelection& selection() { return _selection; } + const ImageEditorSelection& selection() const { return _selection; } + + QColor getPrimaryColor() const { return _primaryColor; } + void setPrimaryColor(QColor color); + + bool showDirectionArrows() const { return _showDirectionArrows; } + void setShowDirectionArrows(bool show); + bool showTags() const { return _showTags; } + void setShowTags(bool show); + bool snapToEdges() const { return _snapToEdges; } + void setSnapToEdges(bool snap); + + SamplingMode getSamplingMode() const { return _samplingMode; } + void setSamplingMode(SamplingMode mode); + + public: + void serialize(SerializationContext& ctx) const; + void deserialize(DeserializationContext& ctx); + + signals: + void primaryColorChanged(QColor); + void showDirectionArrowsChanged(bool); + void showTagsChanged(bool); + void snapToEdgesChanged(bool); + void samplingModeChanged(SamplingMode); + + private: + ImageEditorController* _editorController{nullptr}; + + private: + ImageEditorSelection _selection; + + QColor _primaryColor{255, 255, 255}; + + bool _showDirectionArrows{true}; + bool _showTags{true}; + bool _snapToEdges{true}; + + SamplingMode _samplingMode{SamplingMode::Layer}; + }; +} + +#endif diff --git a/Grinder/ui/image/ImageEditorSelection.cpp b/Grinder/ui/image/ImageEditorSelection.cpp index 9add74d..106a18b 100644 --- a/Grinder/ui/image/ImageEditorSelection.cpp +++ b/Grinder/ui/image/ImageEditorSelection.cpp @@ -95,7 +95,14 @@ void ImageEditorSelection::grabCutSelection(int iterations) { if (auto layer = _imageEditor->controller().activeLayer()) { - auto layerSize = layer->getLayerSize(); + cv::Mat pixelsMatrix; + + if (_imageEditor->environment().getSamplingMode() == ImageEditorEnvironment::SamplingMode::Image) + pixelsMatrix = _imageEditor->getDisplayedImage(); + else + pixelsMatrix = layer->layerPixels().data().toMatrix(); + + auto layerSize = QSize{pixelsMatrix.cols, pixelsMatrix.rows}; cv::Mat mask{layerSize.height(), layerSize.width(), CV_8UC1, cv::Scalar::all(cv::GC_BGD)}; for (int x = 0; x < mask.cols; ++x) @@ -108,7 +115,6 @@ void ImageEditorSelection::grabCutSelection(int iterations) } // Run the GrabCut algorithm - auto pixelsMatrix = layer->layerPixels().data().toMatrix(); cv::Mat backgroundModel = cv::Mat::zeros(1, 65, CV_64FC1); cv::Mat foregroundModel = cv::Mat::zeros(1, 65, CV_64FC1); diff --git a/Grinder/ui/image/ImageEditorTool.cpp b/Grinder/ui/image/ImageEditorTool.cpp index 31ca6a1..6dd889f 100644 --- a/Grinder/ui/image/ImageEditorTool.cpp +++ b/Grinder/ui/image/ImageEditorTool.cpp @@ -1,87 +1,87 @@ -/****************************************************************************** - * File: ImageEditorTool.cpp - * Date: 22.3.2018 - *****************************************************************************/ - -#include "Grinder.h" -#include "ImageEditorTool.h" -#include "ImageEditor.h" -#include "ui/visscene/RubberBandNode.h" - -const char* ImageEditorTool::Serialization_Value_Type = "Type"; - -ImageEditorTool::ImageEditorTool(ImageEditor* imageEditor, QString name, QString icon, QString shortcut, QCursor cursor) : ImageEditorComponent(imageEditor), - _name{name}, _icon{icon}, _shortcut{shortcut}, _cursor{cursor} -{ - if (!imageEditor) - throw std::invalid_argument{_EXCPT("imageEditor may not be null")}; -} - -void ImageEditorTool::initImageEditorTool() -{ - createProperties(); - updateProperties(); -} - -void ImageEditorTool::toolActivated(ImageEditorTool* prevTool) -{ - Q_UNUSED(prevTool); - - _isActive = true; - - if (_supportRightMouseButton && _imageEditor->controller().activeScene()) - { - // If the tool supports the right mouse button, we need to disable the view's context menu - _prevContextMenuPolicy = _imageEditor->controller().activeScene()->view()->contextMenuPolicy(); - _imageEditor->controller().activeScene()->view()->setContextMenuPolicy(Qt::NoContextMenu); - } -} - -void ImageEditorTool::toolDeactivated(ImageEditorTool* nextTool) -{ - Q_UNUSED(nextTool); - - _isActive = false; - - if (_supportRightMouseButton && _imageEditor->controller().activeScene()) - _imageEditor->controller().activeScene()->view()->setContextMenuPolicy(_prevContextMenuPolicy); - - removeRubberBand(); -} - -void ImageEditorTool::serialize(SerializationContext& ctx) const -{ - PropertyObject::serialize(ctx); - - // Serialize values - ctx.settings()(Serialization_Value_Type) = getToolType(); -} - -void ImageEditorTool::deserialize(DeserializationContext& ctx) -{ - PropertyObject::deserialize(ctx); -} - -void ImageEditorTool::createRubberBand(QPointF pos) -{ - if (!_rubberBandNode) - { - if (auto scene = _imageEditor->controller().activeScene()) - _rubberBandNode = new RubberBandNode{scene, pos}; - } -} - -void ImageEditorTool::removeRubberBand() -{ - if (_rubberBandNode) - { - delete _rubberBandNode; - _rubberBandNode = nullptr; - - if (auto scene = _imageEditor->controller().activeScene()) - { - scene->view()->setDragMode(QGraphicsView::ScrollHandDrag); - scene->view()->setDragMode(QGraphicsView::RubberBandDrag); - } - } -} +/****************************************************************************** + * File: ImageEditorTool.cpp + * Date: 22.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "ImageEditorTool.h" +#include "ImageEditor.h" +#include "ui/visscene/RubberBandNode.h" + +const char* ImageEditorTool::Serialization_Value_Type = "Type"; + +ImageEditorTool::ImageEditorTool(ImageEditor* imageEditor, QString name, QString icon, QString shortcut, QCursor cursor) : ImageEditorComponent(imageEditor), + _name{name}, _icon{icon}, _shortcut{shortcut}, _cursor{cursor} +{ + if (!imageEditor) + throw std::invalid_argument{_EXCPT("imageEditor may not be null")}; +} + +void ImageEditorTool::initImageEditorTool() +{ + createProperties(); + updateProperties(); +} + +void ImageEditorTool::toolActivated(ImageEditorTool* prevTool) +{ + Q_UNUSED(prevTool); + + _isActive = true; + + if (_supportRightMouseButton && _imageEditor->controller().activeScene()) + { + // If the tool supports the right mouse button, we need to disable the view's context menu + _prevContextMenuPolicy = _imageEditor->controller().activeScene()->view()->contextMenuPolicy(); + _imageEditor->controller().activeScene()->view()->setContextMenuPolicy(Qt::NoContextMenu); + } +} + +void ImageEditorTool::toolDeactivated(ImageEditorTool* nextTool) +{ + Q_UNUSED(nextTool); + + _isActive = false; + + if (_supportRightMouseButton && _imageEditor->controller().activeScene()) + _imageEditor->controller().activeScene()->view()->setContextMenuPolicy(_prevContextMenuPolicy); + + removeRubberBand(); +} + +void ImageEditorTool::serialize(SerializationContext& ctx) const +{ + PropertyObject::serialize(ctx); + + // Serialize values + ctx.settings()(Serialization_Value_Type) = getToolType(); +} + +void ImageEditorTool::deserialize(DeserializationContext& ctx) +{ + PropertyObject::deserialize(ctx); +} + +void ImageEditorTool::createRubberBand(QPointF pos) +{ + if (!_rubberBandNode) + { + if (auto scene = _imageEditor->controller().activeScene()) + _rubberBandNode = new RubberBandNode{scene, pos}; + } +} + +void ImageEditorTool::removeRubberBand() +{ + if (_rubberBandNode) + { + delete _rubberBandNode; + _rubberBandNode = nullptr; + + if (auto scene = _imageEditor->controller().activeScene()) + { + scene->view()->setDragMode(QGraphicsView::ScrollHandDrag); + scene->view()->setDragMode(QGraphicsView::RubberBandDrag); + } + } +} diff --git a/Grinder/ui/image/ImageEditorTool.h b/Grinder/ui/image/ImageEditorTool.h index 3c9747c..f9b0b6d 100644 --- a/Grinder/ui/image/ImageEditorTool.h +++ b/Grinder/ui/image/ImageEditorTool.h @@ -1,84 +1,84 @@ -/****************************************************************************** - * File: ImageEditorTool.h - * Date: 22.3.2018 - *****************************************************************************/ - -#ifndef IMAGEEDITORTOOL_H -#define IMAGEEDITORTOOL_H - -#include <QIcon> -#include <QCursor> -#include <QKeySequence> - -#include "common/properties/PropertyObject.h" -#include "ui/visscene/VisualSceneInputHandler.h" -#include "ImageEditorComponent.h" - -namespace grndr -{ - class ImageEditor; - class RubberBandNode; - - class ImageEditorTool : public PropertyObject, public ImageEditorComponent, public VisualSceneInputHandler - { - Q_OBJECT - - public: - static const char* Serialization_Value_Type; - - public: - ImageEditorTool(ImageEditor* imageEditor, QString name, QString icon, QString shortcut, QCursor cursor = QCursor{Qt::ArrowCursor}); - - public: - void initImageEditorTool(); - - public: - virtual void toolActivated(ImageEditorTool* prevTool); - virtual void toolDeactivated(ImageEditorTool* nextTool); - - public: - virtual void serialize(SerializationContext& ctx) const override; - virtual void deserialize(DeserializationContext& ctx) override; - - public: - virtual QString getToolType() const = 0; - - QString getName() const { return _name; } - QIcon getIcon() const { return _icon; } - QString getShortcut() const { return _shortcut; } - bool isActionShortcut() const { return _isActionShortcut; } - QCursor getCursor() const { return _cursor; } - - bool isActive() const { return _isActive; } - - virtual PropertyObject* getExternalProperties() const { return nullptr; } - - signals: - void changeCursor(const QCursor&); - - void externalPropertiesInvalidated(const PropertyObject*); - - protected: - void createRubberBand(QPointF pos); - void removeRubberBand(); - - protected: - QString _name{""}; - QIcon _icon; - QString _shortcut; - bool _isActionShortcut{true}; - QCursor _cursor; - - bool _supportRightMouseButton{false}; - - bool _isActive{false}; - - protected: - RubberBandNode* _rubberBandNode{nullptr}; - - private: - Qt::ContextMenuPolicy _prevContextMenuPolicy; - }; -} - -#endif +/****************************************************************************** + * File: ImageEditorTool.h + * Date: 22.3.2018 + *****************************************************************************/ + +#ifndef IMAGEEDITORTOOL_H +#define IMAGEEDITORTOOL_H + +#include <QIcon> +#include <QCursor> +#include <QKeySequence> + +#include "common/properties/PropertyObject.h" +#include "ui/visscene/VisualSceneInputHandler.h" +#include "ImageEditorComponent.h" + +namespace grndr +{ + class ImageEditor; + class RubberBandNode; + + class ImageEditorTool : public PropertyObject, public ImageEditorComponent, public VisualSceneInputHandler + { + Q_OBJECT + + public: + static const char* Serialization_Value_Type; + + public: + ImageEditorTool(ImageEditor* imageEditor, QString name, QString icon, QString shortcut, QCursor cursor = QCursor{Qt::ArrowCursor}); + + public: + void initImageEditorTool(); + + public: + virtual void toolActivated(ImageEditorTool* prevTool); + virtual void toolDeactivated(ImageEditorTool* nextTool); + + public: + virtual void serialize(SerializationContext& ctx) const override; + virtual void deserialize(DeserializationContext& ctx) override; + + public: + virtual QString getToolType() const = 0; + + QString getName() const { return _name; } + QIcon getIcon() const { return _icon; } + QString getShortcut() const { return _shortcut; } + bool isActionShortcut() const { return _isActionShortcut; } + QCursor getCursor() const { return _cursor; } + + bool isActive() const { return _isActive; } + + virtual PropertyObject* getExternalProperties() const { return nullptr; } + + signals: + void changeCursor(const QCursor&); + + void externalPropertiesInvalidated(const PropertyObject*); + + protected: + void createRubberBand(QPointF pos); + void removeRubberBand(); + + protected: + QString _name{""}; + QIcon _icon; + QString _shortcut; + bool _isActionShortcut{true}; + QCursor _cursor; + + bool _supportRightMouseButton{false}; + + bool _isActive{false}; + + protected: + RubberBandNode* _rubberBandNode{nullptr}; + + private: + Qt::ContextMenuPolicy _prevContextMenuPolicy; + }; +} + +#endif diff --git a/Grinder/ui/image/ImageEditorToolList.cpp b/Grinder/ui/image/ImageEditorToolList.cpp index 7336363..bb459c9 100644 --- a/Grinder/ui/image/ImageEditorToolList.cpp +++ b/Grinder/ui/image/ImageEditorToolList.cpp @@ -32,6 +32,7 @@ ImageEditorToolList::ImageEditorToolList(ImageEditor* imageEditor) : ImageEditor _tools.reserve(30); // Reserve enough space so that no resizing occurs when adding all tools (can be problematic under Linux) _tools.push_back(ToolEntry{createTool<DefaultImageEditorTool>(imageEditor), nullptr}); _tools.push_back(ToolEntry{createTool<ColorPickerTool>(imageEditor), nullptr}); + _tools.push_back(ToolEntry{}); _tools.push_back(ToolEntry{createTool<BoxDraftItemTool>(imageEditor), nullptr}); _tools.push_back(ToolEntry{createTool<EllipseDraftItemTool>(imageEditor), nullptr}); _tools.push_back(ToolEntry{createTool<LineDraftItemTool>(imageEditor), nullptr}); diff --git a/Grinder/ui/image/ImageEditorView.cpp b/Grinder/ui/image/ImageEditorView.cpp index 2f0b2cd..7fc3b10 100644 --- a/Grinder/ui/image/ImageEditorView.cpp +++ b/Grinder/ui/image/ImageEditorView.cpp @@ -1,334 +1,418 @@ -/****************************************************************************** - * File: ImageEditorView.cpp - * Date: 15.3.2018 - *****************************************************************************/ - -#include "Grinder.h" -#include "ImageEditorView.h" -#include "ImageEditorScene.h" -#include "ImageEditor.h" -#include "DraftItemNode.h" -#include "core/GrinderApplication.h" -#include "controller/ImageEditorController.h" -#include "ui/UIUtils.h" -#include "res/Resources.h" - -ImageEditorView::ImageEditorView(QWidget* parent) : VisualSceneView(parent) -{ - // Set the background of the view to a checkerboard pattern - setBackgroundBrush(QPixmap{FILE_ICON_EDITOR_BACKGROUND}); - - // Set widget border - setStyleSheet(QString{"QGraphicsView { border: 1px solid %1; }"}.arg(QPalette{}.color(QPalette::Dark).name())); - - // Create view actions - _showDirectionsAction = UIUtils::createAction(this, "&Show direction arrows", FILE_ICON_EDITOR_SHOWDIRECTIONS, SLOT(showDirectionArrows()), "Show direction arrows on items"); - _showDirectionsAction->setCheckable(true); - _showDirectionsAction->setChecked(true); - _showTagsAction = UIUtils::createAction(this, "&Show tags", FILE_ICON_EDITOR_SHOWTAGS, SLOT(showTags()), "Show tags on items"); - _showTagsAction->setCheckable(true); - _showTagsAction->setChecked(true); - _snapToEdgesAction = UIUtils::createAction(this, "Sna&p to edges", FILE_ICON_EDITOR_SNAPTOEDGES, SLOT(snapToEdges()), "Snap items to the image edges"); - _snapToEdgesAction->setCheckable(true); - _snapToEdgesAction->setChecked(true); - - _zoomFitAction = UIUtils::createAction(this, "Zoom to &window", FILE_ICON_ZOOMFIT, SLOT(fitImageToWindow()), "Zoom the view to fit the image to its window", "Ctrl+Alt+A"); - - _copyDraftItems = UIUtils::createAction(this, "&Copy item(s)", FILE_ICON_COPY, SLOT(copyDraftItems()), "Copy the selected items to the clipboard"); - _copyDraftItems->setShortcut(QKeySequence{QKeySequence::Copy}); - _pasteDraftItems = UIUtils::createAction(this, "&Paste item(s)", FILE_ICON_PASTE, SLOT(pasteDraftItems()), "Paste items from the clipboard"); - _pasteDraftItems->setShortcut(QKeySequence{QKeySequence::Paste}); - _cutDraftItems = UIUtils::createAction(this, "Cu&t item(s)", FILE_ICON_CUT, SLOT(cutDraftItems()), "Copy the selected items to the clipboard and remove them afterwards"); - _cutDraftItems->setShortcut(QKeySequence{QKeySequence::Cut}); - _convertDraftItemsAction = UIUtils::createAction(this, "&Convert to pixels", FILE_ICON_EDITOR_CONVERTTOPIXELS, SLOT(convertDraftItems()), "Converts the selected items to pixels", "Return"); - - // Listen for clipboard changes to update our actions - connect(&grinder()->clipboardManager(), &ClipboardManager::dataChanged, this, &ImageEditorView::updateActions); - - updateActions(); -} - -void ImageEditorView::setEditorScene(ImageEditorScene* scene) -{ - if (_editorScene) - { - disconnect(_editorScene, nullptr, this, nullptr); - disconnect(&_editorScene->imageEditor()->editorTools(), nullptr, this, nullptr); - disconnect(&_editorScene->imageEditor()->environment(), nullptr, this, nullptr); - } - - _editorScene = scene; - VisualSceneView::setScene(scene); - - if (_editorScene) - { - // Update the view's cursor when the active draft item in-place editor has changed - connect(_editorScene, &ImageEditorScene::draftItemInPlaceEditorActivated, this, &ImageEditorView::draftItemInPlaceEditorActivated); - connect(_editorScene, &ImageEditorScene::draftItemInPlaceEditorDeactivated, this, &ImageEditorView::draftItemInPlaceEditorDeactivated); - - // Update the view's cursor when the active image editor tool has changed - connect(&_editorScene->imageEditor()->editorTools(), &ImageEditorToolList::activeToolChanging, this, &ImageEditorView::imageEditorToolChanging); - connect(&_editorScene->imageEditor()->editorTools(), &ImageEditorToolList::activeToolChanged, this, &ImageEditorView::imageEditorToolChanged); - - // Update the view's actions when the image editor environment has changed - connect(&_editorScene->imageEditor()->environment(), &ImageEditorEnvironment::showDirectionArrowsChanged, this, &ImageEditorView::showDirectionArrowsChanged); - connect(&_editorScene->imageEditor()->environment(), &ImageEditorEnvironment::showTagsChanged, this, &ImageEditorView::showTagsChanged); - connect(&_editorScene->imageEditor()->environment(), &ImageEditorEnvironment::snapToEdgesChanged, this, &ImageEditorView::snapToEdgesChanged); - } -} - -void ImageEditorView::removeSelectedItems() const -{ - if (_editorScene) - _editorScene->imageEditor()->controller().removeSelectedNodes(); -} - -void ImageEditorView::resizeSelectedItems(QSize delta) -{ - if (_editorScene) - { - for (auto item : _editorScene->getNodes<DraftItemNode>(true)) - { - if (auto draftItem = item->draftItem().lock()) // Make sure the underlying draft item still exists - { - if (auto sizeProperty = draftItem->properties().property<SizeProperty>(PropertyID::Size)) - sizeProperty->setValue(*sizeProperty + delta); - } - } - } -} - -void ImageEditorView::prepareNodeContextMenu(QMenu& menu) const -{ - QAction* firstAction = !menu.actions().isEmpty() ? menu.actions().first() : nullptr; - - menu.insertAction(firstAction, _cutDraftItems); - menu.insertAction(firstAction, _copyDraftItems); - menu.insertAction(firstAction, _pasteDraftItems); - menu.insertSeparator(firstAction); - menu.insertAction(firstAction, _convertDraftItemsAction); -} - -void ImageEditorView::prepareNodesContextMenu(QMenu& menu) const -{ - prepareNodeContextMenu(menu); -} - -std::vector<QAction*> ImageEditorView::getActions(AddActionsMode mode) const -{ - std::vector<QAction*> actions; - - if (mode != AddActionsMode::Toolbar) - { - actions.push_back(_cutDraftItems); - actions.push_back(_copyDraftItems); - actions.push_back(_pasteDraftItems); - actions.push_back(nullptr); - - actions.push_back(_selectAllAction); - actions.push_back(_deleteSelectedAction); - actions.push_back(nullptr); - } - - actions.push_back(_snapToEdgesAction); - actions.push_back(nullptr); - actions.push_back(_showDirectionsAction); - actions.push_back(_showTagsAction); - actions.push_back(nullptr); - actions.push_back(_zoomInAction); - actions.push_back(_zoomOutAction); - actions.push_back(_zoomFullAction); - actions.push_back(_zoomFitAction); - - return actions; -} - -void ImageEditorView::drawBackground(QPainter* painter, const QRectF& rect) -{ - // Draw scale-invariant background - auto scaleFactor = painter->transform().m11(); - auto bgRect = rect; - - bgRect.setTopLeft(bgRect.topLeft() * scaleFactor); - bgRect.setBottomRight(bgRect.bottomRight() * scaleFactor); - - painter->scale(1.0 / scaleFactor, 1.0 / scaleFactor); - painter->fillRect(bgRect, backgroundBrush()); - painter->scale(scaleFactor, scaleFactor); -} - -void ImageEditorView::keyPressEvent(QKeyEvent* event) -{ - if (dynamic_cast<QGraphicsProxyWidget*>(_scene ? _scene->focusItem() : nullptr) == nullptr) // If a widget inside the scene has the input focus, do not process key events here - { - if ((event->key() == Qt::Key_Up || event->key() == Qt::Key_Down || event->key() == Qt::Key_Left || event->key() == Qt::Key_Right) && (event->modifiers() == Qt::ShiftModifier)) // Enable item resizing via cursor keys - { - QSize delta{0, 0}; - int offset = 1; - - switch (event->key()) - { - case Qt::Key_Left: - delta.setWidth(-offset); - break; - - case Qt::Key_Right: - delta.setWidth(offset); - break; - - case Qt::Key_Up: - delta.setHeight(-offset); - break; - - case Qt::Key_Down: - delta.setHeight(offset); - break; - - default: - break; - } - - if (!delta.isNull()) - { - resizeSelectedItems(delta); - return; - } - } - } - - VisualSceneView::keyPressEvent(event); -} - -void ImageEditorView::updateActions() -{ - VisualSceneView::updateActions(); - - bool draftItemSelected = false; - bool editableItemSelected = false; - bool activeLayerEditable = false; - - if (_editorScene) - { - auto selectedNodes = _editorScene->getNodes<DraftItemNode>(true); - draftItemSelected = !selectedNodes.empty(); - - for (const auto& selectedNode : selectedNodes) - { - if (!selectedNode->isLocked()) - { - editableItemSelected = true; - break; - } - } - - auto activeLayer = _editorScene->imageEditor()->controller().activeLayer(); - activeLayerEditable = activeLayer ? !activeLayer->hasFlag(Layer::Flag::Locked) : false; - } - - _zoomFitAction->setEnabled(_scene); - - _copyDraftItems->setEnabled(_scene && draftItemSelected); - _pasteDraftItems->setEnabled(_scene && grinder()->clipboardManager().hasData(DraftItemVector::Serialization_Element) && activeLayerEditable); - _cutDraftItems->setEnabled(_scene && draftItemSelected && editableItemSelected); - _convertDraftItemsAction->setEnabled(_scene && draftItemSelected && editableItemSelected); - _deleteSelectedAction->setEnabled(_deleteSelectedAction->isEnabled() && draftItemSelected && editableItemSelected); -} - -void ImageEditorView::updateCursor(const QCursor& cursor) -{ - setCursor(cursor); - viewport()->setCursor(cursor); -} - -void ImageEditorView::showDirectionArrows() -{ - if (_editorScene) - _editorScene->imageEditor()->environment().setShowDirectionArrows(_showDirectionsAction->isChecked()); -} - -void ImageEditorView::showTags() -{ - if (_editorScene) - _editorScene->imageEditor()->environment().setShowTags(_showTagsAction->isChecked()); -} - -void ImageEditorView::snapToEdges() -{ - if (_editorScene) - _editorScene->imageEditor()->environment().setSnapToEdges(_snapToEdgesAction->isChecked()); -} - -void ImageEditorView::fitImageToWindow() -{ - if (_editorScene) - _editorScene->fitViewToImage(); -} - -void ImageEditorView::imageEditorToolChanging(ImageEditorTool* oldTool, ImageEditorTool* newTool) -{ - if (oldTool) - disconnect(oldTool, &ImageEditorTool::changeCursor, this, &ImageEditorView::updateCursor); - - if (newTool) - connect(newTool, &ImageEditorTool::changeCursor, this, &ImageEditorView::updateCursor); -} - -void ImageEditorView::imageEditorToolChanged(ImageEditorTool* tool) -{ - updateCursor(tool->getCursor()); - - if (_editorScene) - _editorScene->clearSelection(); -} - -void ImageEditorView::draftItemInPlaceEditorActivated(InPlaceEditorBase* editor) -{ - connect(editor, &InPlaceEditorBase::changeCursor, this, &ImageEditorView::updateCursor); -} - -void ImageEditorView::draftItemInPlaceEditorDeactivated(InPlaceEditorBase* editor) -{ - disconnect(editor, &InPlaceEditorBase::changeCursor, this, &ImageEditorView::updateCursor); -} - -void ImageEditorView::showDirectionArrowsChanged(bool show) -{ - _showDirectionsAction->setChecked(show); - update(); -} - -void ImageEditorView::showTagsChanged(bool show) -{ - _showTagsAction->setChecked(show); - update(); -} - -void ImageEditorView::snapToEdgesChanged(bool snap) -{ - _snapToEdgesAction->setChecked(snap); - update(); -} - -void ImageEditorView::copyDraftItems() const -{ - if (_editorScene) - _editorScene->imageEditor()->controller().copySelectedNodes(); -} - -void ImageEditorView::pasteDraftItems() const -{ - if (_editorScene) - _editorScene->imageEditor()->controller().pasteSelectedNodes(); -} - -void ImageEditorView::cutDraftItems() const -{ - if (_editorScene) - _editorScene->imageEditor()->controller().cutSelectedNodes(); -} - -void ImageEditorView::convertDraftItems() const -{ - if (_editorScene) - _editorScene->imageEditor()->controller().convertSelectedNodesToPixels(); -} +/****************************************************************************** + * File: ImageEditorView.cpp + * Date: 15.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "ImageEditorView.h" +#include "ImageEditorScene.h" +#include "ImageEditor.h" +#include "DraftItemNode.h" +#include "core/GrinderApplication.h" +#include "controller/ImageEditorController.h" +#include "ui/UIUtils.h" +#include "res/Resources.h" + +ImageEditorView::ImageEditorView(QWidget* parent) : VisualSceneView(parent) +{ + // Set the background of the view to a checkerboard pattern + setBackgroundBrush(QPixmap{FILE_ICON_EDITOR_BACKGROUND}); + + // Set widget border + setStyleSheet(QString{"QGraphicsView { border: 1px solid %1; }"}.arg(QPalette{}.color(QPalette::Dark).name())); + + // Create the sampling mode menu + QMenu* samplingModeMenu = new QMenu{this}; + _samplingModeLayerAction = samplingModeMenu->addAction(QIcon{FILE_ICON_LAYER}, "Layer sampling", this, SLOT(selectLayerSamplingMode())); + _samplingModeLayerAction->setCheckable(true); + _samplingModeImageAction = samplingModeMenu->addAction(QIcon{FILE_ICON_EDITOR_SAMPLING_IMAGE}, "Image sampling", this, SLOT(selectImageSamplingMode())); + _samplingModeImageAction->setCheckable(true); + + // Create view actions + _showDirectionsAction = UIUtils::createAction(this, "&Show direction arrows", FILE_ICON_EDITOR_SHOWDIRECTIONS, SLOT(showDirectionArrows()), "Show direction arrows on items"); + _showDirectionsAction->setCheckable(true); + _showDirectionsAction->setChecked(true); + _showTagsAction = UIUtils::createAction(this, "&Show tags", FILE_ICON_EDITOR_SHOWTAGS, SLOT(showTags()), "Show tags on items"); + _showTagsAction->setCheckable(true); + _showTagsAction->setChecked(true); + _snapToEdgesAction = UIUtils::createAction(this, "Sna&p to edges", FILE_ICON_EDITOR_SNAPTOEDGES, SLOT(snapToEdges()), "Snap items to the image edges"); + _snapToEdgesAction->setCheckable(true); + _snapToEdgesAction->setChecked(true); + + _zoomFitAction = UIUtils::createAction(this, "Zoom to &window", FILE_ICON_ZOOMFIT, SLOT(fitImageToWindow()), "Zoom the view to fit the image to its window", "Ctrl+Alt+A"); + + _samplingModeAction = UIUtils::createAction(this, "Sampling mode", "", SLOT(toggleSamplingMode()), "Select the sampling mode", "Ctrl+M"); + _samplingModeAction->setMenu(samplingModeMenu); + + _copyDraftItems = UIUtils::createAction(this, "&Copy item(s)", FILE_ICON_COPY, SLOT(copyDraftItems()), "Copy the selected items to the clipboard"); + _copyDraftItems->setShortcut(QKeySequence{QKeySequence::Copy}); + _pasteDraftItems = UIUtils::createAction(this, "&Paste item(s)", FILE_ICON_PASTE, SLOT(pasteDraftItems()), "Paste items from the clipboard"); + _pasteDraftItems->setShortcut(QKeySequence{QKeySequence::Paste}); + _cutDraftItems = UIUtils::createAction(this, "Cu&t item(s)", FILE_ICON_CUT, SLOT(cutDraftItems()), "Copy the selected items to the clipboard and remove them afterwards"); + _cutDraftItems->setShortcut(QKeySequence{QKeySequence::Cut}); + _convertDraftItemsAction = UIUtils::createAction(this, "&Convert to pixels", FILE_ICON_EDITOR_CONVERTTOPIXELS, SLOT(convertDraftItems()), "Converts the selected items to pixels", "Return"); + + // Listen for clipboard changes to update our actions + connect(&grinder()->clipboardManager(), &ClipboardManager::dataChanged, this, &ImageEditorView::updateActions); + + updateActions(); +} + +void ImageEditorView::setEditorScene(ImageEditorScene* scene) +{ + if (_editorScene) + { + disconnect(_editorScene, nullptr, this, nullptr); + disconnect(&_editorScene->imageEditor()->editorTools(), nullptr, this, nullptr); + disconnect(&_editorScene->imageEditor()->environment(), nullptr, this, nullptr); + } + + _editorScene = scene; + VisualSceneView::setScene(scene); + + if (_editorScene) + { + // Update the view's cursor when the active draft item in-place editor has changed + connect(_editorScene, &ImageEditorScene::draftItemInPlaceEditorActivated, this, &ImageEditorView::draftItemInPlaceEditorActivated); + connect(_editorScene, &ImageEditorScene::draftItemInPlaceEditorDeactivated, this, &ImageEditorView::draftItemInPlaceEditorDeactivated); + + // Update the view's cursor when the active image editor tool has changed + connect(&_editorScene->imageEditor()->editorTools(), &ImageEditorToolList::activeToolChanging, this, &ImageEditorView::imageEditorToolChanging); + connect(&_editorScene->imageEditor()->editorTools(), &ImageEditorToolList::activeToolChanged, this, &ImageEditorView::imageEditorToolChanged); + + // Update the view's actions when the image editor environment has changed + connect(&_editorScene->imageEditor()->environment(), &ImageEditorEnvironment::showDirectionArrowsChanged, this, &ImageEditorView::showDirectionArrowsChanged); + connect(&_editorScene->imageEditor()->environment(), &ImageEditorEnvironment::showTagsChanged, this, &ImageEditorView::showTagsChanged); + connect(&_editorScene->imageEditor()->environment(), &ImageEditorEnvironment::snapToEdgesChanged, this, &ImageEditorView::snapToEdgesChanged); + connect(&_editorScene->imageEditor()->environment(), &ImageEditorEnvironment::samplingModeChanged, this, &ImageEditorView::samplingModeChanged); + } +} + +void ImageEditorView::removeSelectedItems() const +{ + if (_editorScene) + _editorScene->imageEditor()->controller().removeSelectedNodes(); +} + +void ImageEditorView::resizeSelectedItems(QSize delta) +{ + if (_editorScene) + { + for (auto item : _editorScene->getNodes<DraftItemNode>(true)) + { + if (auto draftItem = item->draftItem().lock()) // Make sure the underlying draft item still exists + { + if (auto sizeProperty = draftItem->properties().property<SizeProperty>(PropertyID::Size)) + sizeProperty->setValue(*sizeProperty + delta); + } + } + } +} + +void ImageEditorView::prepareNodeContextMenu(QMenu& menu) const +{ + QAction* firstAction = !menu.actions().isEmpty() ? menu.actions().first() : nullptr; + + menu.insertAction(firstAction, _cutDraftItems); + menu.insertAction(firstAction, _copyDraftItems); + menu.insertAction(firstAction, _pasteDraftItems); + menu.insertSeparator(firstAction); + menu.insertAction(firstAction, _convertDraftItemsAction); +} + +void ImageEditorView::prepareNodesContextMenu(QMenu& menu) const +{ + prepareNodeContextMenu(menu); +} + +std::vector<QAction*> ImageEditorView::getActions(AddActionsMode mode) const +{ + std::vector<QAction*> actions; + + if (mode != AddActionsMode::Toolbar) + { + actions.push_back(_cutDraftItems); + actions.push_back(_copyDraftItems); + actions.push_back(_pasteDraftItems); + actions.push_back(nullptr); + + actions.push_back(_selectAllAction); + actions.push_back(_deleteSelectedAction); + actions.push_back(nullptr); + } + + actions.push_back(_samplingModeAction); + actions.push_back(nullptr); + actions.push_back(_snapToEdgesAction); + actions.push_back(nullptr); + actions.push_back(_showDirectionsAction); + actions.push_back(_showTagsAction); + actions.push_back(nullptr); + actions.push_back(_zoomInAction); + actions.push_back(_zoomOutAction); + actions.push_back(_zoomFullAction); + actions.push_back(_zoomFitAction); + + return actions; +} + +void ImageEditorView::drawBackground(QPainter* painter, const QRectF& rect) +{ + // Draw scale-invariant background + auto scaleFactor = painter->transform().m11(); + auto bgRect = rect; + + bgRect.setTopLeft(bgRect.topLeft() * scaleFactor); + bgRect.setBottomRight(bgRect.bottomRight() * scaleFactor); + + painter->scale(1.0 / scaleFactor, 1.0 / scaleFactor); + painter->fillRect(bgRect, backgroundBrush()); + painter->scale(scaleFactor, scaleFactor); +} + +void ImageEditorView::keyPressEvent(QKeyEvent* event) +{ + if (dynamic_cast<QGraphicsProxyWidget*>(_scene ? _scene->focusItem() : nullptr) == nullptr) // If a widget inside the scene has the input focus, do not process key events here + { + if ((event->key() == Qt::Key_Up || event->key() == Qt::Key_Down || event->key() == Qt::Key_Left || event->key() == Qt::Key_Right) && (event->modifiers() == Qt::ShiftModifier)) // Enable item resizing via cursor keys + { + QSize delta{0, 0}; + int offset = 1; + + switch (event->key()) + { + case Qt::Key_Left: + delta.setWidth(-offset); + break; + + case Qt::Key_Right: + delta.setWidth(offset); + break; + + case Qt::Key_Up: + delta.setHeight(-offset); + break; + + case Qt::Key_Down: + delta.setHeight(offset); + break; + + default: + break; + } + + if (!delta.isNull()) + { + resizeSelectedItems(delta); + return; + } + } + } + + VisualSceneView::keyPressEvent(event); +} + +void ImageEditorView::updateActions() +{ + VisualSceneView::updateActions(); + + bool draftItemSelected = false; + bool editableItemSelected = false; + bool activeLayerEditable = false; + + if (_editorScene) + { + auto selectedNodes = _editorScene->getNodes<DraftItemNode>(true); + draftItemSelected = !selectedNodes.empty(); + + for (const auto& selectedNode : selectedNodes) + { + if (!selectedNode->isLocked()) + { + editableItemSelected = true; + break; + } + } + + auto activeLayer = _editorScene->imageEditor()->controller().activeLayer(); + activeLayerEditable = activeLayer ? !activeLayer->hasFlag(Layer::Flag::Locked) : false; + } + + _zoomFitAction->setEnabled(_scene); + + _copyDraftItems->setEnabled(_scene && draftItemSelected); + _pasteDraftItems->setEnabled(_scene && grinder()->clipboardManager().hasData(DraftItemVector::Serialization_Element) && activeLayerEditable); + _cutDraftItems->setEnabled(_scene && draftItemSelected && editableItemSelected); + _convertDraftItemsAction->setEnabled(_scene && draftItemSelected && editableItemSelected); + _deleteSelectedAction->setEnabled(_deleteSelectedAction->isEnabled() && draftItemSelected && editableItemSelected); + + updateSamplingMode(); +} + +void ImageEditorView::updateSamplingMode() +{ + if (_editorScene) + { + auto modifyActions = [this](QString text, QString icon, bool layerChecked, bool imageChecked) { + _samplingModeAction->setText(text); + _samplingModeAction->setToolTip(text); + _samplingModeAction->setIcon(QIcon{icon}); + + _samplingModeLayerAction->setChecked(layerChecked); + _samplingModeImageAction->setChecked(imageChecked); + }; + + switch (_editorScene->imageEditor()->environment().getSamplingMode()) + { + case ImageEditorEnvironment::SamplingMode::Layer: + modifyActions("Sampling mode: Layer", FILE_ICON_LAYER, true, false); + break; + + case ImageEditorEnvironment::SamplingMode::Image: + modifyActions("Sampling mode: Image", FILE_ICON_EDITOR_SAMPLING_IMAGE, false, true); + break; + } + } +} + +void ImageEditorView::updateCursor(const QCursor& cursor) +{ + setCursor(cursor); + viewport()->setCursor(cursor); +} + +void ImageEditorView::showDirectionArrows() +{ + if (_editorScene) + _editorScene->imageEditor()->environment().setShowDirectionArrows(_showDirectionsAction->isChecked()); +} + +void ImageEditorView::showTags() +{ + if (_editorScene) + _editorScene->imageEditor()->environment().setShowTags(_showTagsAction->isChecked()); +} + +void ImageEditorView::snapToEdges() +{ + if (_editorScene) + _editorScene->imageEditor()->environment().setSnapToEdges(_snapToEdgesAction->isChecked()); +} + +void ImageEditorView::fitImageToWindow() +{ + if (_editorScene) + _editorScene->fitViewToImage(); +} + +void ImageEditorView::toggleSamplingMode() +{ + if (_editorScene) + { + switch (_editorScene->imageEditor()->environment().getSamplingMode()) + { + case ImageEditorEnvironment::SamplingMode::Layer: + selectImageSamplingMode(); + break; + + case ImageEditorEnvironment::SamplingMode::Image: + selectLayerSamplingMode(); + break; + } + } +} + +void ImageEditorView::selectLayerSamplingMode() +{ + if (_editorScene) + { + _editorScene->imageEditor()->environment().setSamplingMode(ImageEditorEnvironment::SamplingMode::Layer); + updateSamplingMode(); + } +} + +void ImageEditorView::selectImageSamplingMode() +{ + if (_editorScene) + { + _editorScene->imageEditor()->environment().setSamplingMode(ImageEditorEnvironment::SamplingMode::Image); + updateSamplingMode(); + } +} + +void ImageEditorView::imageEditorToolChanging(ImageEditorTool* oldTool, ImageEditorTool* newTool) +{ + if (oldTool) + disconnect(oldTool, &ImageEditorTool::changeCursor, this, &ImageEditorView::updateCursor); + + if (newTool) + connect(newTool, &ImageEditorTool::changeCursor, this, &ImageEditorView::updateCursor); +} + +void ImageEditorView::imageEditorToolChanged(ImageEditorTool* tool) +{ + updateCursor(tool->getCursor()); + + if (_editorScene) + _editorScene->clearSelection(); +} + +void ImageEditorView::draftItemInPlaceEditorActivated(InPlaceEditorBase* editor) +{ + connect(editor, &InPlaceEditorBase::changeCursor, this, &ImageEditorView::updateCursor); +} + +void ImageEditorView::draftItemInPlaceEditorDeactivated(InPlaceEditorBase* editor) +{ + disconnect(editor, &InPlaceEditorBase::changeCursor, this, &ImageEditorView::updateCursor); +} + +void ImageEditorView::showDirectionArrowsChanged(bool show) +{ + _showDirectionsAction->setChecked(show); + update(); +} + +void ImageEditorView::showTagsChanged(bool show) +{ + _showTagsAction->setChecked(show); + update(); +} + +void ImageEditorView::snapToEdgesChanged(bool snap) +{ + _snapToEdgesAction->setChecked(snap); + update(); +} + +void ImageEditorView::samplingModeChanged(ImageEditorEnvironment::SamplingMode mode) +{ + Q_UNUSED(mode); + + updateSamplingMode(); + update(); +} + +void ImageEditorView::copyDraftItems() const +{ + if (_editorScene) + _editorScene->imageEditor()->controller().copySelectedNodes(); +} + +void ImageEditorView::pasteDraftItems() const +{ + if (_editorScene) + _editorScene->imageEditor()->controller().pasteSelectedNodes(); +} + +void ImageEditorView::cutDraftItems() const +{ + if (_editorScene) + _editorScene->imageEditor()->controller().cutSelectedNodes(); +} + +void ImageEditorView::convertDraftItems() const +{ + if (_editorScene) + _editorScene->imageEditor()->controller().convertSelectedNodesToPixels(); +} diff --git a/Grinder/ui/image/ImageEditorView.h b/Grinder/ui/image/ImageEditorView.h index 62b9163..f77e39d 100644 --- a/Grinder/ui/image/ImageEditorView.h +++ b/Grinder/ui/image/ImageEditorView.h @@ -1,93 +1,104 @@ -/****************************************************************************** - * File: ImageEditorView.h - * Date: 15.3.2018 - *****************************************************************************/ - -#ifndef IMAGEEDITORVIEW_H -#define IMAGEEDITORVIEW_H - -#include "ui/visscene/VisualSceneView.h" -#include "ImageEditorStyle.h" - -namespace grndr -{ - class ImageEditorScene; - class ImageEditorTool; - class InPlaceEditorBase; - - class ImageEditorView : public VisualSceneView - { - Q_OBJECT - - public: - ImageEditorView(QWidget *parent = nullptr); - - public: - ImageEditorScene* editorScene() { return _editorScene; } - const ImageEditorScene* editorScene() const { return _editorScene; } - void setEditorScene(ImageEditorScene* scene); - - public slots: - virtual void removeSelectedItems() const override; - virtual void resizeSelectedItems(QSize delta); - - public: - virtual const ImageEditorStyle& sceneStyle() const override { return ImageEditorStyle::style(); } - - virtual void prepareNodeContextMenu(QMenu& menu) const override; - virtual void prepareNodesContextMenu(QMenu& menu) const override; - - protected: - virtual std::vector<QAction*> getActions(AddActionsMode mode) const override; - - protected: - virtual void drawBackground(QPainter* painter, const QRectF& rect) override; - - virtual void keyPressEvent(QKeyEvent* event) override; - - protected slots: - virtual void updateActions() override; - - private: - void updateCursor(const QCursor& cursor); - - private slots: - void showDirectionArrows(); - void showTags(); - void snapToEdges(); - - void fitImageToWindow(); - - void imageEditorToolChanging(ImageEditorTool* oldTool, ImageEditorTool* newTool); - void imageEditorToolChanged(ImageEditorTool* tool); - - void draftItemInPlaceEditorActivated(InPlaceEditorBase* editor); - void draftItemInPlaceEditorDeactivated(InPlaceEditorBase* editor); - - void showDirectionArrowsChanged(bool show); - void showTagsChanged(bool show); - void snapToEdgesChanged(bool snap); - - void copyDraftItems() const; - void pasteDraftItems() const; - void cutDraftItems() const; - void convertDraftItems() const; - - private: - ImageEditorScene* _editorScene{nullptr}; - - private: - QAction* _showDirectionsAction{nullptr}; - QAction* _showTagsAction{nullptr}; - QAction* _snapToEdgesAction{nullptr}; - - QAction* _zoomFitAction{nullptr}; - - QAction* _copyDraftItems{nullptr}; - QAction* _pasteDraftItems{nullptr}; - QAction* _cutDraftItems{nullptr}; - QAction* _convertDraftItemsAction{nullptr}; - }; -} - -#endif +/****************************************************************************** + * File: ImageEditorView.h + * Date: 15.3.2018 + *****************************************************************************/ + +#ifndef IMAGEEDITORVIEW_H +#define IMAGEEDITORVIEW_H + +#include "ui/visscene/VisualSceneView.h" +#include "ImageEditorEnvironment.h" +#include "ImageEditorStyle.h" + +namespace grndr +{ + class ImageEditorScene; + class ImageEditorTool; + class InPlaceEditorBase; + + class ImageEditorView : public VisualSceneView + { + Q_OBJECT + + public: + ImageEditorView(QWidget *parent = nullptr); + + public: + ImageEditorScene* editorScene() { return _editorScene; } + const ImageEditorScene* editorScene() const { return _editorScene; } + void setEditorScene(ImageEditorScene* scene); + + public slots: + virtual void removeSelectedItems() const override; + virtual void resizeSelectedItems(QSize delta); + + public: + virtual const ImageEditorStyle& sceneStyle() const override { return ImageEditorStyle::style(); } + + virtual void prepareNodeContextMenu(QMenu& menu) const override; + virtual void prepareNodesContextMenu(QMenu& menu) const override; + + protected: + virtual std::vector<QAction*> getActions(AddActionsMode mode) const override; + + protected: + virtual void drawBackground(QPainter* painter, const QRectF& rect) override; + + virtual void keyPressEvent(QKeyEvent* event) override; + + protected slots: + virtual void updateActions() override; + + private: + void updateSamplingMode(); + void updateCursor(const QCursor& cursor); + + private slots: + void showDirectionArrows(); + void showTags(); + void snapToEdges(); + + void fitImageToWindow(); + + void toggleSamplingMode(); + void selectLayerSamplingMode(); + void selectImageSamplingMode(); + + void imageEditorToolChanging(ImageEditorTool* oldTool, ImageEditorTool* newTool); + void imageEditorToolChanged(ImageEditorTool* tool); + + void draftItemInPlaceEditorActivated(InPlaceEditorBase* editor); + void draftItemInPlaceEditorDeactivated(InPlaceEditorBase* editor); + + void showDirectionArrowsChanged(bool show); + void showTagsChanged(bool show); + void snapToEdgesChanged(bool snap); + void samplingModeChanged(ImageEditorEnvironment::SamplingMode); + + void copyDraftItems() const; + void pasteDraftItems() const; + void cutDraftItems() const; + void convertDraftItems() const; + + private: + ImageEditorScene* _editorScene{nullptr}; + + private: + QAction* _showDirectionsAction{nullptr}; + QAction* _showTagsAction{nullptr}; + QAction* _snapToEdgesAction{nullptr}; + + QAction* _zoomFitAction{nullptr}; + + QAction* _samplingModeAction{nullptr}; + QAction* _samplingModeLayerAction{nullptr}; + QAction* _samplingModeImageAction{nullptr}; + + QAction* _copyDraftItems{nullptr}; + QAction* _pasteDraftItems{nullptr}; + QAction* _cutDraftItems{nullptr}; + QAction* _convertDraftItemsAction{nullptr}; + }; +} + +#endif diff --git a/Grinder/ui/image/tools/ColorPickerTool.cpp b/Grinder/ui/image/tools/ColorPickerTool.cpp index 19e78a0..83f7cf8 100644 --- a/Grinder/ui/image/tools/ColorPickerTool.cpp +++ b/Grinder/ui/image/tools/ColorPickerTool.cpp @@ -1,62 +1,62 @@ -/****************************************************************************** - * File: ColorPickerTool.cpp - * Date: 22.3.2018 - *****************************************************************************/ - -#include "Grinder.h" -#include "ColorPickerTool.h" -#include "ui/image/ImageEditor.h" -#include "res/Resources.h" - -const char* ColorPickerTool::tool_type = "ColorPickerTool"; - -ColorPickerTool::ColorPickerTool(ImageEditor* imageEditor) : ImageEditorTool(imageEditor, "Color picker", FILE_ICON_EDITOR_COLORPICKER, "K", QCursor{QPixmap{FILE_CURSOR_EDITOR_COLORPICKER}, 0, 23}) -{ - -} - -void ColorPickerTool::toolActivated(ImageEditorTool* prevTool) -{ - ImageEditorTool::toolActivated(prevTool); - - _previousTool = prevTool; - _switchToPreviousTool = false; -} - -ImageEditorTool::InputEventResult ColorPickerTool::mousePressed(const QGraphicsSceneMouseEvent* event) -{ - if (event->widget()) - { - // Grab the current scene display and get the pixel color under the cursor - auto pixmap = event->widget()->grab(); - auto image = pixmap.toImage(); - auto pos = event->widget()->mapFromGlobal(event->screenPos()); - - // Set the grabbed color as the primary one and switch back to the previous tool - _imageEditor->environment().setPrimaryColor(image.pixelColor(pos)); - _switchToPreviousTool = true; - } - - return InputEventResult::Process; -} - -ImageEditorTool::InputEventResult ColorPickerTool::mouseMoved(const QGraphicsSceneMouseEvent* event) -{ - Q_UNUSED(event); - return InputEventResult::Process; -} - -ImageEditorTool::InputEventResult ColorPickerTool::mouseReleased(const QGraphicsSceneMouseEvent* event) -{ - Q_UNUSED(event); - - if (_switchToPreviousTool) - { - if (_previousTool) - _imageEditor->editorTools().activateTool(_previousTool->getToolType()); - - _switchToPreviousTool = false; - } - - return InputEventResult::Process; -} +/****************************************************************************** + * File: ColorPickerTool.cpp + * Date: 22.3.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "ColorPickerTool.h" +#include "ui/image/ImageEditor.h" +#include "res/Resources.h" + +const char* ColorPickerTool::tool_type = "ColorPickerTool"; + +ColorPickerTool::ColorPickerTool(ImageEditor* imageEditor) : ImageEditorTool(imageEditor, "Color picker", FILE_ICON_EDITOR_COLORPICKER, "K", QCursor{QPixmap{FILE_CURSOR_EDITOR_COLORPICKER}, 0, 23}) +{ + +} + +void ColorPickerTool::toolActivated(ImageEditorTool* prevTool) +{ + ImageEditorTool::toolActivated(prevTool); + + _previousTool = prevTool; + _switchToPreviousTool = false; +} + +ImageEditorTool::InputEventResult ColorPickerTool::mousePressed(const QGraphicsSceneMouseEvent* event) +{ + if (event->widget()) + { + // Grab the current scene display and get the pixel color under the cursor + auto pixmap = event->widget()->grab(); + auto image = pixmap.toImage(); + auto pos = event->widget()->mapFromGlobal(event->screenPos()); + + // Set the grabbed color as the primary one and switch back to the previous tool + _imageEditor->environment().setPrimaryColor(image.pixelColor(pos)); + _switchToPreviousTool = true; + } + + return InputEventResult::Process; +} + +ImageEditorTool::InputEventResult ColorPickerTool::mouseMoved(const QGraphicsSceneMouseEvent* event) +{ + Q_UNUSED(event); + return InputEventResult::Process; +} + +ImageEditorTool::InputEventResult ColorPickerTool::mouseReleased(const QGraphicsSceneMouseEvent* event) +{ + Q_UNUSED(event); + + if (_switchToPreviousTool) + { + if (_previousTool) + _imageEditor->editorTools().activateTool(_previousTool->getToolType()); + + _switchToPreviousTool = false; + } + + return InputEventResult::Process; +} diff --git a/Grinder/ui/image/tools/EraserTool.cpp b/Grinder/ui/image/tools/EraserTool.cpp index b3fc73f..149ef2d 100644 --- a/Grinder/ui/image/tools/EraserTool.cpp +++ b/Grinder/ui/image/tools/EraserTool.cpp @@ -1,72 +1,72 @@ -/****************************************************************************** - * File: EraserTool.cpp - * Date: 21.5.2018 - *****************************************************************************/ - -#include "Grinder.h" -#include "EraserTool.h" -#include "ui/image/ImageEditor.h" -#include "ui/visscene/RubberBandNode.h" -#include "image/ImageUtils.h" -#include "res/Resources.h" - -const char* EraserTool::tool_type = "EraserTool"; - -EraserTool::EraserTool(ImageEditor* imageEditor) : PaintbrushTool(imageEditor, "Eraser", FILE_ICON_EDITOR_ERASER, "E", QCursor{QPixmap{FILE_CURSOR_EDITOR_ERASER}, 12, 12}) -{ - _eraseByDefault = true; -} - -void EraserTool::createProperties() -{ - PaintbrushTool::createProperties(); - - // The eraser should have a default width equal to the cursor size - setPropertyGroup("General"); - - penWidth()->setValue(_cursor.pixmap().width()); - penWidth()->setName("Eraser width"); - penWidth()->setDescription("The width of the eraser in (screen) pixels."); -} - -VisualSceneInputHandler::InputEventResult EraserTool::rightMousePressed(const QGraphicsSceneMouseEvent* event) -{ - createRubberBand(event->scenePos()); - return InputEventResult::Process; -} - -VisualSceneInputHandler::InputEventResult EraserTool::rightMouseMoved(const QGraphicsSceneMouseEvent* event) -{ - if (_rubberBandNode) - { - _rubberBandNode->move(event->scenePos()); - return InputEventResult::Process; - } - else - return InputEventResult::Ignore; -} - -VisualSceneInputHandler::InputEventResult EraserTool::rightMouseReleased(const QGraphicsSceneMouseEvent* event) -{ - Q_UNUSED(event); - - if (_rubberBandNode) - { - // Erase all pixels within the rubber band rectangle - auto eraseRect = _rubberBandNode->getRect(); - std::list<QPoint> points; - - for (int r = eraseRect.top(); r <= eraseRect.bottom(); ++r) - { - for (int c = eraseRect.left(); c <= eraseRect.right(); ++c) - points.emplace_back(c, r); - } - - _imageEditor->controller().paintPixels(nullptr, points, QColor{}); - - removeRubberBand(); - return InputEventResult::Process; - } - else - return InputEventResult::Ignore; -} +/****************************************************************************** + * File: EraserTool.cpp + * Date: 21.5.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "EraserTool.h" +#include "ui/image/ImageEditor.h" +#include "ui/visscene/RubberBandNode.h" +#include "image/ImageUtils.h" +#include "res/Resources.h" + +const char* EraserTool::tool_type = "EraserTool"; + +EraserTool::EraserTool(ImageEditor* imageEditor) : PaintbrushTool(imageEditor, "Eraser", FILE_ICON_EDITOR_ERASER, "E", QCursor{QPixmap{FILE_CURSOR_EDITOR_ERASER}, 12, 12}) +{ + _eraseByDefault = true; +} + +void EraserTool::createProperties() +{ + PaintbrushTool::createProperties(); + + // The eraser should have a default width equal to the cursor size + setPropertyGroup("General"); + + penWidth()->setValue(_cursor.pixmap().width()); + penWidth()->setName("Eraser width"); + penWidth()->setDescription("The width of the eraser in (screen) pixels."); +} + +VisualSceneInputHandler::InputEventResult EraserTool::rightMousePressed(const QGraphicsSceneMouseEvent* event) +{ + createRubberBand(event->scenePos()); + return InputEventResult::Process; +} + +VisualSceneInputHandler::InputEventResult EraserTool::rightMouseMoved(const QGraphicsSceneMouseEvent* event) +{ + if (_rubberBandNode) + { + _rubberBandNode->move(event->scenePos()); + return InputEventResult::Process; + } + else + return InputEventResult::Ignore; +} + +VisualSceneInputHandler::InputEventResult EraserTool::rightMouseReleased(const QGraphicsSceneMouseEvent* event) +{ + Q_UNUSED(event); + + if (_rubberBandNode) + { + // Erase all pixels within the rubber band rectangle + auto eraseRect = _rubberBandNode->getRect(); + std::list<QPoint> points; + + for (int r = eraseRect.top(); r <= eraseRect.bottom(); ++r) + { + for (int c = eraseRect.left(); c <= eraseRect.right(); ++c) + points.emplace_back(c, r); + } + + _imageEditor->controller().paintPixels(nullptr, points, QColor{}); + + removeRubberBand(); + return InputEventResult::Process; + } + else + return InputEventResult::Ignore; +} diff --git a/Grinder/ui/image/tools/EraserTool.h b/Grinder/ui/image/tools/EraserTool.h index 2946b0a..500de2f 100644 --- a/Grinder/ui/image/tools/EraserTool.h +++ b/Grinder/ui/image/tools/EraserTool.h @@ -1,36 +1,36 @@ -/****************************************************************************** - * File: EraserTool.h - * Date: 21.5.2018 - *****************************************************************************/ - -#ifndef ERASERTOOL_H -#define ERASERTOOL_H - -#include "PaintbrushTool.h" - -namespace grndr -{ - class EraserTool : public PaintbrushTool - { - Q_OBJECT - - public: - static const char* tool_type; - - public: - EraserTool(ImageEditor* imageEditor); - - public: - virtual QString getToolType() const override { return tool_type; } - - protected: - virtual void createProperties() override; - - protected: - virtual InputEventResult rightMousePressed(const QGraphicsSceneMouseEvent* event) override; - virtual InputEventResult rightMouseMoved(const QGraphicsSceneMouseEvent* event) override; - virtual InputEventResult rightMouseReleased(const QGraphicsSceneMouseEvent* event) override; - }; -} - -#endif +/****************************************************************************** + * File: EraserTool.h + * Date: 21.5.2018 + *****************************************************************************/ + +#ifndef ERASERTOOL_H +#define ERASERTOOL_H + +#include "PaintbrushTool.h" + +namespace grndr +{ + class EraserTool : public PaintbrushTool + { + Q_OBJECT + + public: + static const char* tool_type; + + public: + EraserTool(ImageEditor* imageEditor); + + public: + virtual QString getToolType() const override { return tool_type; } + + protected: + virtual void createProperties() override; + + protected: + virtual InputEventResult rightMousePressed(const QGraphicsSceneMouseEvent* event) override; + virtual InputEventResult rightMouseMoved(const QGraphicsSceneMouseEvent* event) override; + virtual InputEventResult rightMouseReleased(const QGraphicsSceneMouseEvent* event) override; + }; +} + +#endif diff --git a/Grinder/ui/image/tools/SelectionTool.cpp b/Grinder/ui/image/tools/SelectionTool.cpp index aaebe19..114697f 100644 --- a/Grinder/ui/image/tools/SelectionTool.cpp +++ b/Grinder/ui/image/tools/SelectionTool.cpp @@ -33,6 +33,9 @@ void SelectionTool::toolActivated(ImageEditorTool* prevTool) // Listen to layer switching in order to hide any active selection connect(&_imageEditor->controller(), &ImageEditorController::layerSwitching, this, &SelectionTool::layerSwitching); + // Reset the current selection when changing the sampling mode + connect(&_imageEditor->environment(), &ImageEditorEnvironment::samplingModeChanged, this, &SelectionTool::resetSelection); + // Install an event filter to catch all key events in the application to properly handle control and shift states grinder()->installEventFilter(this); } @@ -287,7 +290,9 @@ void SelectionTool::processSelection() { if (auto layer = _imageEditor->controller().activeLayer()) { - _selection.trimSelection(layer); + if (_imageEditor->environment().getSamplingMode() == ImageEditorEnvironment::SamplingMode::Layer) + _selection.trimSelection(layer); + _selection.createSelectionNode(layer); } } diff --git a/Grinder/ui/image/tools/WandSelectionTool.cpp b/Grinder/ui/image/tools/WandSelectionTool.cpp index 005a26a..ccfd2a3 100644 --- a/Grinder/ui/image/tools/WandSelectionTool.cpp +++ b/Grinder/ui/image/tools/WandSelectionTool.cpp @@ -134,12 +134,17 @@ void WandSelectionTool::createSelectionFromSeeds(bool handleReplace) { if (auto layer = _imageEditor->controller().activeLayer()) { + cv::Mat sourceImage; + + if (_imageEditor->environment().getSamplingMode() == ImageEditorEnvironment::SamplingMode::Image) + sourceImage = _imageEditor->getDisplayedImage(); + LayerPixelsData::Selection selection; bool initialized = false; for (const auto& seed : _seeds) { - auto seedSelection = _wandAlgorithm->execute(seed, *layer); + auto seedSelection = _wandAlgorithm->execute(seed, *layer, sourceImage); if (!initialized) { -- GitLab