diff --git a/krita/pics/tools/SVG/16/dark_tool_magnetic_selection.svg b/krita/pics/tools/SVG/16/dark_tool_magnetic_selection.svg new file mode 100644 index 0000000000..07f84e0bd5 --- /dev/null +++ b/krita/pics/tools/SVG/16/dark_tool_magnetic_selection.svg @@ -0,0 +1,168 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + Andrei Rudenko + + + + + + + + + + + diff --git a/krita/pics/tools/SVG/16/light_tool_magnetic_selection.svg b/krita/pics/tools/SVG/16/light_tool_magnetic_selection.svg new file mode 100644 index 0000000000..10f9a0401f --- /dev/null +++ b/krita/pics/tools/SVG/16/light_tool_magnetic_selection.svg @@ -0,0 +1,169 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + Andrei Rudenko + + + + + + + + + + + + diff --git a/krita/pics/tools/SVG/16/tools-svg-16-icons.qrc b/krita/pics/tools/SVG/16/tools-svg-16-icons.qrc index d760fb0128..e831473360 100644 --- a/krita/pics/tools/SVG/16/tools-svg-16-icons.qrc +++ b/krita/pics/tools/SVG/16/tools-svg-16-icons.qrc @@ -1,82 +1,84 @@ dark_calligraphy.svg dark_draw-text.svg dark_format-fill-color.svg dark_krita_draw_path.svg dark_krita_tool_color_fill.svg dark_krita_tool_color_picker.svg dark_krita_tool_dyna.svg dark_krita_tool_ellipse.svg dark_krita_tool_freehand.svg dark_krita_tool_freehandvector.svg dark_krita_tool_gradient.svg dark_krita_tool_grid.svg dark_krita_tool_line.svg dark_krita_tool_measure.svg dark_krita_tool_move.svg dark_krita_tool_multihand.svg dark_krita_tool_polygon.svg dark_krita_tool_rectangle.svg dark_krita_tool_transform.svg dark_pattern.svg dark_polyline.svg dark_select.svg dark_tool_contiguous_selection.svg dark_tool_crop.svg dark_tool_elliptical_selection.svg + dark_tool_magnetic_selection.svg dark_tool_outline_selection.svg dark_tool_pan.svg dark_tool_path_selection.svg dark_tool_perspectivegrid.svg dark_tool_polygonal_selection.svg dark_tool_rect_selection.svg dark_tool_similar_selection.svg dark_tool_zoom.svg light_calligraphy.svg light_draw-text.svg light_format-fill-color.svg light_krita_draw_path.svg light_krita_tool_color_fill.svg light_krita_tool_color_picker.svg light_krita_tool_dyna.svg light_krita_tool_ellipse.svg light_krita_tool_freehand.svg light_krita_tool_freehandvector.svg light_krita_tool_gradient.svg light_krita_tool_grid.svg light_krita_tool_line.svg light_krita_tool_measure.svg light_krita_tool_move.svg light_krita_tool_multihand.svg light_krita_tool_polygon.svg light_krita_tool_rectangle.svg light_krita_tool_transform.svg light_pattern.svg light_polyline.svg light_select.svg light_tool_contiguous_selection.svg light_tool_crop.svg light_tool_elliptical_selection.svg light_tool_outline_selection.svg + light_tool_magnetic_selection.svg light_tool_pan.svg light_tool_path_selection.svg light_tool_perspectivegrid.svg light_tool_polygonal_selection.svg light_tool_rect_selection.svg light_tool_similar_selection.svg light_tool_zoom.svg dark_shape_handling.svg dark_artistic_text.svg light_artistic_text.svg light_shape_handling.svg dark_krita_tool_lazybrush.svg light_krita_tool_lazybrush.svg dark_krita_tool_smart_patch.svg light_krita_tool_smart_patch.svg light_krita_tool_assistant.svg dark_krita_tool_assistant.svg dark_krita_tool_reference_images.svg light_krita_tool_reference_images.svg diff --git a/libs/image/kis_gaussian_kernel.cpp b/libs/image/kis_gaussian_kernel.cpp index 1b82b6c4ef..851bc5cc4d 100644 --- a/libs/image/kis_gaussian_kernel.cpp +++ b/libs/image/kis_gaussian_kernel.cpp @@ -1,359 +1,400 @@ /* * Copyright (c) 2014 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_gaussian_kernel.h" #include "kis_global.h" #include "kis_convolution_kernel.h" #include #include #include qreal KisGaussianKernel::sigmaFromRadius(qreal radius) { return 0.3 * radius + 0.3; } int KisGaussianKernel::kernelSizeFromRadius(qreal radius) { return 6 * ceil(sigmaFromRadius(radius)) + 1; } Eigen::Matrix KisGaussianKernel::createHorizontalMatrix(qreal radius) { int kernelSize = kernelSizeFromRadius(radius); Eigen::Matrix matrix(1, kernelSize); const qreal sigma = sigmaFromRadius(radius); const qreal multiplicand = 1 / (sqrt(2 * M_PI * sigma * sigma)); const qreal exponentMultiplicand = 1 / (2 * sigma * sigma); /** * The kernel size should always be odd, then the position of the * central pixel can be easily calculated */ KIS_ASSERT_RECOVER_NOOP(kernelSize & 0x1); const int center = kernelSize / 2; for (int x = 0; x < kernelSize; x++) { qreal xDistance = center - x; matrix(0, x) = multiplicand * exp( -xDistance * xDistance * exponentMultiplicand ); } return matrix; } Eigen::Matrix KisGaussianKernel::createVerticalMatrix(qreal radius) { int kernelSize = kernelSizeFromRadius(radius); Eigen::Matrix matrix(kernelSize, 1); const qreal sigma = sigmaFromRadius(radius); const qreal multiplicand = 1 / (sqrt(2 * M_PI * sigma * sigma)); const qreal exponentMultiplicand = 1 / (2 * sigma * sigma); /** * The kernel size should always be odd, then the position of the * central pixel can be easily calculated */ KIS_ASSERT_RECOVER_NOOP(kernelSize & 0x1); const int center = kernelSize / 2; for (int y = 0; y < kernelSize; y++) { qreal yDistance = center - y; matrix(y, 0) = multiplicand * exp( -yDistance * yDistance * exponentMultiplicand ); } return matrix; } KisConvolutionKernelSP KisGaussianKernel::createHorizontalKernel(qreal radius) { Eigen::Matrix matrix = createHorizontalMatrix(radius); return KisConvolutionKernel::fromMatrix(matrix, 0, matrix.sum()); } KisConvolutionKernelSP KisGaussianKernel::createVerticalKernel(qreal radius) { Eigen::Matrix matrix = createVerticalMatrix(radius); return KisConvolutionKernel::fromMatrix(matrix, 0, matrix.sum()); } KisConvolutionKernelSP KisGaussianKernel::createUniform2DKernel(qreal xRadius, qreal yRadius) { Eigen::Matrix h = createHorizontalMatrix(xRadius); Eigen::Matrix v = createVerticalMatrix(yRadius); Eigen::Matrix uni = v * h; return KisConvolutionKernel::fromMatrix(uni, 0, uni.sum()); } void KisGaussianKernel::applyGaussian(KisPaintDeviceSP device, const QRect& rect, qreal xRadius, qreal yRadius, const QBitArray &channelFlags, KoUpdater *progressUpdater, bool createTransaction) { QPoint srcTopLeft = rect.topLeft(); if (KisConvolutionPainter::supportsFFTW()) { KisConvolutionPainter painter(device, KisConvolutionPainter::FFTW); painter.setChannelFlags(channelFlags); painter.setProgress(progressUpdater); KisConvolutionKernelSP kernel2D = KisGaussianKernel::createUniform2DKernel(xRadius, yRadius); QScopedPointer transaction; if (createTransaction && painter.needsTransaction(kernel2D)) { transaction.reset(new KisTransaction(device)); } painter.applyMatrix(kernel2D, device, srcTopLeft, srcTopLeft, rect.size(), BORDER_REPEAT); } else if (xRadius > 0.0 && yRadius > 0.0) { KisPaintDeviceSP interm = new KisPaintDevice(device->colorSpace()); interm->prepareClone(device); KisConvolutionKernelSP kernelHoriz = KisGaussianKernel::createHorizontalKernel(xRadius); KisConvolutionKernelSP kernelVertical = KisGaussianKernel::createVerticalKernel(yRadius); qreal verticalCenter = qreal(kernelVertical->height()) / 2.0; KisConvolutionPainter horizPainter(interm); horizPainter.setChannelFlags(channelFlags); horizPainter.setProgress(progressUpdater); horizPainter.applyMatrix(kernelHoriz, device, srcTopLeft - QPoint(0, ceil(verticalCenter)), srcTopLeft - QPoint(0, ceil(verticalCenter)), rect.size() + QSize(0, 2 * ceil(verticalCenter)), BORDER_REPEAT); KisConvolutionPainter verticalPainter(device); verticalPainter.setChannelFlags(channelFlags); verticalPainter.setProgress(progressUpdater); verticalPainter.applyMatrix(kernelVertical, interm, srcTopLeft, srcTopLeft, rect.size(), BORDER_REPEAT); } else if (xRadius > 0.0) { KisConvolutionPainter painter(device); painter.setChannelFlags(channelFlags); painter.setProgress(progressUpdater); KisConvolutionKernelSP kernelHoriz = KisGaussianKernel::createHorizontalKernel(xRadius); QScopedPointer transaction; if (createTransaction && painter.needsTransaction(kernelHoriz)) { transaction.reset(new KisTransaction(device)); } painter.applyMatrix(kernelHoriz, device, srcTopLeft, srcTopLeft, rect.size(), BORDER_REPEAT); } else if (yRadius > 0.0) { KisConvolutionPainter painter(device); painter.setChannelFlags(channelFlags); painter.setProgress(progressUpdater); KisConvolutionKernelSP kernelVertical = KisGaussianKernel::createVerticalKernel(yRadius); QScopedPointer transaction; if (createTransaction && painter.needsTransaction(kernelVertical)) { transaction.reset(new KisTransaction(device)); } painter.applyMatrix(kernelVertical, device, srcTopLeft, srcTopLeft, rect.size(), BORDER_REPEAT); } } Eigen::Matrix -KisGaussianKernel::createLoGMatrix(qreal radius, qreal coeff) +KisGaussianKernel::createLoGMatrix(qreal radius, qreal coeff, bool zeroCentered, bool includeWrappedArea) { - int kernelSize = 4 * std::ceil(radius) + 1; + int kernelSize = 2 * (includeWrappedArea ? 2 : 1) * std::ceil(radius) + 1; Eigen::Matrix matrix(kernelSize, kernelSize); const qreal sigma = radius/* / sqrt(2)*/; const qreal multiplicand = -1.0 / (M_PI * pow2(pow2(sigma))); const qreal exponentMultiplicand = 1 / (2 * pow2(sigma)); /** * The kernel size should always be odd, then the position of the * central pixel can be easily calculated */ KIS_ASSERT_RECOVER_NOOP(kernelSize & 0x1); const int center = kernelSize / 2; for (int y = 0; y < kernelSize; y++) { const qreal yDistance = center - y; for (int x = 0; x < kernelSize; x++) { const qreal xDistance = center - x; const qreal distance = pow2(xDistance) + pow2(yDistance); const qreal normalizedDistance = exponentMultiplicand * distance; matrix(x, y) = multiplicand * (1.0 - normalizedDistance) * exp(-normalizedDistance); } } qreal lateral = matrix.sum() - matrix(center, center); matrix(center, center) = -lateral; + qreal totalSum = 0; + + if (zeroCentered) { + for (int y = 0; y < kernelSize; y++) { + for (int x = 0; x < kernelSize; x++) { + const qreal value = matrix(x, y); + totalSum += value; + } + } + } + qreal positiveSum = 0; qreal sideSum = 0; qreal quarterSum = 0; + totalSum = 0; + + const qreal offset = totalSum / pow2(qreal(kernelSize)); for (int y = 0; y < kernelSize; y++) { for (int x = 0; x < kernelSize; x++) { - const qreal value = matrix(x, y); + qreal value = matrix(x, y); + value -= offset; + matrix(x, y) = value; + if (value > 0) { positiveSum += value; } if (x > center) { sideSum += value; } if (x > center && y > center) { quarterSum += value; } + totalSum += value; } } const qreal scale = coeff * 2.0 / positiveSum; matrix *= scale; positiveSum *= scale; sideSum *= scale; quarterSum *= scale; //qDebug() << ppVar(positiveSum) << ppVar(sideSum) << ppVar(quarterSum); return matrix; } void KisGaussianKernel::applyLoG(KisPaintDeviceSP device, const QRect& rect, qreal radius, qreal coeff, const QBitArray &channelFlags, KoUpdater *progressUpdater) { QPoint srcTopLeft = rect.topLeft(); KisConvolutionPainter painter(device); painter.setChannelFlags(channelFlags); painter.setProgress(progressUpdater); - Eigen::Matrix matrix = createLoGMatrix(radius, coeff); + Eigen::Matrix matrix = + createLoGMatrix(radius, coeff, false, true); + KisConvolutionKernelSP kernel = + KisConvolutionKernel::fromMatrix(matrix, + 0, + 0); + + painter.applyMatrix(kernel, device, srcTopLeft, srcTopLeft, rect.size(), BORDER_REPEAT); +} + +void KisGaussianKernel::applyTightLoG(KisPaintDeviceSP device, + const QRect& rect, + qreal radius, qreal coeff, + const QBitArray &channelFlags, + KoUpdater *progressUpdater) +{ + QPoint srcTopLeft = rect.topLeft(); + + KisConvolutionPainter painter(device); + painter.setChannelFlags(channelFlags); + painter.setProgress(progressUpdater); + + Eigen::Matrix matrix = + createLoGMatrix(radius, coeff, true, false); KisConvolutionKernelSP kernel = KisConvolutionKernel::fromMatrix(matrix, 0, 0); painter.applyMatrix(kernel, device, srcTopLeft, srcTopLeft, rect.size(), BORDER_REPEAT); } Eigen::Matrix KisGaussianKernel::createDilateMatrix(qreal radius) { const int kernelSize = 2 * std::ceil(radius) + 1; Eigen::Matrix matrix(kernelSize, kernelSize); const qreal fadeStart = qMax(1.0, radius - 1.0); /** * The kernel size should always be odd, then the position of the * central pixel can be easily calculated */ KIS_ASSERT_RECOVER_NOOP(kernelSize & 0x1); const int center = kernelSize / 2; for (int y = 0; y < kernelSize; y++) { const qreal yDistance = center - y; for (int x = 0; x < kernelSize; x++) { const qreal xDistance = center - x; const qreal distance = std::sqrt(pow2(xDistance) + pow2(yDistance)); qreal value = 1.0; if (distance >= radius) { value = 0.0; } else if (distance > fadeStart) { value = radius - distance; } matrix(x, y) = value; } } return matrix; } void KisGaussianKernel::applyDilate(KisPaintDeviceSP device, const QRect &rect, qreal radius, const QBitArray &channelFlags, KoUpdater *progressUpdater, bool createTransaction) { KIS_SAFE_ASSERT_RECOVER_RETURN(device->colorSpace()->pixelSize() == 1); QPoint srcTopLeft = rect.topLeft(); KisConvolutionPainter painter(device); painter.setChannelFlags(channelFlags); painter.setProgress(progressUpdater); Eigen::Matrix matrix = createDilateMatrix(radius); KisConvolutionKernelSP kernel = KisConvolutionKernel::fromMatrix(matrix, 0, 1.0); QScopedPointer transaction; if (createTransaction && painter.needsTransaction(kernel)) { transaction.reset(new KisTransaction(device)); } painter.applyMatrix(kernel, device, srcTopLeft, srcTopLeft, rect.size(), BORDER_REPEAT); } #include "kis_sequential_iterator.h" void KisGaussianKernel::applyErodeU8(KisPaintDeviceSP device, const QRect &rect, qreal radius, const QBitArray &channelFlags, KoUpdater *progressUpdater, bool createTransaction) { KIS_SAFE_ASSERT_RECOVER_RETURN(device->colorSpace()->pixelSize() == 1); { KisSequentialIterator dstIt(device, rect); while (dstIt.nextPixel()) { quint8 *dstPtr = dstIt.rawData(); *dstPtr = 255 - *dstPtr; } } applyDilate(device, rect, radius, channelFlags, progressUpdater, createTransaction); { KisSequentialIterator dstIt(device, rect); while (dstIt.nextPixel()) { quint8 *dstPtr = dstIt.rawData(); *dstPtr = 255 - *dstPtr; } } } diff --git a/libs/image/kis_gaussian_kernel.h b/libs/image/kis_gaussian_kernel.h index c9c300fbdd..d1ae29e8c4 100644 --- a/libs/image/kis_gaussian_kernel.h +++ b/libs/image/kis_gaussian_kernel.h @@ -1,83 +1,90 @@ /* * Copyright (c) 2014 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __KIS_GAUSSIAN_KERNEL_H #define __KIS_GAUSSIAN_KERNEL_H #include "kritaimage_export.h" #include "kis_types.h" #include class QRect; class KRITAIMAGE_EXPORT KisGaussianKernel { public: static Eigen::Matrix createHorizontalMatrix(qreal radius); static Eigen::Matrix createVerticalMatrix(qreal radius); static KisConvolutionKernelSP createHorizontalKernel(qreal radius); static KisConvolutionKernelSP createVerticalKernel(qreal radius); static KisConvolutionKernelSP createUniform2DKernel(qreal xRadius, qreal yRadius); static qreal sigmaFromRadius(qreal radius); static int kernelSizeFromRadius(qreal radius); static void applyGaussian(KisPaintDeviceSP device, const QRect& rect, qreal xRadius, qreal yRadius, const QBitArray &channelFlags, KoUpdater *updater, bool createTransaction = false); - static Eigen::Matrix createLoGMatrix(qreal radius, qreal coeff = 1.0); + static Eigen::Matrix createLoGMatrix(qreal radius, qreal coeff, bool zeroCentered, bool includeWrappedArea); static void applyLoG(KisPaintDeviceSP device, const QRect& rect, qreal radius, qreal coeff, const QBitArray &channelFlags, KoUpdater *progressUpdater); + static void applyTightLoG(KisPaintDeviceSP device, + const QRect& rect, + qreal radius, qreal coeff, + const QBitArray &channelFlags, + KoUpdater *progressUpdater); + + static Eigen::Matrix createDilateMatrix(qreal radius); static void applyDilate(KisPaintDeviceSP device, const QRect& rect, qreal radius, const QBitArray &channelFlags, KoUpdater *progressUpdater, bool createTransaction = false); static void applyErodeU8(KisPaintDeviceSP device, const QRect& rect, qreal radius, const QBitArray &channelFlags, KoUpdater *progressUpdater, bool createTransaction = false); }; #endif /* __KIS_GAUSSIAN_KERNEL_H */ diff --git a/libs/image/lazybrush/kis_lazy_fill_graph.h b/libs/image/lazybrush/kis_lazy_fill_graph.h index 410cf27127..ec30531818 100644 --- a/libs/image/lazybrush/kis_lazy_fill_graph.h +++ b/libs/image/lazybrush/kis_lazy_fill_graph.h @@ -1,1028 +1,1030 @@ /* * Copyright (c) 2016 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __KIS_LAZY_FILL_GRAPH_H #define __KIS_LAZY_FILL_GRAPH_H #include #include #include #include #include #include #include #include //#define USE_LAZY_FILL_SANITY_CHECKS 1 #ifdef USE_LAZY_FILL_SANITY_CHECKS #define LF_SANITY_ASSERT(x) KIS_ASSERT(x) #define LF_SANITY_ASSERT_RECOVER(x) KIS_ASSERT_RECOVER(x) #else #define LF_SANITY_ASSERT(x) #define LF_SANITY_ASSERT_RECOVER(x) if (0) #endif /* USE_LAZY_FILL_SANITY_CHECKS */ using namespace boost; /* BOOST_CONCEPT_ASSERT(( ReadablePropertyMapConcept )); //read flow-values from vertices */ class KisLazyFillGraph; //=================== // Index Property Map //=================== template struct lazy_fill_graph_index_map { public: typedef Index value_type; typedef Index reference_type; typedef reference_type reference; typedef Descriptor key_type; typedef readable_property_map_tag category; lazy_fill_graph_index_map() { } lazy_fill_graph_index_map(const Graph& graph) : m_graph(&graph) { } value_type operator[](key_type key) const { value_type index = m_graph->index_of(key); LF_SANITY_ASSERT(index >= 0); return index; } friend inline Index get(const lazy_fill_graph_index_map& index_map, const typename lazy_fill_graph_index_map::key_type& key) { return (index_map[key]); } protected: const Graph* m_graph; }; //========================== // Reverse Edge Property Map //========================== template struct lazy_fill_graph_reverse_edge_map { public: typedef Descriptor value_type; typedef Descriptor reference_type; typedef reference_type reference; typedef Descriptor key_type; typedef readable_property_map_tag category; lazy_fill_graph_reverse_edge_map() { } value_type operator[](const key_type& key) const { return (value_type(key.second, key.first)); } friend inline Descriptor get(const lazy_fill_graph_reverse_edge_map& rev_map, const typename lazy_fill_graph_reverse_edge_map::key_type& key) { return (rev_map[key]); } }; //================= // Function Objects //================= namespace kis_detail { // vertex_at template struct lazy_fill_graph_vertex_at { typedef typename graph_traits::vertex_descriptor result_type; lazy_fill_graph_vertex_at() : m_graph(0) {} lazy_fill_graph_vertex_at(const Graph* graph) : m_graph(graph) { } result_type operator() (typename graph_traits::vertices_size_type vertex_index) const { return (vertex(vertex_index, *m_graph)); } private: const Graph* m_graph; }; // out_edge_at template struct lazy_fill_graph_out_edge_at { private: typedef typename graph_traits::vertex_descriptor vertex_descriptor; public: typedef typename graph_traits::edge_descriptor result_type; lazy_fill_graph_out_edge_at() : m_vertex(), m_graph(0) {} lazy_fill_graph_out_edge_at(vertex_descriptor source_vertex, const Graph* graph) : m_vertex(source_vertex), m_graph(graph) { } result_type operator() (typename graph_traits::degree_size_type out_edge_index) const { return (out_edge_at(m_vertex, out_edge_index, *m_graph)); } private: vertex_descriptor m_vertex; const Graph* m_graph; }; // in_edge_at template struct lazy_fill_graph_in_edge_at { private: typedef typename graph_traits::vertex_descriptor vertex_descriptor; public: typedef typename graph_traits::edge_descriptor result_type; lazy_fill_graph_in_edge_at() : m_vertex(), m_graph(0) {} lazy_fill_graph_in_edge_at(vertex_descriptor target_vertex, const Graph* graph) : m_vertex(target_vertex), m_graph(graph) { } result_type operator() (typename graph_traits::degree_size_type in_edge_index) const { return (in_edge_at(m_vertex, in_edge_index, *m_graph)); } private: vertex_descriptor m_vertex; const Graph* m_graph; }; // edge_at template struct lazy_fill_graph_edge_at { typedef typename graph_traits::edge_descriptor result_type; lazy_fill_graph_edge_at() : m_graph(0) {} lazy_fill_graph_edge_at(const Graph* graph) : m_graph(graph) { } result_type operator() (typename graph_traits::edges_size_type edge_index) const { return (edge_at(edge_index, *m_graph)); } private: const Graph* m_graph; }; // adjacent_vertex_at template struct lazy_fill_graph_adjacent_vertex_at { public: typedef typename graph_traits::vertex_descriptor result_type; lazy_fill_graph_adjacent_vertex_at(result_type source_vertex, const Graph* graph) : m_vertex(source_vertex), m_graph(graph) { } result_type operator() (typename graph_traits::degree_size_type adjacent_index) const { return (target(out_edge_at(m_vertex, adjacent_index, *m_graph), *m_graph)); } private: result_type m_vertex; const Graph* m_graph; }; } // namespace kis_detail class KisLazyFillGraph { public: typedef KisLazyFillGraph type; typedef long VertexIndex; typedef long EdgeIndex; // sizes typedef VertexIndex vertices_size_type; typedef EdgeIndex edges_size_type; typedef EdgeIndex degree_size_type; struct VertexDescriptor : public equality_comparable { enum VertexType { NORMAL = 0, LABEL_A, LABEL_B }; - vertices_size_type x; vertices_size_type y; VertexType type; VertexDescriptor(vertices_size_type _x, vertices_size_type _y, VertexType _type = NORMAL) : x(_x), y(_y), type(_type) {} + // TODO: Extra constructors look unnecessary, ask Dmitry before removing VertexDescriptor(VertexType _type) : x(0), y(0), type(_type) {} VertexDescriptor() : x(0), y(0), type(NORMAL) {} bool operator ==(const VertexDescriptor &rhs) const { return rhs.x == x && rhs.y == y && rhs.type == type; } }; // descriptors typedef VertexDescriptor vertex_descriptor; typedef std::pair edge_descriptor; friend QDebug operator<<(QDebug dbg, const KisLazyFillGraph::edge_descriptor &e); friend QDebug operator<<(QDebug dbg, const KisLazyFillGraph::vertex_descriptor &v); // vertex_iterator typedef counting_iterator vertex_index_iterator; typedef kis_detail::lazy_fill_graph_vertex_at vertex_function; typedef transform_iterator vertex_iterator; // edge_iterator typedef counting_iterator edge_index_iterator; typedef kis_detail::lazy_fill_graph_edge_at edge_function; typedef transform_iterator edge_iterator; // out_edge_iterator typedef counting_iterator degree_iterator; typedef kis_detail::lazy_fill_graph_out_edge_at out_edge_function; typedef transform_iterator out_edge_iterator; // adjacency_iterator typedef kis_detail::lazy_fill_graph_adjacent_vertex_at adjacent_vertex_function; typedef transform_iterator adjacency_iterator; // categories typedef directed_tag directed_category; typedef disallow_parallel_edge_tag edge_parallel_category; struct traversal_category : virtual public incidence_graph_tag, virtual public adjacency_graph_tag, virtual public vertex_list_graph_tag, virtual public edge_list_graph_tag, virtual public adjacency_matrix_tag { }; static inline vertex_descriptor null_vertex() { vertex_descriptor maxed_out_vertex( std::numeric_limits::max(), std::numeric_limits::max(), vertex_descriptor::NORMAL); return (maxed_out_vertex); } KisLazyFillGraph() {} KisLazyFillGraph(const QRect &mainRect, const QRegion &aLabelRegion, const QRegion &bLabelRegion) : m_x(mainRect.x()), m_y(mainRect.y()), m_width(mainRect.width()), m_height(mainRect.height()) { m_mainArea = mainRect; m_aLabelArea = aLabelRegion.boundingRect(); m_bLabelArea = bLabelRegion.boundingRect(); + + //QRegion::rects() is deprecated, https://doc.qt.io/qt-5/qregion-obsolete.html#rects m_aLabelRects = aLabelRegion.rects(); m_bLabelRects = bLabelRegion.rects(); KIS_ASSERT(m_mainArea.contains(m_aLabelArea)); KIS_ASSERT(m_mainArea.contains(m_bLabelArea)); m_numVertices = m_width * m_height + 2; m_edgeBins << EdgeIndexBin(0, m_mainArea.adjusted(0, 0, -1, 0), HORIZONTAL); m_edgeBins << EdgeIndexBin(m_edgeBins.last(), m_mainArea.adjusted(0, 0, -1, 0), HORIZONTAL_REVERSED); m_edgeBins << EdgeIndexBin(m_edgeBins.last(), m_mainArea.adjusted(0, 0, 0, -1), VERTICAL); m_edgeBins << EdgeIndexBin(m_edgeBins.last(), m_mainArea.adjusted(0, 0, 0, -1), VERTICAL_REVERSED); Q_FOREACH (const QRect &rc, m_aLabelRects) { m_edgeBins << EdgeIndexBin(m_edgeBins.last(), rc, LABEL_A); } // out_edge_at relies on the sequential layout of reversed edges of one type m_aReversedEdgesStart = m_edgeBins.last().last() + 1; Q_FOREACH (const QRect &rc, m_aLabelRects) { m_edgeBins << EdgeIndexBin(m_edgeBins.last(), rc, LABEL_A_REVERSED); } m_numAEdges = m_edgeBins.last().last() + 1 - m_aReversedEdgesStart; Q_FOREACH (const QRect &rc, m_bLabelRects) { m_edgeBins << EdgeIndexBin(m_edgeBins.last(), rc, LABEL_B); } // out_edge_at relies on the sequential layout of reversed edges of one type m_bReversedEdgesStart = m_edgeBins.last().last() + 1; Q_FOREACH (const QRect &rc, m_bLabelRects) { m_edgeBins << EdgeIndexBin(m_edgeBins.last(), rc, LABEL_B_REVERSED); } m_numBEdges = m_edgeBins.last().last() + 1 - m_bReversedEdgesStart; m_numEdges = m_edgeBins.last().last() + 1; } ~KisLazyFillGraph() { } QSize size() const { return QSize(m_width, m_height); } QRect rect() const { return QRect(m_x, m_y, m_width, m_height); } vertices_size_type m_x; vertices_size_type m_y; vertices_size_type m_width; vertices_size_type m_height; vertices_size_type m_numVertices; vertices_size_type m_numEdges; vertices_size_type m_aReversedEdgesStart; vertices_size_type m_bReversedEdgesStart; vertices_size_type m_numAEdges; vertices_size_type m_numBEdges; enum EdgeIndexBinId { HORIZONTAL, HORIZONTAL_REVERSED, VERTICAL, VERTICAL_REVERSED, LABEL_A, LABEL_A_REVERSED, LABEL_B, LABEL_B_REVERSED, }; struct EdgeIndexBin { EdgeIndexBin() : start(0), stride(0), size(0) {} EdgeIndexBin(edges_size_type _start, const QRect &_rect, EdgeIndexBinId _binId) : start(_start), stride(_rect.width()), size(_rect.width() * _rect.height()), xOffset(_rect.x()), yOffset(_rect.y()), binId(_binId), isReversed(int(_binId) & 0x1), rect(_rect) {} EdgeIndexBin(const EdgeIndexBin &putAfter, const QRect &_rect, EdgeIndexBinId _binId) : start(putAfter.last() + 1), stride(_rect.width()), size(_rect.width() * _rect.height()), xOffset(_rect.x()), yOffset(_rect.y()), binId(_binId), isReversed(int(_binId) & 0x1), rect(_rect) {} edges_size_type last() const { return start + size - 1; } bool contains(edges_size_type index) const { index -= start; return index >= 0 && index < size; } bool contains(const edge_descriptor &edge) const { return indexOf(edge) >= 0; } edges_size_type indexOf(const edge_descriptor &edge) const { vertex_descriptor src_vertex = source(edge, *this); vertex_descriptor dst_vertex = target(edge, *this); const bool srcColoredA = src_vertex.type == vertex_descriptor::LABEL_A; const bool dstColoredA = dst_vertex.type == vertex_descriptor::LABEL_A; const bool srcColoredB = src_vertex.type == vertex_descriptor::LABEL_B; const bool dstColoredB = dst_vertex.type == vertex_descriptor::LABEL_B; if (srcColoredA || dstColoredA) { const bool edgeReversed = srcColoredA; if (isReversed != edgeReversed || (binId != LABEL_A && binId != LABEL_A_REVERSED) || (srcColoredA && (dst_vertex.type != vertex_descriptor::NORMAL)) || (dstColoredA && (src_vertex.type != vertex_descriptor::NORMAL))) { return -1; } } else if (srcColoredB || dstColoredB) { const bool edgeReversed = srcColoredB; if (isReversed != edgeReversed || (binId != LABEL_B && binId != LABEL_B_REVERSED) || (srcColoredB && (dst_vertex.type != vertex_descriptor::NORMAL)) || (dstColoredB && (src_vertex.type != vertex_descriptor::NORMAL))) { return -1; } } else { const vertices_size_type xDiff = dst_vertex.x - src_vertex.x; const vertices_size_type yDiff = dst_vertex.y - src_vertex.y; const vertices_size_type xAbsDiff = qAbs(xDiff); const vertices_size_type yAbsDiff = qAbs(yDiff); const bool edgeReversed = xDiff < 0 || yDiff < 0; if (isReversed != edgeReversed || (xDiff && binId != HORIZONTAL && binId != HORIZONTAL_REVERSED) || (yDiff && binId != VERTICAL && binId != VERTICAL_REVERSED) || xAbsDiff > 1 || yAbsDiff > 1 || xAbsDiff == yAbsDiff) { return -1; } } if (isReversed) { std::swap(src_vertex, dst_vertex); } // using direct QRect::contains makes the code 30% slower const int x = src_vertex.x; const int y = src_vertex.y; if (x < rect.x() || x > rect.right() || y < rect.y() || y > rect.bottom()) { return -1; } edges_size_type internalIndex = (src_vertex.x - xOffset) + (src_vertex.y - yOffset) * stride; LF_SANITY_ASSERT_RECOVER(internalIndex >= 0 && internalIndex < size) { return -1; } return internalIndex + start; } edge_descriptor edgeAt(edges_size_type index) const { edges_size_type localOffset = index - start; if (localOffset < 0 || localOffset >= size) { return edge_descriptor(); } const edges_size_type x = localOffset % stride + xOffset; const edges_size_type y = localOffset / stride + yOffset; vertex_descriptor src_vertex(x, y, vertex_descriptor::NORMAL); vertex_descriptor dst_vertex; switch (binId) { case HORIZONTAL: case HORIZONTAL_REVERSED: dst_vertex.x = x + 1; dst_vertex.y = y; dst_vertex.type = vertex_descriptor::NORMAL; break; case VERTICAL: case VERTICAL_REVERSED: dst_vertex.x = x; dst_vertex.y = y + 1; dst_vertex.type = vertex_descriptor::NORMAL; break; case LABEL_A: case LABEL_A_REVERSED: dst_vertex.type = vertex_descriptor::LABEL_A; break; case LABEL_B: case LABEL_B_REVERSED: dst_vertex.type = vertex_descriptor::LABEL_B; break; } if (isReversed) { std::swap(src_vertex, dst_vertex); } return std::make_pair(src_vertex, dst_vertex); } edges_size_type start; edges_size_type stride; edges_size_type size; edges_size_type xOffset; edges_size_type yOffset; EdgeIndexBinId binId; bool isReversed; QRect rect; }; QVector m_edgeBins; QRect m_aLabelArea; QRect m_bLabelArea; QRect m_mainArea; QVector m_aLabelRects; QVector m_bLabelRects; public: // Returns the number of vertices in the graph inline vertices_size_type num_vertices() const { return (m_numVertices); } // Returns the number of edges in the graph inline edges_size_type num_edges() const { return (m_numEdges); } // Returns the index of [vertex] (See also vertex_at) vertices_size_type index_of(vertex_descriptor vertex) const { vertices_size_type vertex_index = -1; switch (vertex.type) { case vertex_descriptor::NORMAL: vertex_index = vertex.x - m_x + (vertex.y - m_y) * m_width; break; case vertex_descriptor::LABEL_A: vertex_index = m_numVertices - 2; break; case vertex_descriptor::LABEL_B: vertex_index = m_numVertices - 1; break; } return vertex_index; } // Returns the vertex whose index is [vertex_index] (See also // index_of(vertex_descriptor)) vertex_descriptor vertex_at (vertices_size_type vertex_index) const { vertex_descriptor vertex; if (vertex_index == m_numVertices - 2) { vertex.type = vertex_descriptor::LABEL_A; } else if (vertex_index == m_numVertices - 1) { vertex.type = vertex_descriptor::LABEL_B; } else if (vertex_index >= 0) { vertex.x = vertex_index % m_width + m_x; vertex.y = vertex_index / m_width + m_y; vertex.type = vertex_descriptor::NORMAL; } return vertex; } // Returns the edge whose index is [edge_index] (See also // index_of(edge_descriptor)). NOTE: The index mapping is // dependent upon dimension wrapping. edge_descriptor edge_at(edges_size_type edge_index) const { int binIndex = 0; while (binIndex < m_edgeBins.size() && !m_edgeBins[binIndex].contains(edge_index)) { binIndex++; } if (binIndex >= m_edgeBins.size()) { return edge_descriptor(); } return m_edgeBins[binIndex].edgeAt(edge_index); } // Returns the index for [edge] (See also edge_at) edges_size_type index_of(edge_descriptor edge) const { edges_size_type index = -1; auto it = m_edgeBins.constBegin(); for (; it != m_edgeBins.constEnd(); ++it) { index = it->indexOf(edge); if (index >= 0) break; } return index; } private: static vertices_size_type numVacantEdges(const vertex_descriptor &vertex, const QRect &rc) { vertices_size_type vacantEdges = 4; if (vertex.x == rc.x()) { vacantEdges--; } if (vertex.y == rc.y()) { vacantEdges--; } if (vertex.x == rc.right()) { vacantEdges--; } if (vertex.y == rc.bottom()) { vacantEdges--; } return vacantEdges; } static inline bool findInRects(const QVector &rects, const QPoint &pt) { bool result = false; auto it = rects.constBegin(); for (; it != rects.constEnd(); ++it) { if (it->contains(pt)) { result = true; break; } } return result; } public: // Returns the number of out-edges for [vertex] degree_size_type out_degree(vertex_descriptor vertex) const { degree_size_type out_edge_count = 0; if (index_of(vertex) < 0) return out_edge_count; switch (vertex.type) { case vertex_descriptor::NORMAL: { out_edge_count = numVacantEdges(vertex, m_mainArea); const QPoint pt = QPoint(vertex.x, vertex.y); if (m_aLabelArea.contains(pt) && findInRects(m_aLabelRects, pt)) { out_edge_count++; } if (m_bLabelArea.contains(pt) && findInRects(m_bLabelRects, pt)) { out_edge_count++; } break; } case vertex_descriptor::LABEL_A: out_edge_count = m_numAEdges; break; case vertex_descriptor::LABEL_B: out_edge_count = m_numBEdges; break; } return (out_edge_count); } // Returns an out-edge for [vertex] by index. Indices are in the // range [0, out_degree(vertex)). edge_descriptor out_edge_at (vertex_descriptor vertex, degree_size_type out_edge_index) const { const QPoint pt = QPoint(vertex.x, vertex.y); vertex_descriptor dst_vertex = vertex; switch (vertex.type) { case vertex_descriptor::NORMAL: if (vertex.x > m_mainArea.x() && !out_edge_index--) { dst_vertex.x--; } else if (vertex.y > m_mainArea.y() && !out_edge_index--) { dst_vertex.y--; } else if (vertex.x < m_mainArea.right() && !out_edge_index--) { dst_vertex.x++; } else if (vertex.y < m_mainArea.bottom() && !out_edge_index--) { dst_vertex.y++; } else if (m_aLabelArea.contains(pt) && findInRects(m_aLabelRects, pt) && !out_edge_index--) { dst_vertex = vertex_descriptor(0, 0, vertex_descriptor::LABEL_A); } else if (m_bLabelArea.contains(pt) && findInRects(m_bLabelRects, pt) && !out_edge_index--) { dst_vertex = vertex_descriptor(0, 0, vertex_descriptor::LABEL_B); } else { dbgImage << ppVar(vertex) << ppVar(out_edge_index) << ppVar(out_degree(vertex)); qFatal("Wrong edge sub-index"); } break; case vertex_descriptor::LABEL_A: { edge_descriptor edge = edge_at(m_aReversedEdgesStart + out_edge_index); dst_vertex = edge.second; break; } case vertex_descriptor::LABEL_B: { edge_descriptor edge = edge_at(m_bReversedEdgesStart + out_edge_index); dst_vertex = edge.second; break; } } return std::make_pair(vertex, dst_vertex); } public: //================ // VertexListGraph //================ friend inline std::pair vertices(const type& graph) { typedef typename type::vertex_iterator vertex_iterator; typedef typename type::vertex_function vertex_function; typedef typename type::vertex_index_iterator vertex_index_iterator; return (std::make_pair (vertex_iterator(vertex_index_iterator(0), vertex_function(&graph)), vertex_iterator(vertex_index_iterator(graph.num_vertices()), vertex_function(&graph)))); } friend inline typename type::vertices_size_type num_vertices(const type& graph) { return (graph.num_vertices()); } friend inline typename type::vertex_descriptor vertex(typename type::vertices_size_type vertex_index, const type& graph) { return (graph.vertex_at(vertex_index)); } //=============== // IncidenceGraph //=============== friend inline std::pair out_edges(typename type::vertex_descriptor vertex, const type& graph) { typedef typename type::degree_iterator degree_iterator; typedef typename type::out_edge_function out_edge_function; typedef typename type::out_edge_iterator out_edge_iterator; return (std::make_pair (out_edge_iterator(degree_iterator(0), out_edge_function(vertex, &graph)), out_edge_iterator(degree_iterator(graph.out_degree(vertex)), out_edge_function(vertex, &graph)))); } friend inline typename type::degree_size_type out_degree (typename type::vertex_descriptor vertex, const type& graph) { return (graph.out_degree(vertex)); } friend inline typename type::edge_descriptor out_edge_at(typename type::vertex_descriptor vertex, typename type::degree_size_type out_edge_index, const type& graph) { return (graph.out_edge_at(vertex, out_edge_index)); } //=============== // AdjacencyGraph //=============== friend typename std::pair adjacent_vertices (typename type::vertex_descriptor vertex, const type& graph) { typedef typename type::degree_iterator degree_iterator; typedef typename type::adjacent_vertex_function adjacent_vertex_function; typedef typename type::adjacency_iterator adjacency_iterator; return (std::make_pair (adjacency_iterator(degree_iterator(0), adjacent_vertex_function(vertex, &graph)), adjacency_iterator(degree_iterator(graph.out_degree(vertex)), adjacent_vertex_function(vertex, &graph)))); } //================== // Adjacency Matrix //================== friend std::pair edge (typename type::vertex_descriptor source_vertex, typename type::vertex_descriptor destination_vertex, const type& graph) { std::pair edge_exists = std::make_pair(std::make_pair(source_vertex, destination_vertex), false); const edges_size_type index = graph.index_of(edge_exists.first); edge_exists.second = index >= 0; return edge_exists; } //============== // EdgeListGraph //============== friend inline typename type::edges_size_type num_edges(const type& graph) { return (graph.num_edges()); } friend inline typename type::edge_descriptor edge_at(typename type::edges_size_type edge_index, const type& graph) { return (graph.edge_at(edge_index)); } friend inline std::pair edges(const type& graph) { typedef typename type::edge_index_iterator edge_index_iterator; typedef typename type::edge_function edge_function; typedef typename type::edge_iterator edge_iterator; return (std::make_pair (edge_iterator(edge_index_iterator(0), edge_function(&graph)), edge_iterator(edge_index_iterator(graph.num_edges()), edge_function(&graph)))); } //============================= // Index Property Map Functions //============================= friend inline typename type::vertices_size_type get(vertex_index_t, const type& graph, typename type::vertex_descriptor vertex) { type::vertices_size_type index = graph.index_of(vertex); LF_SANITY_ASSERT(index >= 0); return index; } friend inline typename type::edges_size_type get(edge_index_t, const type& graph, typename type::edge_descriptor edge) { type::edges_size_type index = graph.index_of(edge); LF_SANITY_ASSERT(index >= 0); return index; } friend inline lazy_fill_graph_index_map< type, typename type::vertex_descriptor, typename type::vertices_size_type> get(vertex_index_t, const type& graph) { return (lazy_fill_graph_index_map< type, typename type::vertex_descriptor, typename type::vertices_size_type>(graph)); } friend inline lazy_fill_graph_index_map< type, typename type::edge_descriptor, typename type::edges_size_type> get(edge_index_t, const type& graph) { return (lazy_fill_graph_index_map< type, typename type::edge_descriptor, typename type::edges_size_type>(graph)); } friend inline lazy_fill_graph_reverse_edge_map< typename type::edge_descriptor> get(edge_reverse_t, const type& graph) { Q_UNUSED(graph); return (lazy_fill_graph_reverse_edge_map< typename type::edge_descriptor>()); } template friend struct lazy_fill_graph_index_map; template friend struct lazy_fill_graph_reverse_edge_map; }; namespace boost { template <> struct property_map { typedef lazy_fill_graph_index_map::vertex_descriptor, typename graph_traits::vertices_size_type> type; typedef type const_type; }; template<> struct property_map { typedef lazy_fill_graph_index_map::edge_descriptor, typename graph_traits::edges_size_type> type; typedef type const_type; }; } namespace boost { template <> struct property_map { typedef lazy_fill_graph_reverse_edge_map::edge_descriptor> type; typedef type const_type; }; } QDebug operator<<(QDebug dbg, const KisLazyFillGraph::vertex_descriptor &v) { const QString type = v.type == KisLazyFillGraph::vertex_descriptor::NORMAL ? "normal" : v.type == KisLazyFillGraph::vertex_descriptor::LABEL_A ? "label_A" : v.type == KisLazyFillGraph::vertex_descriptor::LABEL_B ? "label_B" : ""; dbg.nospace() << "(" << v.x << ", " << v.y << ", " << type << ")"; return dbg.space(); } QDebug operator<<(QDebug dbg, const KisLazyFillGraph::edge_descriptor &e) { KisLazyFillGraph::vertex_descriptor src = e.first; KisLazyFillGraph::vertex_descriptor dst = e.second; dbg.nospace() << "(" << src << " -> " << dst << ")"; return dbg.space(); } #endif /* __KIS_LAZY_FILL_GRAPH_H */ diff --git a/plugins/tools/selectiontools/CMakeLists.txt b/plugins/tools/selectiontools/CMakeLists.txt index c81049076c..756cab5959 100644 --- a/plugins/tools/selectiontools/CMakeLists.txt +++ b/plugins/tools/selectiontools/CMakeLists.txt @@ -1,30 +1,37 @@ +add_subdirectory(tests) + set(kritaselectiontools_SOURCES selection_tools.cc kis_tool_select_rectangular.cc kis_tool_select_polygonal.cc kis_tool_select_elliptical.cc kis_tool_select_contiguous.cc kis_tool_select_outline.cc kis_tool_select_path.cc kis_tool_select_similar.cc kis_selection_modifier_mapper.cc + KisMagneticWorker.cc + KisToolSelectMagnetic.cc ) qt5_add_resources(kritaselectiontools_SOURCES selectiontools.qrc) add_library(kritaselectiontools MODULE ${kritaselectiontools_SOURCES}) -target_link_libraries(kritaselectiontools kritaui kritabasicflakes) +generate_export_header(kritaselectiontools BASE_NAME kritaselectiontools) + +target_link_libraries(kritaselectiontools kritaui kritabasicflakes kritaimage) install(TARGETS kritaselectiontools DESTINATION ${KRITA_PLUGIN_INSTALL_DIR}) install( FILES KisToolSelectPolygonal.action KisToolSelectElliptical.action KisToolSelectSimilar.action KisToolSelectContiguous.action KisToolSelectRectangular.action KisToolSelectOutline.action KisToolSelectPath.action + KisToolSelectMagnetic.action DESTINATION ${DATA_INSTALL_DIR}/krita/actions ) diff --git a/plugins/tools/selectiontools/KisMagneticGraph.h b/plugins/tools/selectiontools/KisMagneticGraph.h new file mode 100644 index 0000000000..a49b456ae6 --- /dev/null +++ b/plugins/tools/selectiontools/KisMagneticGraph.h @@ -0,0 +1,289 @@ +/* + * Copyright (c) 2019 Kuntal Majumder + * + * This library is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef KISMAGNETICGRAPH_H +#define KISMAGNETICGRAPH_H + +#include +#include +#include +#include + +#include +#include +#include + +struct VertexDescriptor { + long x, y; + + enum Direction { + MIN = 0, + N = MIN, S, E, W, NW, NE, SW, SE, NONE + }; + + VertexDescriptor(long _x, long _y) : + x(_x), y(_y) + { } + + VertexDescriptor(QPoint pt) : + x(pt.x()), y(pt.y()) + { } + + VertexDescriptor() : + x(0), y(0) + { } + + bool operator == (VertexDescriptor const &rhs) const + { + return rhs.x == x && rhs.y == y; + } + + bool operator == (QPoint const &rhs) const + { + return rhs.x() == x && rhs.y() == y; + } + + bool operator != (VertexDescriptor const &rhs) const + { + return rhs.x != x || rhs.y != y; + } + + bool operator < (VertexDescriptor const &rhs) const + { + return x < rhs.x || (x == rhs.x && y < rhs.y); + } + + // returns one of the 8 neighboring pixel based on the direction + // it gives out multiple warnings, but I am lazy, sorry + VertexDescriptor neighbor(Direction direction) const + { + int dx = 0, dy = 0; + + switch (direction) { + case W: + Q_FALLTHROUGH(); + case SW: + Q_FALLTHROUGH(); + case NW: + dx = -1; + break; + case E: + Q_FALLTHROUGH(); + case SE: + Q_FALLTHROUGH(); + case NE: + dx = 1; + default: + ; + } + + switch (direction) { + case N: + Q_FALLTHROUGH(); + case NW: + Q_FALLTHROUGH(); + case NE: + dy = -1; + break; + case S: + Q_FALLTHROUGH(); + case SW: + Q_FALLTHROUGH(); + case SE: + dy = 1; + default: + ; + } + + VertexDescriptor const neighbor(x + dx, y + dy); + return neighbor; + } // neighbor +}; + +QDebug operator << (QDebug dbg, const VertexDescriptor &v) +{ + dbg.nospace() << "(" << v.x << ", " << v.y << ")"; + return dbg.space(); +} + +struct neighbour_iterator; + +struct KisMagneticGraph { + typedef KisMagneticGraph type; + + KisMagneticGraph(){ } + + KisMagneticGraph(KisPaintDeviceSP dev) : + m_dev(dev) + { + m_randAccess = m_dev->createRandomAccessorNG(m_dev->exactBounds().x(), m_dev->exactBounds().y()); + } + + KisMagneticGraph(KisPaintDeviceSP dev, QRect graphRect) : + m_rect(graphRect), m_dev(dev) + { + m_randAccess = m_dev->createRandomAccessorNG(m_dev->exactBounds().x(), m_dev->exactBounds().y()); + } + + typedef VertexDescriptor vertex_descriptor; + typedef std::pair edge_descriptor; + typedef boost::undirected_tag directed_category; + typedef boost::disallow_parallel_edge_tag edge_parallel_category; + typedef boost::incidence_graph_tag traversal_category; + typedef neighbour_iterator out_edge_iterator; + typedef unsigned degree_size_type; + + + quint8 getIntensity(VertexDescriptor pt) + { + m_randAccess->moveTo(pt.x, pt.y); + quint8 val = *(m_randAccess->rawData()); + return val; + } + + unsigned outDegree(VertexDescriptor pt) + { + // corners + if (pt == m_rect.topLeft() || pt == m_rect.topRight() || + pt == m_rect.bottomLeft() || pt == m_rect.bottomRight()) + { + if (m_rect.width() == 1 || m_rect.height() == 1) + return 1; + + return 3; + } + + // edges + if (pt.x == m_rect.topLeft().x() || pt.y == m_rect.topLeft().y() || + pt.x == m_rect.bottomRight().x() || pt.y == m_rect.bottomRight().y()) + { + if (m_rect.width() == 1 || m_rect.height() == 1) + return 2; + + return 5; + } + return 8; + } + + QRect m_rect; + +private: + KisPaintDeviceSP m_dev; + KisRandomAccessorSP m_randAccess; +}; + +struct neighbour_iterator : public boost::iterator_facade + , boost::forward_traversal_tag + , std::pair > { + neighbour_iterator(VertexDescriptor v, KisMagneticGraph g, VertexDescriptor::Direction d) : + m_point(v), m_direction(d), m_graph(g) + { } + + neighbour_iterator() + { } + + std::pair operator * () const + { + std::pair const result = std::make_pair(m_point, m_point.neighbor(m_direction)); + return result; + } + + void operator ++ () + { + m_direction = static_cast(int(m_direction) + 1); + VertexDescriptor next = m_point.neighbor(m_direction); + if (m_direction == VertexDescriptor::NONE) { + return; + } + if (!m_graph.m_rect.contains(next.x, next.y)) { + operator ++ (); + } + } + + bool operator == (neighbour_iterator const& that) const + { + return m_point == that.m_point && m_direction == that.m_direction; + } + + bool equal(neighbour_iterator const& that) const + { + return operator == (that); + } + + void increment() + { + operator ++ (); + } + +private: + VertexDescriptor m_point; + VertexDescriptor::Direction m_direction; + KisMagneticGraph m_graph; +}; + +// Requirements for an Incidence Graph, +// https://www.boost.org/doc/libs/1_70_0/libs/graph/doc/IncidenceGraph.html + +namespace boost { +template <> +struct graph_traits { + typedef typename KisMagneticGraph::vertex_descriptor vertex_descriptor; + typedef typename KisMagneticGraph::edge_descriptor edge_descriptor; + typedef typename KisMagneticGraph::out_edge_iterator out_edge_iterator; + typedef typename KisMagneticGraph::directed_category directed_category; + typedef typename KisMagneticGraph::edge_parallel_category edge_parallel_category; + typedef typename KisMagneticGraph::traversal_category traversal_category; + typedef typename KisMagneticGraph::degree_size_type degree_size_type; + + typedef void in_edge_iterator; + typedef void vertex_iterator; + typedef void vertices_size_type; + typedef void edge_iterator; + typedef void edges_size_type; +}; +} + +typename KisMagneticGraph::vertex_descriptor source(typename KisMagneticGraph::edge_descriptor e, KisMagneticGraph g) +{ + Q_UNUSED(g) + return e.first; +} + +typename KisMagneticGraph::vertex_descriptor target(typename KisMagneticGraph::edge_descriptor e, KisMagneticGraph g) +{ + Q_UNUSED(g) + return e.second; +} + +std::pair out_edges( + typename KisMagneticGraph::vertex_descriptor v, KisMagneticGraph g) +{ + return std::make_pair( + KisMagneticGraph::out_edge_iterator(v, g, VertexDescriptor::Direction::MIN), + KisMagneticGraph::out_edge_iterator(v, g, VertexDescriptor::Direction::NONE) + ); +} + +typename KisMagneticGraph::degree_size_type out_degree(typename KisMagneticGraph::vertex_descriptor v, + KisMagneticGraph g) +{ + return g.outDegree(v); +} + +#endif // ifndef KISMAGNETICGRAPH_H diff --git a/plugins/tools/selectiontools/KisMagneticWorker.cc b/plugins/tools/selectiontools/KisMagneticWorker.cc new file mode 100644 index 0000000000..85026f5341 --- /dev/null +++ b/plugins/tools/selectiontools/KisMagneticWorker.cc @@ -0,0 +1,284 @@ +/* + * Copyright (c) 2019 Kuntal Majumder + * + * This library is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "KisMagneticWorker.h" + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include "KisMagneticGraph.h" + +struct DistanceMap { + typedef VertexDescriptor key_type; + typedef double data_type; + typedef std::pair value_type; + + explicit DistanceMap(double const &dval) + : m_default(dval) + { } + + data_type &operator [] (key_type const &k) + { + if (m.find(k) == m.end()) + m[k] = m_default; + return m[k]; + } + +private: + std::map m; + data_type const m_default; +}; + +struct PredecessorMap { + PredecessorMap() = default; + + PredecessorMap(PredecessorMap const &that) = default; + + typedef VertexDescriptor key_type; + typedef VertexDescriptor value_type; + typedef boost::read_write_property_map_tag category; + + VertexDescriptor &operator [] (VertexDescriptor v) + { + return m_map[v]; + } + + std::map m_map; +}; + +VertexDescriptor get(PredecessorMap const &m, VertexDescriptor v) +{ + auto found = m.m_map.find(v); + return found != m.m_map.end() ? found->second : v; +} + +void put(PredecessorMap &m, VertexDescriptor key, VertexDescriptor value) +{ + m.m_map[key] = value; +} + +double EuclideanDistance(VertexDescriptor p1, VertexDescriptor p2) +{ + return std::sqrt(std::pow(p1.y - p2.y, 2) + std::pow(p1.x - p2.x, 2)); +} + +class AStarHeuristic : public boost::astar_heuristic +{ +private: + VertexDescriptor m_goal; + +public: + explicit AStarHeuristic(VertexDescriptor goal) : + m_goal(goal) + { } + + double operator () (VertexDescriptor v) + { + return EuclideanDistance(v, m_goal); + } +}; + +struct GoalFound { }; + +class AStarGoalVisitor : public boost::default_astar_visitor +{ +public: + explicit AStarGoalVisitor(VertexDescriptor goal) : m_goal(goal){ } + + void examine_vertex(VertexDescriptor u, KisMagneticGraph const &g) + { + Q_UNUSED(g) + if (u == m_goal) { + throw GoalFound(); + } + } + +private: + VertexDescriptor m_goal; +}; + +struct WeightMap { + typedef std::pair key_type; + typedef double data_type; + typedef std::pair value_type; + + WeightMap() = default; + + explicit WeightMap(const KisMagneticGraph &g) : + m_graph(g) + { } + + data_type &operator [] (key_type const &k) + { + if (m_map.find(k) == m_map.end()) { + double edge_gradient = (m_graph.getIntensity(k.first) + m_graph.getIntensity(k.second)) / 2; + m_map[k] = EuclideanDistance(k.first, k.second) + 255.0 - edge_gradient; + } + return m_map[k]; + } + +private: + std::map m_map; + KisMagneticGraph m_graph; +}; + +KisMagneticLazyTiles::KisMagneticLazyTiles(KisPaintDeviceSP dev) +{ + m_dev = KisPainter::convertToAlphaAsGray(dev); + QSize s = m_dev->exactBounds().size(); + m_tileSize = KritaUtils::optimalPatchSize(); + m_tilesPerRow = std::ceil((double) s.width() / (double) m_tileSize.width()); + int tilesPerColumn = std::ceil((double) s.height() / (double) m_tileSize.height()); + m_dev->setDefaultBounds(dev->defaultBounds()); + + for (int i = 0; i < tilesPerColumn; i++) { + for (int j = 0; j < m_tilesPerRow; j++) { + int width = std::min(m_dev->exactBounds().width() - j * m_tileSize.width(), m_tileSize.width()); + int height = std::min(m_dev->exactBounds().height() - i * m_tileSize.height(), m_tileSize.height()); + QRect temp(j *m_tileSize.width(), i *m_tileSize.height(), width, height); + m_tiles.push_back(temp); + } + } + + m_radiusRecord = QVector(m_tiles.size(), -1); +} + +void KisMagneticLazyTiles::filter(qreal radius, QRect &rect) +{ + auto divide = [](QPoint p, QSize s){ + return QPoint(p.x() / s.width(), p.y() / s.height()); + }; + + QPoint firstTile = divide(rect.topLeft(), m_tileSize); + QPoint lastTile = divide(rect.bottomRight(), m_tileSize); + for (int i = firstTile.y(); i <= lastTile.y(); i++) { + for (int j = firstTile.x(); j <= lastTile.x(); j++) { + int currentTile = i * m_tilesPerRow + j; + if (radius != m_radiusRecord[currentTile]) { + QRect bounds = m_tiles[currentTile]; + KisGaussianKernel::applyTightLoG(m_dev, bounds, radius, -1.0, QBitArray(), nullptr); + KisLazyFillTools::normalizeAlpha8Device(m_dev, bounds); + m_radiusRecord[currentTile] = radius; + } + } + } +} + +KisMagneticWorker::KisMagneticWorker(const KisPaintDeviceSP &dev) : + m_lazyTileFilter(dev) +{ } + +QVector KisMagneticWorker::computeEdge(int bounds, QPoint begin, QPoint end, qreal radius) +{ + QRect rect; + KisAlgebra2D::accumulateBounds(QVector { begin, end }, &rect); + rect = kisGrowRect(rect, bounds); + + m_lazyTileFilter.filter(radius, rect); + + VertexDescriptor goal(end); + VertexDescriptor start(begin); + + m_graph = new KisMagneticGraph(m_lazyTileFilter.device(), rect); + + // How many maps does it require? + // Take a look here, if it doesn't make sense, https://www.boost.org/doc/libs/1_70_0/libs/graph/doc/astar_search.html + PredecessorMap pmap; + DistanceMap dmap(std::numeric_limits::max()); + dmap[start] = 0; + std::map rmap; + std::map cmap; + std::map imap; + WeightMap wmap(*m_graph); + AStarHeuristic heuristic(goal); + QVector result; + + try { + boost::astar_search_no_init( + *m_graph, start, heuristic, + boost::visitor(AStarGoalVisitor(goal)) + .distance_map(boost::associative_property_map(dmap)) + .predecessor_map(boost::ref(pmap)) + .weight_map(boost::associative_property_map(wmap)) + .vertex_index_map(boost::associative_property_map >(imap)) + .rank_map(boost::associative_property_map >(rmap)) + .color_map(boost::associative_property_map > + (cmap)) + .distance_combine(std::plus()) + .distance_compare(std::less()) + ); + } catch (GoalFound const &) { + for (VertexDescriptor u = goal; u != start; u = pmap[u]) { + result.push_front(QPointF(u.x, u.y)); + } + } + + result.push_front(QPoint(start.x, start.y)); + + return result; +} // KisMagneticWorker::computeEdge + +qreal KisMagneticWorker::intensity(QPoint pt) +{ + return m_graph->getIntensity(VertexDescriptor(pt)); +} + +void KisMagneticWorker::saveTheImage(vQPointF points) +{ + QImage img = m_lazyTileFilter.device()->convertToQImage(nullptr, m_lazyTileFilter.device()->exactBounds()); + + const QPointF offset = m_lazyTileFilter.device()->exactBounds().topLeft(); + for (QPointF &pt : points) { + pt -= offset; + } + + img = img.convertToFormat(QImage::Format_ARGB32); + QPainter gc(&img); + + QPainterPath path; + + for (int i = 0; i < points.size(); i++) { + if (i == 0) { + path.moveTo(points[i]); + } else { + path.lineTo(points[i]); + } + } + + gc.setPen(Qt::blue); + gc.drawPath(path); + + gc.setPen(Qt::green); + gc.drawEllipse(points[0], 3, 3); + gc.setPen(Qt::red); + gc.drawEllipse(points[points.count() - 1], 2, 2); + + for (QRect &r : m_lazyTileFilter.tiles() ) { + gc.drawRect(r); + } + + img.save("result.png"); +} // KisMagneticWorker::saveTheImage diff --git a/plugins/tools/selectiontools/KisMagneticWorker.h b/plugins/tools/selectiontools/KisMagneticWorker.h new file mode 100644 index 0000000000..3ceed4fdf6 --- /dev/null +++ b/plugins/tools/selectiontools/KisMagneticWorker.h @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2019 Kuntal Majumder + * + * This library is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef KISMAGNETICWORKER_H +#define KISMAGNETICWORKER_H + +#include +#include + +class KisMagneticGraph; + +class KisMagneticLazyTiles { +private: + QVector m_tiles; + QVector m_radiusRecord; + KisPaintDeviceSP m_dev; + QSize m_tileSize; + int m_tilesPerRow; + +public: + KisMagneticLazyTiles(KisPaintDeviceSP dev); + void filter(qreal radius, QRect &rect); + inline KisPaintDeviceSP device(){ return m_dev; } + inline QVector tiles(){ return m_tiles; } +}; + +class KRITASELECTIONTOOLS_EXPORT KisMagneticWorker { +public: + KisMagneticWorker(const KisPaintDeviceSP &dev); + + QVector computeEdge(int bounds, QPoint start, QPoint end, qreal radius); + void saveTheImage(vQPointF points); + qreal intensity(QPoint pt); + +private: + KisMagneticLazyTiles m_lazyTileFilter; + KisMagneticGraph *m_graph; +}; + +#endif // ifndef KISMAGNETICWORKER_H diff --git a/plugins/tools/selectiontools/KisToolSelectMagnetic.action b/plugins/tools/selectiontools/KisToolSelectMagnetic.action new file mode 100644 index 0000000000..fe0fbb9f7c --- /dev/null +++ b/plugins/tools/selectiontools/KisToolSelectMagnetic.action @@ -0,0 +1,6 @@ + + + + Magnetic Selection Tool + + diff --git a/plugins/tools/selectiontools/KisToolSelectMagnetic.cc b/plugins/tools/selectiontools/KisToolSelectMagnetic.cc new file mode 100644 index 0000000000..a3fb1cb4a7 --- /dev/null +++ b/plugins/tools/selectiontools/KisToolSelectMagnetic.cc @@ -0,0 +1,719 @@ +/* + * Copyright (c) 2019 Kuntal Majumder + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "KisToolSelectMagnetic.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "kis_painter.h" +#include +#include "canvas/kis_canvas2.h" +#include "kis_pixel_selection.h" +#include "kis_selection_tool_helper.h" + +#include "kis_algebra_2d.h" + +#include "KisHandlePainterHelper.h" + +#include + +#define FEEDBACK_LINE_WIDTH 2 + +KisToolSelectMagnetic::KisToolSelectMagnetic(KoCanvasBase *canvas) + : KisToolSelect(canvas, + KisCursor::load("tool_magnetic_selection_cursor.png", 5, 5), + i18n("Magnetic Selection")), + m_continuedMode(false), m_complete(false), m_selected(false), m_finished(false), + m_worker(image()->projection()), m_threshold(70), m_searchRadius(30), m_anchorGap(30), + m_filterRadius(3.0), m_mouseHoverCompressor(100, KisSignalCompressor::FIRST_ACTIVE) + +{ } + +void KisToolSelectMagnetic::keyPressEvent(QKeyEvent *event) +{ + if (event->key() == Qt::Key_Control) { + m_continuedMode = true; + } + + KisToolSelect::keyPressEvent(event); +} + +/* + * Calculates the checkpoints responsible to determining the last point from where + * the edge is calculated. + * Takes 3 point, min, median and max, searches for an edge point from median to max, if fails, + * searches for the same from median to min, if fails, median becomes that edge point. + */ +void KisToolSelectMagnetic::calculateCheckPoints(vQPointF points) +{ + qreal totalDistance = 0.0; + int checkPoint = 0; + int finalPoint = 2; + int midPoint = 1; + int minPoint = 0; + qreal maxFactor = 2; + + for (; finalPoint < points.count(); finalPoint++) { + totalDistance += kisDistance(points[finalPoint], points[finalPoint - 1]); + + if (totalDistance <= m_anchorGap / 3) { + minPoint = finalPoint; + } + + if (totalDistance <= m_anchorGap) { + midPoint = finalPoint; + } + + if (totalDistance > maxFactor * m_anchorGap) { + break; + } + } + + if (totalDistance > maxFactor * m_anchorGap) { + bool foundSomething = false; + + for (int i = midPoint; i < finalPoint; i++) { + if (m_worker.intensity(points.at(i).toPoint()) >= m_threshold) { + m_lastAnchor = points.at(i).toPoint(); + m_anchorPoints.push_back(m_lastAnchor); + + vQPointF temp; + for (int j = 0; j <= i; j++) { + temp.push_back(points[j]); + } + + m_pointCollection.push_back(temp); + foundSomething = true; + checkPoint = i; + break; + } + } + + if (!foundSomething) { + for (int i = midPoint - 1; i >= minPoint; i--) { + if (m_worker.intensity(points.at(i).toPoint()) >= m_threshold) { + m_lastAnchor = points.at(i).toPoint(); + m_anchorPoints.push_back(m_lastAnchor); + vQPointF temp; + for (int j = midPoint - 1; j >= i; j--) { + temp.push_front(points[j]); + } + + m_pointCollection.push_back(temp); + foundSomething = true; + checkPoint = i; + break; + } + } + } + + if (!foundSomething) { + m_lastAnchor = points[midPoint].toPoint(); + m_anchorPoints.push_back(m_lastAnchor); + vQPointF temp; + + for (int j = 0; j <= midPoint; j++) { + temp.push_back(points[j]); + } + + m_pointCollection.push_back(temp); + checkPoint = midPoint; + foundSomething = true; + } + } + + totalDistance = 0.0; + reEvaluatePoints(); + + for (; finalPoint < points.count(); finalPoint++) { + totalDistance += kisDistance(points[finalPoint], points[checkPoint]); + if (totalDistance > maxFactor * m_anchorGap) { + points.remove(0, checkPoint + 1); + calculateCheckPoints(points); + break; + } + } +} // KisToolSelectMagnetic::calculateCheckPoints + +void KisToolSelectMagnetic::keyReleaseEvent(QKeyEvent *event) +{ + if (event->key() == Qt::Key_Control || + !(event->modifiers() & Qt::ControlModifier)) + { + m_continuedMode = false; + if (mode() != PAINT_MODE && !m_points.isEmpty()) { + finishSelectionAction(); + } + } + + KisToolSelect::keyReleaseEvent(event); +} + +vQPointF KisToolSelectMagnetic::computeEdgeWrapper(QPoint a, QPoint b) +{ + return m_worker.computeEdge(m_searchRadius, a, b, m_filterRadius); +} + +// the cursor is still tracked even when no mousebutton is pressed +void KisToolSelectMagnetic::mouseMoveEvent(KoPointerEvent *event) +{ + m_lastCursorPos = convertToPixelCoord(event); + KisToolSelect::mouseMoveEvent(event); + updatePaintPath(); +} // KisToolSelectMagnetic::mouseMoveEvent + +// press primary mouse button +void KisToolSelectMagnetic::beginPrimaryAction(KoPointerEvent *event) +{ + setMode(KisTool::PAINT_MODE); + QPointF temp(convertToPixelCoord(event)); + m_cursorOnPress = temp; + + if (!image()->bounds().contains(temp.toPoint())) { + return; + } + + checkIfAnchorIsSelected(temp); + + if (m_complete || m_selected) { + return; + } + + if (m_anchorPoints.count() != 0) { + vQPointF edge = computeEdgeWrapper(m_anchorPoints.last(), temp.toPoint()); + m_points.append(edge); + m_pointCollection.push_back(edge); + } else { + updateInitialAnchorBounds(temp.toPoint()); + } + + m_lastAnchor = temp.toPoint(); + m_anchorPoints.push_back(m_lastAnchor); + m_lastCursorPos = temp; + reEvaluatePoints(); + updateCanvasPixelRect(image()->bounds()); +} // KisToolSelectMagnetic::beginPrimaryAction + +void KisToolSelectMagnetic::checkIfAnchorIsSelected(QPointF temp) +{ + Q_FOREACH (const QPoint pt, m_anchorPoints) { + qreal zoomLevel = canvas()->viewConverter()->zoom(); + int sides = (int) std::ceil(10.0 / zoomLevel); + QRect r = QRect(QPoint(0, 0), QSize(sides, sides)); + r.moveCenter(pt); + if (r.contains(temp.toPoint())) { + m_selected = true; + m_selectedAnchor = m_anchorPoints.lastIndexOf(pt); + return; + } + } +} + +void KisToolSelectMagnetic::beginPrimaryDoubleClickAction(KoPointerEvent *event) +{ + QPointF temp = convertToPixelCoord(event); + checkIfAnchorIsSelected(temp); + + if (m_selected) { + deleteSelectedAnchor(); + return; + } + + if (m_complete) { + if (!image()->bounds().contains(temp.toPoint())) { + return; + } + + int pointA = 0, pointB = 1; + double dist = std::numeric_limits::max(); + int total = m_anchorPoints.count(); + for (int i = 0; i < total; i++) { + double distToCompare = kisDistance(m_anchorPoints[i], temp) + + kisDistance(temp, m_anchorPoints[(i + 1) % total]); + if (dist > distToCompare) { + pointA = i; + pointB = (i + 1) % total; + dist = distToCompare; + } + } + + vQPointF path1 = computeEdgeWrapper(m_anchorPoints[pointA], temp.toPoint()); + vQPointF path2 = computeEdgeWrapper(temp.toPoint(), m_anchorPoints[pointB]); + + m_pointCollection[pointA] = path1; + m_pointCollection.insert(pointB, path2); + m_anchorPoints.insert(pointB, temp.toPoint()); + + reEvaluatePoints(); + } +} // KisToolSelectMagnetic::beginPrimaryDoubleClickAction + +// drag while primary mouse button is pressed +void KisToolSelectMagnetic::continuePrimaryAction(KoPointerEvent *event) +{ + if (m_selected) { + m_anchorPoints[m_selectedAnchor] = convertToPixelCoord(event).toPoint(); + } else { + m_lastCursorPos = convertToPixelCoord(event); + m_mouseHoverCompressor.start(); + } + KisToolSelectBase::continuePrimaryAction(event); +} + +void KisToolSelectMagnetic::slotCalculateEdge() +{ + QPoint current = m_lastCursorPos.toPoint(); + vQPointF pointSet = computeEdgeWrapper(m_lastAnchor, current); + calculateCheckPoints(pointSet); +} + +// release primary mouse button +void KisToolSelectMagnetic::endPrimaryAction(KoPointerEvent *event) +{ + if (m_selected && convertToPixelCoord(event) != m_cursorOnPress) { + if (!image()->bounds().contains(m_anchorPoints[m_selectedAnchor])) { + deleteSelectedAnchor(); + } else { + updateSelectedAnchor(); + } + } else if (m_selected) { + QPointF temp(convertToPixelCoord(event)); + if (m_snapBound.contains(temp) && m_anchorPoints.count() > 1) { + vQPointF edge = computeEdgeWrapper(m_anchorPoints.last(), temp.toPoint()); + m_points.append(edge); + m_pointCollection.push_back(edge); + m_complete = true; + } + } + if (m_mouseHoverCompressor.isActive()) { + m_mouseHoverCompressor.stop(); + slotCalculateEdge(); + } + m_selected = false; + KisToolSelectBase::endPrimaryAction(event); +} // KisToolSelectMagnetic::endPrimaryAction + +void KisToolSelectMagnetic::deleteSelectedAnchor() +{ + if (m_anchorPoints.isEmpty()) + return; + + // if it is the initial anchor + if (m_selectedAnchor == 0) { + m_anchorPoints.pop_front(); + if (m_anchorPoints.isEmpty()) { + // it was the only point lol + reEvaluatePoints(); + return; + } + m_pointCollection.pop_front(); + if (m_complete) { + m_pointCollection[0] = computeEdgeWrapper(m_anchorPoints.first(), m_anchorPoints.last()); + } + reEvaluatePoints(); + return; + } + + // if it is the last anchor + if (m_selectedAnchor == m_anchorPoints.count() - 1) { + m_anchorPoints.pop_back(); + m_pointCollection.pop_back(); + if (m_complete) { + m_pointCollection[m_selectedAnchor] = computeEdgeWrapper(m_anchorPoints.last(), m_anchorPoints.first()); + } + reEvaluatePoints(); + return; + } + + // it is in the middle + m_anchorPoints.remove(m_selectedAnchor); + m_pointCollection.remove(m_selectedAnchor); + m_pointCollection[m_selectedAnchor - 1] = computeEdgeWrapper(m_anchorPoints[m_selectedAnchor - 1], + m_anchorPoints[m_selectedAnchor]); + reEvaluatePoints(); +} // KisToolSelectMagnetic::deleteSelectedAnchor + +void KisToolSelectMagnetic::updateSelectedAnchor() +{ + // initial + if (m_selectedAnchor == 0 && m_anchorPoints.count() > 1) { + m_pointCollection[m_selectedAnchor] = computeEdgeWrapper(m_anchorPoints[0], m_anchorPoints[1]); + if (m_complete) { + m_pointCollection[m_anchorPoints.count() - 1] = computeEdgeWrapper(m_anchorPoints.last(), + m_anchorPoints.first()); + } + reEvaluatePoints(); + return; + } + + // last + if (m_selectedAnchor == m_anchorPoints.count() - 1) { + m_pointCollection[m_selectedAnchor - 1] = computeEdgeWrapper(m_anchorPoints[m_selectedAnchor - 1], + m_anchorPoints.last()); + if (m_complete) { + m_pointCollection[m_selectedAnchor] = computeEdgeWrapper(m_anchorPoints.last(), m_anchorPoints.first()); + } + reEvaluatePoints(); + return; + } + + // middle + m_pointCollection[m_selectedAnchor - 1] = computeEdgeWrapper(m_anchorPoints[m_selectedAnchor - 1], + m_anchorPoints[m_selectedAnchor]); + m_pointCollection[m_selectedAnchor] = computeEdgeWrapper(m_anchorPoints[m_selectedAnchor], + m_anchorPoints[m_selectedAnchor + 1]); + reEvaluatePoints(); +} + +int KisToolSelectMagnetic::updateInitialAnchorBounds(QPoint pt) +{ + qreal zoomLevel = canvas()->viewConverter()->zoom(); + int sides = (int) std::ceil(10.0 / zoomLevel); + m_snapBound = QRectF(QPoint(0, 0), QSize(sides, sides)); + m_snapBound.moveCenter(pt); + return sides; +} + +void KisToolSelectMagnetic::reEvaluatePoints() +{ + m_points.clear(); + Q_FOREACH (const vQPointF vec, m_pointCollection) { + m_points.append(vec); + } + + updatePaintPath(); +} + +void KisToolSelectMagnetic::finishSelectionAction() +{ + KisCanvas2 *kisCanvas = dynamic_cast(canvas()); + KIS_ASSERT_RECOVER_RETURN(kisCanvas) + kisCanvas->updateCanvas(); + setMode(KisTool::HOVER_MODE); + m_complete = false; + m_finished = true; + + // just for testing out + // m_worker.saveTheImage(m_points); + + QRectF boundingViewRect = + pixelToView(KisAlgebra2D::accumulateBounds(m_points)); + + KisSelectionToolHelper helper(kisCanvas, kundo2_i18n("Magnetic Selection")); + + if (m_points.count() > 2 && + !helper.tryDeselectCurrentSelection(boundingViewRect, selectionAction())) + { + QApplication::setOverrideCursor(KisCursor::waitCursor()); + + const SelectionMode mode = + helper.tryOverrideSelectionMode(kisCanvas->viewManager()->selection(), + selectionMode(), + selectionAction()); + if (mode == PIXEL_SELECTION) { + KisPixelSelectionSP tmpSel = KisPixelSelectionSP(new KisPixelSelection()); + + KisPainter painter(tmpSel); + painter.setPaintColor(KoColor(Qt::black, tmpSel->colorSpace())); + painter.setAntiAliasPolygonFill(antiAliasSelection()); + painter.setFillStyle(KisPainter::FillStyleForegroundColor); + painter.setStrokeStyle(KisPainter::StrokeStyleNone); + + painter.paintPolygon(m_points); + + QPainterPath cache; + cache.addPolygon(m_points); + cache.closeSubpath(); + tmpSel->setOutlineCache(cache); + + helper.selectPixelSelection(tmpSel, selectionAction()); + } else { + KoPathShape *path = new KoPathShape(); + path->setShapeId(KoPathShapeId); + + QTransform resolutionMatrix; + resolutionMatrix.scale(1 / currentImage()->xRes(), 1 / currentImage()->yRes()); + path->moveTo(resolutionMatrix.map(m_points[0])); + for (int i = 1; i < m_points.count(); i++) + path->lineTo(resolutionMatrix.map(m_points[i])); + path->close(); + path->normalize(); + helper.addSelectionShape(path, selectionAction()); + } + QApplication::restoreOverrideCursor(); + } + + resetVariables(); +} // KisToolSelectMagnetic::finishSelectionAction + +void KisToolSelectMagnetic::resetVariables() +{ + m_points.clear(); + m_anchorPoints.clear(); + m_pointCollection.clear(); + m_paintPath = QPainterPath(); +} + +void KisToolSelectMagnetic::updatePaintPath() +{ + m_paintPath = QPainterPath(); + if (m_points.size() > 0) { + m_paintPath.moveTo(pixelToView(m_points[0])); + } + for (int i = 1; i < m_points.count(); i++) { + m_paintPath.lineTo(pixelToView(m_points[i])); + } + + updateFeedback(); + + if (m_continuedMode && mode() != PAINT_MODE) { + updateContinuedMode(); + } + + updateCanvasPixelRect(image()->bounds()); +} + +void KisToolSelectMagnetic::paint(QPainter& gc, const KoViewConverter &converter) +{ + Q_UNUSED(converter) + updatePaintPath(); + if ((mode() == KisTool::PAINT_MODE || m_continuedMode) && + !m_anchorPoints.isEmpty()) + { + QPainterPath outline = m_paintPath; + if (m_continuedMode && mode() != KisTool::PAINT_MODE) { + outline.lineTo(pixelToView(m_lastCursorPos)); + } + paintToolOutline(&gc, outline); + drawAnchors(gc); + } +} + +void KisToolSelectMagnetic::drawAnchors(QPainter &gc) +{ + int sides = updateInitialAnchorBounds(m_anchorPoints.first()); + Q_FOREACH (const QPoint pt, m_anchorPoints) { + KisHandlePainterHelper helper(&gc, handleRadius()); + QRect r(QPoint(0, 0), QSize(sides, sides)); + r.moveCenter(pt); + if (r.contains(m_lastCursorPos.toPoint())) { + helper.setHandleStyle(KisHandleStyle::highlightedPrimaryHandles()); + } else { + helper.setHandleStyle(KisHandleStyle::primarySelection()); + } + helper.drawHandleRect(pixelToView(pt), 4, QPoint(0, 0)); + } +} + +void KisToolSelectMagnetic::updateFeedback() +{ + if (m_points.count() > 1) { + qint32 lastPointIndex = m_points.count() - 1; + + QRectF updateRect = QRectF(m_points[lastPointIndex - 1], m_points[lastPointIndex]).normalized(); + updateRect = kisGrowRect(updateRect, FEEDBACK_LINE_WIDTH); + + updateCanvasPixelRect(updateRect); + } +} + +void KisToolSelectMagnetic::updateContinuedMode() +{ + if (!m_points.isEmpty()) { + qint32 lastPointIndex = m_points.count() - 1; + + QRectF updateRect = QRectF(m_points[lastPointIndex - 1], m_lastCursorPos).normalized(); + updateRect = kisGrowRect(updateRect, FEEDBACK_LINE_WIDTH); + + updateCanvasPixelRect(updateRect); + } +} + +void KisToolSelectMagnetic::activate(KoToolBase::ToolActivation activation, const QSet &shapes) +{ + m_worker = KisMagneticWorker(image()->projection()); + m_configGroup = KSharedConfig::openConfig()->group(toolId()); + connect(action("undo_polygon_selection"), SIGNAL(triggered()), SLOT(undoPoints()), Qt::UniqueConnection); + connect(&m_mouseHoverCompressor, SIGNAL(timeout()), this, SLOT(slotCalculateEdge())); + KisToolSelect::activate(activation, shapes); +} + +void KisToolSelectMagnetic::deactivate() +{ + KisCanvas2 *kisCanvas = dynamic_cast(canvas()); + KIS_ASSERT_RECOVER_RETURN(kisCanvas) + kisCanvas->updateCanvas(); + + m_continuedMode = false; + m_complete = true; + + disconnect(action("undo_polygon_selection"), nullptr, this, nullptr); + + KisTool::deactivate(); +} + +void KisToolSelectMagnetic::undoPoints() +{ + if (m_complete) return; + + m_anchorPoints.pop_back(); + m_pointCollection.pop_back(); + reEvaluatePoints(); +} + +void KisToolSelectMagnetic::requestStrokeEnd() +{ + if (m_finished || m_anchorPoints.count() < 2) return; + + finishSelectionAction(); + m_finished = false; +} + +void KisToolSelectMagnetic::requestStrokeCancellation() +{ + m_complete = false; + m_points.clear(); + m_anchorPoints.clear(); + m_paintPath = QPainterPath(); + updatePaintPath(); +} + +QWidget * KisToolSelectMagnetic::createOptionWidget() +{ + KisToolSelectBase::createOptionWidget(); + KisSelectionOptions *selectionWidget = selectionOptionWidget(); + QHBoxLayout *f1 = new QHBoxLayout(); + QLabel *filterRadiusLabel = new QLabel(i18n("Filter Radius: "), selectionWidget); + f1->addWidget(filterRadiusLabel); + + KisDoubleSliderSpinBox *filterRadiusInput = new KisDoubleSliderSpinBox(selectionWidget); + filterRadiusInput->setObjectName("radius"); + filterRadiusInput->setRange(2.5, 100.0, 2); + filterRadiusInput->setSingleStep(0.5); + filterRadiusInput->setToolTip("Radius of the filter for the detecting edges, might take some time to calculate"); + f1->addWidget(filterRadiusInput); + connect(filterRadiusInput, SIGNAL(valueChanged(qreal)), this, SLOT(slotSetFilterRadius(qreal))); + + QHBoxLayout *f2 = new QHBoxLayout(); + QLabel *thresholdLabel = new QLabel(i18n("Threshold: "), selectionWidget); + f2->addWidget(thresholdLabel); + + KisSliderSpinBox *thresholdInput = new KisSliderSpinBox(selectionWidget); + thresholdInput->setObjectName("threshold"); + thresholdInput->setRange(1, 255); + thresholdInput->setSingleStep(10); + thresholdInput->setToolTip("Threshold for determining the minimum intensity of the edges"); + f2->addWidget(thresholdInput); + connect(thresholdInput, SIGNAL(valueChanged(int)), this, SLOT(slotSetThreshold(int))); + + QHBoxLayout *f3 = new QHBoxLayout(); + QLabel *searchRadiusLabel = new QLabel(i18n("Search Radius: "), selectionWidget); + f3->addWidget(searchRadiusLabel); + + KisSliderSpinBox *searchRadiusInput = new KisSliderSpinBox(selectionWidget); + searchRadiusInput->setObjectName("frequency"); + searchRadiusInput->setRange(20, 200); + searchRadiusInput->setSingleStep(10); + searchRadiusInput->setToolTip("Extra area to be searched"); + searchRadiusInput->setSuffix(" px"); + f3->addWidget(searchRadiusInput); + connect(searchRadiusInput, SIGNAL(valueChanged(int)), this, SLOT(slotSetSearchRadius(int))); + + QHBoxLayout *f4 = new QHBoxLayout(); + QLabel *anchorGapLabel = new QLabel(i18n("Anchor Gap: "), selectionWidget); + f4->addWidget(anchorGapLabel); + + KisSliderSpinBox *anchorGapInput = new KisSliderSpinBox(selectionWidget); + anchorGapInput->setObjectName("anchorgap"); + anchorGapInput->setRange(20, 200); + anchorGapInput->setSingleStep(10); + anchorGapInput->setToolTip("Gap between 2 anchors in interative mode"); + anchorGapInput->setSuffix(" px"); + f4->addWidget(anchorGapInput); + + connect(anchorGapInput, SIGNAL(valueChanged(int)), this, SLOT(slotSetAnchorGap(int))); + + QVBoxLayout *l = dynamic_cast(selectionWidget->layout()); + + l->insertLayout(1, f1); + l->insertLayout(2, f2); + l->insertLayout(3, f3); + l->insertLayout(5, f4); + + filterRadiusInput->setValue(m_configGroup.readEntry("filterradius", 3.0)); + thresholdInput->setValue(m_configGroup.readEntry("threshold", 100)); + searchRadiusInput->setValue(m_configGroup.readEntry("searchradius", 30)); + anchorGapInput->setValue(m_configGroup.readEntry("anchorgap", 20)); + + return selectionWidget; +} // KisToolSelectMagnetic::createOptionWidget + +void KisToolSelectMagnetic::slotSetFilterRadius(qreal r) +{ + m_filterRadius = r; + m_configGroup.writeEntry("filterradius", r); +} + +void KisToolSelectMagnetic::slotSetThreshold(int t) +{ + m_threshold = t; + m_configGroup.writeEntry("threshold", t); +} + +void KisToolSelectMagnetic::slotSetSearchRadius(int r) +{ + m_searchRadius = r; + m_configGroup.writeEntry("searchradius", r); +} + +void KisToolSelectMagnetic::slotSetAnchorGap(int g) +{ + m_anchorGap = g; + m_configGroup.writeEntry("anchorgap", g); +} + +void KisToolSelectMagnetic::resetCursorStyle() +{ + if (selectionAction() == SELECTION_ADD) { + useCursor(KisCursor::load("tool_magnetic_selection_cursor_add.png", 6, 6)); + } else if (selectionAction() == SELECTION_SUBTRACT) { + useCursor(KisCursor::load("tool_magnetic_selection_cursor_sub.png", 6, 6)); + } else { + KisToolSelect::resetCursorStyle(); + } +} diff --git a/plugins/tools/selectiontools/KisToolSelectMagnetic.h b/plugins/tools/selectiontools/KisToolSelectMagnetic.h new file mode 100644 index 0000000000..92be32a30c --- /dev/null +++ b/plugins/tools/selectiontools/KisToolSelectMagnetic.h @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2019 Kuntal Majumder + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef KIS_TOOL_SELECT_MAGNETIC_H_ +#define KIS_TOOL_SELECT_MAGNETIC_H_ + +#include +#include "KisSelectionToolFactoryBase.h" +#include +#include +#include +#include "KisMagneticWorker.h" + +class QPainterPath; + +class KisToolSelectMagnetic : public KisToolSelect +{ + Q_OBJECT + +public: + KisToolSelectMagnetic(KoCanvasBase *canvas); + ~KisToolSelectMagnetic() override = default; + void beginPrimaryAction(KoPointerEvent *event) override; + void continuePrimaryAction(KoPointerEvent *event) override; + void endPrimaryAction(KoPointerEvent *event) override; + void paint(QPainter& gc, const KoViewConverter &converter) override; + + void beginPrimaryDoubleClickAction(KoPointerEvent *event) override; + + void keyPressEvent(QKeyEvent *event) override; + void keyReleaseEvent(QKeyEvent *event) override; + + void mouseMoveEvent(KoPointerEvent *event) override; + + void resetCursorStyle() override; + void requestStrokeEnd() override; + void requestStrokeCancellation() override; + QWidget * createOptionWidget() override; + +public Q_SLOTS: + void deactivate() override; + void activate(KoToolBase::ToolActivation activation, const QSet &shapes) override; + void undoPoints(); + void slotSetFilterRadius(qreal); + void slotSetThreshold(int); + void slotSetSearchRadius(int); + void slotSetAnchorGap(int); + void slotCalculateEdge(); + +protected: + using KisToolSelectBase::m_widgetHelper; + +private: + void finishSelectionAction(); + void updateFeedback(); + void updateContinuedMode(); + void updateCanvas(); + void updatePaintPath(); + void resetVariables(); + void drawAnchors(QPainter &gc); + void checkIfAnchorIsSelected(QPointF pt); + vQPointF computeEdgeWrapper(QPoint a, QPoint b); + void reEvaluatePoints(); + void calculateCheckPoints(vQPointF points); + void deleteSelectedAnchor(); + void updateSelectedAnchor(); + int updateInitialAnchorBounds(QPoint pt); + + QPainterPath m_paintPath; + QVector m_points; + QVector m_anchorPoints; + bool m_continuedMode; + QPointF m_lastCursorPos, m_cursorOnPress; + QPoint m_lastAnchor; + bool m_complete, m_selected, m_finished; + KisMagneticWorker m_worker; + int m_threshold, m_searchRadius, m_selectedAnchor, m_anchorGap; + qreal m_filterRadius; + QRectF m_snapBound; + KConfigGroup m_configGroup; + QVector m_pointCollection; + KisSignalCompressor m_mouseHoverCompressor; +}; + +class KisToolSelectMagneticFactory : public KisSelectionToolFactoryBase +{ +public: + KisToolSelectMagneticFactory() + : KisSelectionToolFactoryBase("KisToolSelectMagnetic") + { + setToolTip(i18n("Magnetic Selection Tool")); + setSection(TOOL_TYPE_SELECTION); + setIconName(koIconNameCStr("tool_magnetic_selection")); + setPriority(8); + setActivationShapeId(KRITA_TOOL_ACTIVATION_ID); + } + + ~KisToolSelectMagneticFactory() override { } + + KoToolBase * createTool(KoCanvasBase *canvas) override + { + return new KisToolSelectMagnetic(canvas); + } + + QList createActionsImpl() override + { + KisActionRegistry *actionRegistry = KisActionRegistry::instance(); + QList actions = KisSelectionToolFactoryBase::createActionsImpl(); + + actions << actionRegistry->makeQAction("undo_polygon_selection"); + + return actions; + } +}; + + +#endif // __selecttoolmagnetic_h__ diff --git a/plugins/tools/selectiontools/selection_tools.cc b/plugins/tools/selectiontools/selection_tools.cc index e0e44fbe60..fb0af54745 100644 --- a/plugins/tools/selectiontools/selection_tools.cc +++ b/plugins/tools/selectiontools/selection_tools.cc @@ -1,59 +1,61 @@ /* * selection_tools.cc -- Part of Krita * * Copyright (c) 2004 Boudewijn Rempt (boud@valdyas.org) * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "selection_tools.h" #include #include #include #include "KoToolRegistry.h" #include "kis_global.h" #include "kis_types.h" #include "kis_tool_select_outline.h" #include "kis_tool_select_polygonal.h" #include "kis_tool_select_rectangular.h" #include "kis_tool_select_contiguous.h" #include "kis_tool_select_elliptical.h" #include "kis_tool_select_path.h" #include "kis_tool_select_similar.h" +#include "KisToolSelectMagnetic.h" K_PLUGIN_FACTORY_WITH_JSON(SelectionToolsFactory, "kritaselectiontools.json", registerPlugin();) SelectionTools::SelectionTools(QObject *parent, const QVariantList &) : QObject(parent) { KoToolRegistry::instance()->add(new KisToolSelectOutlineFactory()); KoToolRegistry::instance()->add(new KisToolSelectPolygonalFactory()); KoToolRegistry::instance()->add(new KisToolSelectRectangularFactory()); KoToolRegistry::instance()->add(new KisToolSelectEllipticalFactory()); KoToolRegistry::instance()->add(new KisToolSelectContiguousFactory()); KoToolRegistry::instance()->add(new KisToolSelectPathFactory()); KoToolRegistry::instance()->add(new KisToolSelectSimilarFactory()); + KoToolRegistry::instance()->add(new KisToolSelectMagneticFactory()); } SelectionTools::~SelectionTools() { } #include "selection_tools.moc" diff --git a/plugins/tools/selectiontools/selectiontools.qrc b/plugins/tools/selectiontools/selectiontools.qrc index cd3646a389..37430ba7d6 100644 --- a/plugins/tools/selectiontools/selectiontools.qrc +++ b/plugins/tools/selectiontools/selectiontools.qrc @@ -1,24 +1,26 @@ - - - + + tool_contiguous_selection_cursor_add.png tool_contiguous_selection_cursor.png tool_contiguous_selection_cursor_sub.png tool_elliptical_selection_cursor_add.png tool_elliptical_selection_cursor.png tool_elliptical_selection_cursor_sub.png tool_eraser_selection_cursor.png tool_outline_selection_cursor_add.png tool_outline_selection_cursor.png tool_outline_selection_cursor_sub.png tool_polygonal_selection_cursor_add.png tool_polygonal_selection_cursor.png tool_polygonal_selection_cursor_sub.png tool_rectangular_selection_cursor_add.png tool_rectangular_selection_cursor.png tool_rectangular_selection_cursor_sub.png tool_similar_selection_cursor_add.png tool_similar_selection_cursor.png tool_similar_selection_cursor_sub.png + tool_magnetic_selection_cursor.png + tool_magnetic_selection_cursor_sub.png + tool_magnetic_selection_cursor_add.png diff --git a/plugins/tools/selectiontools/tests/CMakeLists.txt b/plugins/tools/selectiontools/tests/CMakeLists.txt new file mode 100644 index 0000000000..18ed773851 --- /dev/null +++ b/plugins/tools/selectiontools/tests/CMakeLists.txt @@ -0,0 +1,14 @@ +set( EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR} ) +include_directories( + ${CMAKE_CURRENT_SOURCE_DIR}/.. + ${CMAKE_CURRENT_BINARY_DIR}/.. + ${CMAKE_SOURCE_DIR}/sdk/tests +) + +macro_add_unittest_definitions() + +########### next target ############### + +ecm_add_test(KisMagneticWorkerTest.cc + NAME_PREFIX plugins-magneticselection- + LINK_LIBRARIES kritaselectiontools kritaimage Qt5::Test) diff --git a/plugins/tools/selectiontools/tests/KisMagneticWorkerTest.cc b/plugins/tools/selectiontools/tests/KisMagneticWorkerTest.cc new file mode 100644 index 0000000000..33c9006f66 --- /dev/null +++ b/plugins/tools/selectiontools/tests/KisMagneticWorkerTest.cc @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2019 Kuntal Majumder + * + * This library is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "KisMagneticWorkerTest.h" + +#include +#include +#include +#include +#include + +#include + +inline KisPaintDeviceSP loadTestImage(const QString &name, bool convertToAlpha) +{ + QImage image(TestUtil::fetchDataFileLazy(name)); + KisPaintDeviceSP dev = new KisPaintDevice(KoColorSpaceRegistry::instance()->rgb8()); + dev->convertFromQImage(image, 0); + + if (convertToAlpha) { + dev = KisPainter::convertToAlphaAsAlpha(dev); + } + + return dev; +} + +void KisMagneticWorkerTest::testWorker() +{ + KisPaintDeviceSP dev = loadTestImage("test_main.png", false); + const QRect rect = dev->exactBounds(); + KisPaintDeviceSP grayscaleDev = KisPainter::convertToAlphaAsGray(dev); + KisMagneticWorker worker(grayscaleDev); + + const QPoint startPos(80, 10); + const QPoint endPos(10, 100); + + auto points = worker.computeEdge(20, startPos, endPos, 3.0); + KIS_DUMP_DEVICE_2(grayscaleDev, rect, "draw", "dd"); + + /* + QVector result = { QPointF(50,65), + QPointF(49,64), + QPointF(48,63), + QPointF(47,62), + QPointF(46,61), + QPointF(45,60), + QPointF(44,59), + QPointF(44,58), + QPointF(44,57), + QPointF(44,56), + QPointF(44,55), + QPointF(44,54), + QPointF(44,53), + QPointF(44,52), + QPointF(44,51), + QPointF(44,50), + QPointF(44,49), + QPointF(44,48), + QPointF(44,47), + QPointF(44,46), + QPointF(44,45), + QPointF(44,44), + QPointF(44,43), + QPointF(44,42), + QPointF(43,41), + QPointF(43,40), + QPointF(43,39), + QPointF(44,38), + QPointF(44,37), + QPointF(44,36), + QPointF(44,35), + QPointF(44,34), + QPointF(44,33), + QPointF(44,32), + QPointF(44,31), + QPointF(44,30), + QPointF(44,29), + QPointF(44,28), + QPointF(44,27), + QPointF(44,26), + QPointF(44,25), + QPointF(44,24), + QPointF(44,23), + QPointF(44,22), + QPointF(44,21), + QPointF(44,20), + QPointF(44,19), + QPointF(44,18), + QPointF(43,17), + QPointF(44,16), + QPointF(44,15), + QPointF(44,14), + QPointF(44,13), + QPointF(43,12), + QPointF(42,11), + QPointF(41,11), + QPointF(40,10)}; + + QCOMPARE(result, points); + */ + + QImage img = dev->convertToQImage(0, rect); + img = img.convertToFormat(QImage::Format_ARGB32); + QPainter gc(&img); + + QPainterPath path; + + for (int i = 0; i < points.size(); i++) { + if (i == 0) { + path.moveTo(points[i]); + } else { + path.lineTo(points[i]); + } + } + + gc.setPen(Qt::blue); + gc.drawPath(path); + + gc.setPen(Qt::green); + gc.drawEllipse(startPos, 3, 3); + gc.setPen(Qt::red); + gc.drawEllipse(endPos, 2, 2); + + img.save("result.png"); + +} + +QTEST_MAIN(KisMagneticWorkerTest) diff --git a/plugins/tools/selectiontools/tests/KisMagneticWorkerTest.h b/plugins/tools/selectiontools/tests/KisMagneticWorkerTest.h new file mode 100644 index 0000000000..934b6f8a4f --- /dev/null +++ b/plugins/tools/selectiontools/tests/KisMagneticWorkerTest.h @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2019 Kuntal Majumder + * + * This library is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef KISMAGNETICWORKERTEST_H +#define KISMAGNETICWORKERTEST_H + +#include + +class KisMagneticWorkerTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testWorker(); + +}; + +#endif diff --git a/plugins/tools/selectiontools/tests/data/test_main.png b/plugins/tools/selectiontools/tests/data/test_main.png new file mode 100644 index 0000000000..cb0e8e5429 Binary files /dev/null and b/plugins/tools/selectiontools/tests/data/test_main.png differ diff --git a/plugins/tools/selectiontools/tool_magnetic_selection_cursor.png b/plugins/tools/selectiontools/tool_magnetic_selection_cursor.png new file mode 100644 index 0000000000..302df70a6d Binary files /dev/null and b/plugins/tools/selectiontools/tool_magnetic_selection_cursor.png differ diff --git a/plugins/tools/selectiontools/tool_magnetic_selection_cursor_add.png b/plugins/tools/selectiontools/tool_magnetic_selection_cursor_add.png new file mode 100644 index 0000000000..951bea6c04 Binary files /dev/null and b/plugins/tools/selectiontools/tool_magnetic_selection_cursor_add.png differ diff --git a/plugins/tools/selectiontools/tool_magnetic_selection_cursor_sub.png b/plugins/tools/selectiontools/tool_magnetic_selection_cursor_sub.png new file mode 100644 index 0000000000..a196e597c0 Binary files /dev/null and b/plugins/tools/selectiontools/tool_magnetic_selection_cursor_sub.png differ diff --git a/sdk/.uncrustify.cfg b/sdk/.uncrustify.cfg new file mode 100644 index 0000000000..568bfda207 --- /dev/null +++ b/sdk/.uncrustify.cfg @@ -0,0 +1,396 @@ +# -------------------------------------------------------------------------------------------------# +# # +# _ _ _ _ __ ___ _____ _ _ __ _ # +# | | | |_ _ __ _ _ _ _ __| |_(_)/ _|_ _ / __| / / __|| |_ _| |_ __ ___ _ _ / _(_)__ _ # +# | |_| | ' \/ _| '_| || (_-< _| | _| || | | (__ / / (_|_ _|_ _| / _/ _ \ ' \| _| / _` | # +# \___/|_||_\__|_| \_,_/__/\__|_|_| \_, | \___/_/ \___||_| |_| \__\___/_||_|_| |_\__, | # +# |__/ |___/ # +# # +# -------------------------------------------------------------------------------------------------# +# # +# Style: rindeal # +# # +# -------------------------------------------------------------------------------------------------# +# Boilerplate: https://github.com/bengardner/uncrustify/blob/master/etc/defaults.cfg # +# -------------------------------------------------------------------------------------------------# + + +## General +## ------------------------------------------------------------------------------------------------- + +# The type of line endings +newlines = lf # auto/lf/crlf/cr + +code_width = 120 + +# empty_lines_max = nl_max - 1 +nl_max = 3 + + +## UNICODE +## ------------------------------------------------------------------------------------------------- +## Ideally ASCII, UTF-8 otherwise + +# If the file contains bytes with values between 128 and 255, but is not UTF-8, then output as UTF-8 +utf8_byte = false + +# Force the output encoding to UTF-8 +utf8_force = true + + +## Tabs +## ------------------------------------------------------------------------------------------------- +## Always use 4 spaces + +input_tab_size = 4 +output_tab_size = 4 + +indent_with_tabs = 0 + +# Comments that are not a brace level are indented with tabs on a tabstop. +# Requires indent_with_tabs = 2. If false, will use spaces. +indent_cmt_with_tabs = false + +# Whether to use tabs for aligning +align_with_tabs = false + +# Whether to keep non-indenting tabs +align_keep_tabs = false + +# Whether to bump out to the next tab when aligning +align_on_tabstop = false + + +## Indenting +## ------------------------------------------------------------------------------------------------- + +# True: indent_func_call_param will be used (default) +# False: indent_func_call_param will NOT be used. +use_indent_func_call_param = true # false/true + + +# The continuation indent for func_*_param if they are true. +# If non-zero, this overrides the indent. +indent_param = 1 # unsigned number + +# The number of columns to indent per level. +# Usually 2, 3, 4, or 8. +indent_columns = 4 + +# The continuation indent. If non-zero, this overrides the indent of '(' and '=' continuation indents. +# For FreeBSD, this is set to 4. Negative value is absolute and not increased for each ( level +indent_continue = 0 + + +## Spacing +## ------------------------------------------------------------------------------------------------- + +# Whether to balance spaces inside nested parens +sp_balance_nested_parens = false + + +## Parentheses +## ------------------------------------------------------------------------------------------------- + +# Controls the indent of a close paren after a newline. +# 0: Indent to body level +# 1: Align under the open paren +# 2: Indent to the brace level +indent_paren_close = 0 + + +## Preprocessor +## ------------------------------------------------------------------------------------------------- + +# Control indent of preprocessors inside #if blocks at brace level 0 +pp_indent = remove # ignore/add/remove/force + +# indent by 1 space +pp_space = add +pp_space_count = 1 + +# indent pp at code level +pp_indent_at_level = true +pp_define_at_level = true + +# Control whether to indent the code between #if, #else and #endif when not at file-level +pp_if_indent_code = false + +# # Align macro functions and variables together +align_pp_define_together = false + +# The minimum space between label and value of a preprocessor define +align_pp_define_gap = 1 + +# The span for aligning on '#define' bodies (0=don't align) +align_pp_define_span = 2 + +# Add or remove space around preprocessor '##' concatenation operator. Default=Add +sp_pp_concat = add # ignore/add/remove/force + +# Add or remove space after preprocessor '#' stringify operator. Also affects the '#@' charizing operator. +sp_pp_stringify = ignore # ignore/add/remove/force + +# Add or remove space before preprocessor '#' stringify operator as in '#define x(y) L#y'. +sp_before_pp_stringify = ignore # ignore/add/remove/force + + +# Template +# -------------------------------------------------------------------------------------------------- + +# Add or remove space in 'template <' vs 'template<'. +# If set to ignore, sp_before_angle is used. +sp_template_angle = add # ignore/add/remove/force + +# Add or remove space before '<>' +sp_before_angle = remove # ignore/add/remove/force + +# Add or remove space inside '<' and '>' +sp_inside_angle = remove # ignore/add/remove/force + +# Add or remove space after '<>' +sp_after_angle = add # ignore/add/remove/force + +# Add or remove space between '<>' and '(' as found in 'new List();' +sp_angle_paren = remove # ignore/add/remove/force + +# Add or remove space between '<>' and a word as in 'List m;' +sp_angle_word = add # ignore/add/remove/force + +# Add or remove space between '>' and '>' in '>>' (template stuff C++/C# only). Default=Add +sp_angle_shift = add # ignore/add/remove/force + + + + + +indent_align_string = false + +# Whether braces are indented to the body level +indent_braces = false +# Disabled indenting function braces if indent_braces is true +indent_braces_no_func = false +# Disabled indenting class braces if indent_braces is true +indent_braces_no_class = false +# Disabled indenting struct braces if indent_braces is true +indent_braces_no_struct = false +# Indent based on the size of the brace parent, i.e. 'if' => 3 spaces, 'for' => 4 spaces, etc. +indent_brace_parent = false + +indent_namespace = false +indent_extern = false +indent_class = true +indent_class_colon = false +indent_else_if = false +indent_var_def_cont = true + +indent_func_call_param = false +indent_func_def_param = true +indent_func_proto_param = true +indent_func_class_param = false +indent_func_ctor_var_param = false +indent_func_param_double = true + +indent_template_param = false +indent_relative_single_line_comments = false +indent_col1_comment = true +indent_access_spec_body = false +indent_paren_nl = false +indent_comma_paren = false +indent_bool_paren = false +indent_first_bool_expr = false +indent_square_nl = false +indent_preserve_sql = false +indent_align_assign = true + +#align_number_left = true +align_func_params = true +align_same_func_call_params = false +align_var_def_colon = false +align_var_def_attribute = true +align_var_def_inline = true +align_right_cmt_mix = false +align_on_operator = false +align_mix_var_proto = false +align_single_line_func = false +align_single_line_brace = false +align_nl_cont = false +align_left_shift = true +align_oc_decl_colon = false + +nl_collapse_empty_body = true +nl_assign_leave_one_liners = true +nl_class_leave_one_liners = true +nl_enum_leave_one_liners = true +nl_getset_leave_one_liners = true +nl_func_leave_one_liners = true +nl_if_leave_one_liners = true +nl_multi_line_cond = true +nl_multi_line_define = true +nl_before_case = false +nl_after_case = false +nl_after_return = true +nl_after_semicolon = true +nl_after_brace_open = false +nl_after_brace_open_cmt = false +nl_after_vbrace_open = false +nl_after_vbrace_open_empty = false +nl_after_brace_close = false +nl_after_vbrace_close = false +nl_define_macro = false +nl_squeeze_ifdef = false +nl_ds_struct_enum_cmt = false +nl_ds_struct_enum_close_brace = false +nl_create_if_one_liner = false +nl_create_for_one_liner = false +nl_create_while_one_liner = false +ls_for_split_full = true +ls_func_split_full = false +nl_after_multiline_comment = true +eat_blanks_after_open_brace = true +eat_blanks_before_close_brace = true +mod_full_brace_if_chain = true +mod_pawn_semicolon = false +mod_full_paren_if_bool = false +mod_remove_extra_semicolon = false +mod_sort_import = false +mod_sort_using = false +mod_sort_include = false +mod_move_case_break = false +mod_remove_empty_return = true +cmt_indent_multi = true +cmt_c_group = false +cmt_c_nl_start = false +cmt_c_nl_end = false +cmt_cpp_group = false +cmt_cpp_nl_start = false +cmt_cpp_nl_end = false +cmt_cpp_to_c = false +cmt_star_cont = true +cmt_multi_check_last = true +cmt_insert_before_preproc = false +indent_sing_line_comments = 0 +indent_switch_case = 4 +indent_case_shift = 0 + +align_var_def_star_style = 0 +align_var_def_amp_style = 1 +align_assign_span = 1 +align_assign_thresh = 8 +align_enum_equ_span = 3 +align_var_struct_span = 3 +align_var_struct_gap = 1 +align_struct_init_span = 2 +align_right_cmt_span = 2 +align_right_cmt_gap = 1 +align_right_cmt_at_col = 2 + +nl_end_of_file_min = 1 +nl_func_var_def_blk = 0 +nl_after_func_body = 2 +nl_after_func_body_one_liner = 1 +nl_before_block_comment = 2 +nl_after_struct = 1 +mod_full_brace_nl = 1 +mod_add_long_function_closebrace_comment = 32 +mod_add_long_ifdef_endif_comment = 10 +mod_add_long_ifdef_else_comment = 10 +sp_arith = force +sp_assign = force +sp_assign_default = add +sp_enum_assign = force +sp_bool = force +sp_compare = force +sp_before_ptr_star = add +sp_before_unnamed_ptr_star = add +sp_between_ptr_star = remove +sp_after_ptr_star = remove +sp_after_ptr_star_func = force +sp_before_ptr_star_func = force +sp_after_type = force +sp_before_sparen = force +sp_inside_sparen = remove +sp_after_sparen = add +sp_sparen_brace = add +sp_special_semi = remove +sp_before_semi = remove +sp_before_semi_for_empty = remove +sp_after_semi = add +sp_after_semi_for_empty = remove +sp_after_comma = force +sp_before_comma = remove +sp_before_case_colon = remove +sp_after_operator = add +sp_after_operator_sym = add +sp_after_cast = add +sp_inside_paren_cast = remove +sp_sizeof_paren = remove +sp_inside_braces_enum = add +sp_inside_braces_struct = add +sp_inside_braces = add +sp_inside_braces_empty = add +sp_func_proto_paren = remove +sp_func_def_paren = remove +sp_inside_fparens = remove +sp_inside_fparen = remove +sp_fparen_brace = remove +sp_func_call_paren = remove +sp_func_call_paren_empty = remove +sp_func_call_user_paren = remove +sp_return_paren = add +sp_attribute_paren = remove +sp_defined_paren = remove +sp_macro = add +sp_macro_func = add +sp_else_brace = add +sp_brace_else = add +sp_brace_typedef = add +sp_not = remove +sp_inv = remove +sp_addr = remove +sp_member = remove +sp_deref = remove +sp_sign = remove +sp_incdec = remove +sp_before_nl_cont = add +sp_cond_colon = force +sp_cond_question = force +sp_cmt_cpp_start = add +nl_start_of_file = remove +nl_end_of_file = force +nl_assign_brace = remove +nl_assign_square = remove +nl_enum_brace = remove +nl_struct_brace = remove +nl_union_brace = remove +nl_if_brace = remove +nl_brace_else = remove +nl_elseif_brace = remove +nl_else_brace = remove +nl_else_if = remove +nl_for_brace = remove +nl_while_brace = remove +nl_do_brace = remove +nl_brace_while = remove +nl_switch_brace = remove +nl_case_colon_brace = remove +nl_func_type_name = remove +nl_func_proto_type_name = remove +nl_func_paren = remove +nl_func_def_paren = remove +nl_func_decl_empty = remove +nl_func_def_empty = remove +nl_fdef_brace = add +nl_return_expr = remove +pos_arith = lead +pos_assign = trail +pos_bool = trail +pos_conditional = trail +pos_comma = trail +pos_class_comma = lead +pos_class_colon = lead +mod_full_brace_do = remove +mod_full_brace_for = remove +mod_full_brace_function = force +mod_full_brace_while = remove +mod_paren_on_return = ignore