diff --git a/benchmarks/kis_stroke_benchmark.cpp b/benchmarks/kis_stroke_benchmark.cpp index 93296b2dbe..1a2a4cc951 100644 --- a/benchmarks/kis_stroke_benchmark.cpp +++ b/benchmarks/kis_stroke_benchmark.cpp @@ -1,634 +1,635 @@ /* * Copyright (c) 2010 Lukáš Tvrdý lukast.dev@gmail.com * * 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 #if defined(_WIN32) || defined(_WIN64) #define srand48 srand inline double drand48() { return double(rand()) / RAND_MAX; } #endif +#include #include #include "kis_stroke_benchmark.h" #include "kis_benchmark_values.h" #include "kis_paint_device.h" #include #include #include #include #include #include #include #include #define GMP_IMAGE_WIDTH 3274 #define GMP_IMAGE_HEIGHT 2067 #include #include //#define SAVE_OUTPUT static const int LINES = 20; static const int RECTANGLES = 20; const QString OUTPUT_FORMAT = ".png"; void KisStrokeBenchmark::initTestCase() { m_dataPath = QString(FILES_DATA_DIR) + QDir::separator(); m_outputPath = QString(FILES_OUTPUT_DIR) + QDir::separator(); m_colorSpace = KoColorSpaceRegistry::instance()->rgb8(); m_color = KoColor(m_colorSpace); int width = TEST_IMAGE_WIDTH; int height = TEST_IMAGE_HEIGHT; m_image = new KisImage(0, width, height, m_colorSpace, "stroke sample image"); m_layer = new KisPaintLayer(m_image, "temporary for stroke sample", OPACITY_OPAQUE_U8, m_colorSpace); m_painter = new KisPainter(m_layer->paintDevice()); m_painter->setPaintColor(KoColor(Qt::black, m_colorSpace)); // for bezier curve test initCurvePoints(width, height); // for the lines test initLines(width,height); // for the rectangles test initRectangles(width, height); } void KisStrokeBenchmark::init() { KoColor white(m_colorSpace); white.fromQColor(Qt::white); m_layer->paintDevice()->fill(0,0, m_image->width(), m_image->height(),white.data()); } void KisStrokeBenchmark::initCurvePoints(int width, int height) { QPointF p1(0 , 7.0 / 12.0 * height); QPointF p2(1.0 / 2.0 * width , 7.0 / 12.0 * height); QPointF p3(width - 4.0, height - 4.0); m_c1 = QPointF(1.0 / 4.0 * width, height - 2.0); m_c2 = QPointF(3.0 / 4.0 * width, 0); m_pi1 = KisPaintInformation(p1, 0.0); m_pi2 = KisPaintInformation(p2, 0.95); m_pi3 = KisPaintInformation(p3, 0.0); } void KisStrokeBenchmark::initLines(int width, int height) { srand(12345678); for (int i = 0; i < LINES; i++){ qreal sx = rand() / qreal(RAND_MAX - 1); qreal sy = rand() / qreal(RAND_MAX - 1); m_startPoints.append(QPointF(sx * width,sy * height)); qreal ex = rand() / qreal(RAND_MAX - 1); qreal ey = rand() / qreal(RAND_MAX - 1); m_endPoints.append(QPointF(ex * width,ey * height)); } } void KisStrokeBenchmark::initRectangles(int width, int height) { qreal margin = 0.5; qreal skip = 0.01; qreal marginWidth = margin*width; qreal marginHeight = margin*height; qreal skipWidth = skip*width >= 1 ? skip*width : 1; qreal skipHeight = skip*width >= 1 ? skip*width : 1; // "concentric" rectangles for (int i = 0; i < RECTANGLES; i++){ QPoint corner1 = QPoint(marginWidth + i*skipWidth, marginHeight + i*skipHeight); QPoint corner2 = QPoint(width - marginWidth - i*skipWidth, height - marginHeight - i*skipHeight); if(corner1.x() < corner2.x() && corner1.y() < corner2.y()) { // if the rectangle is not empty m_rectangleLeftLowerCorners.append(corner1); m_rectangleRightUpperCorners.append(corner2); } } } void KisStrokeBenchmark::cleanupTestCase() { delete m_painter; } void KisStrokeBenchmark::deformBrush() { QString presetFileName = "deform-default.kpp"; benchmarkStroke(presetFileName); } void KisStrokeBenchmark::deformBrushRL() { QString presetFileName = "deform-default.kpp"; benchmarkRandomLines(presetFileName); } void KisStrokeBenchmark::pixelbrush300px() { QString presetFileName = "autobrush_300px.kpp"; benchmarkStroke(presetFileName); } void KisStrokeBenchmark::pixelbrush300pxRL() { QString presetFileName = "autobrush_300px.kpp"; benchmarkRandomLines(presetFileName); } void KisStrokeBenchmark::sprayPixels() { QString presetFileName = "spray_wu_pixels1.kpp"; benchmarkStroke(presetFileName); } void KisStrokeBenchmark::sprayPixelsRL() { QString presetFileName = "spray_wu_pixels1.kpp"; benchmarkRandomLines(presetFileName); } void KisStrokeBenchmark::sprayTexture() { QString presetFileName = "spray_21_textures1.kpp"; benchmarkStroke(presetFileName); } void KisStrokeBenchmark::sprayTextureRL() { QString presetFileName = "spray_21_textures1.kpp"; benchmarkRandomLines(presetFileName); } void KisStrokeBenchmark::spray30px21particles() { QString presetFileName = "spray_30px21rasterParticles.kpp"; benchmarkStroke(presetFileName); } void KisStrokeBenchmark::spray30px21particlesRL() { QString presetFileName = "spray_30px21rasterParticles.kpp"; benchmarkRandomLines(presetFileName); } void KisStrokeBenchmark::sprayPencil() { QString presetFileName = "spray_scaled2rasterParticles.kpp"; benchmarkStroke(presetFileName); } void KisStrokeBenchmark::sprayPencilRL() { QString presetFileName = "spray_scaled2rasterParticles.kpp"; benchmarkRandomLines(presetFileName); } void KisStrokeBenchmark::softbrushDefault30() { QString presetFileName = "softbrush_30px.kpp"; benchmarkStroke(presetFileName); } void KisStrokeBenchmark::softbrushCircle30() { QString presetFileName = "softbrush_30px.kpp"; benchmarkCircle(presetFileName); } void KisStrokeBenchmark::softbrushDefault30RL() { QString presetFileName = "softbrush_30px.kpp"; benchmarkRandomLines(presetFileName);} void KisStrokeBenchmark::softbrushFullFeatures30() { QString presetFileName = "softbrush_30px_full.kpp"; benchmarkStroke(presetFileName); } void KisStrokeBenchmark::softbrushFullFeatures30RL() { QString presetFileName = "softbrush_30px_full.kpp"; benchmarkRandomLines(presetFileName); } void KisStrokeBenchmark::hairy30pxDefault() { QString presetFileName = "hairybrush_thesis30px1.kpp"; benchmarkStroke(presetFileName); } void KisStrokeBenchmark::hairy30pxDefaultRL() { QString presetFileName = "hairybrush_thesis30px1.kpp"; benchmarkRandomLines(presetFileName); } void KisStrokeBenchmark::hairy30pxAntiAlias() { QString presetFileName = "hairybrush_thesis30px_antialiasing1.kpp"; benchmarkStroke(presetFileName); } void KisStrokeBenchmark::hairy30pxAntiAliasRL() { QString presetFileName = "hairybrush_thesis30px_antialiasing1.kpp"; benchmarkRandomLines(presetFileName); } void KisStrokeBenchmark::hairy30px30density() { QString presetFileName = "hairybrush_thesis30px_density301.kpp"; benchmarkStroke(presetFileName); } void KisStrokeBenchmark::hairy30px30densityRL() { QString presetFileName = "hairybrush_thesis30px_density301.kpp"; benchmarkRandomLines(presetFileName); } void KisStrokeBenchmark::hairy30InkDepletion() { QString presetFileName = "hairy30inkDepletion1.kpp"; benchmarkStroke(presetFileName); } void KisStrokeBenchmark::hairy30InkDepletionRL() { QString presetFileName = "hairy30inkDepletion1.kpp"; benchmarkRandomLines(presetFileName); } void KisStrokeBenchmark::softbrushOpacity() { QString presetFileName = "softbrush_opacity1.kpp"; benchmarkLine(presetFileName); } void KisStrokeBenchmark::softbrushSoftness() { QString presetFileName = "softbrush_softness1.kpp"; benchmarkLine(presetFileName); } void KisStrokeBenchmark::dynabrush() { QString presetFileName = "dyna301.kpp"; benchmarkLine(presetFileName); } void KisStrokeBenchmark::dynabrushRL() { QString presetFileName = "dyna301.kpp"; benchmarkRandomLines(presetFileName); } void KisStrokeBenchmark::experimental() { QString presetFileName = "experimental.kpp"; benchmarkStroke(presetFileName); } void KisStrokeBenchmark::experimentalCircle() { QString presetFileName = "experimental.kpp"; benchmarkCircle(presetFileName); } void KisStrokeBenchmark::colorsmudge() { QString presetFileName = "colorsmudge.kpp"; benchmarkStroke(presetFileName); } void KisStrokeBenchmark::colorsmudgeRL() { QString presetFileName = "colorsmudge.kpp"; benchmarkStroke(presetFileName); } void KisStrokeBenchmark::roundMarker() { // Quick Brush engine ( b) Basic - 1 brush, size 40px) QString presetFileName = "roundmarker40px.kpp"; benchmarkStroke(presetFileName); } void KisStrokeBenchmark::roundMarkerRandomLines() { // Quick Brush engine ( b) Basic - 1 brush, size 40px) QString presetFileName = "roundmarker40px.kpp"; benchmarkRandomLines(presetFileName); } void KisStrokeBenchmark::roundMarkerRectangle() { // Quick Brush engine ( b) Basic - 1 brush, size 40px) QString presetFileName = "roundmarker40px.kpp"; benchmarkStroke(presetFileName); } void KisStrokeBenchmark::roundMarkerHalfPixel() { // Quick Brush engine ( b) Basic - 1 brush, size 0.5px) QString presetFileName = "roundmarker05px.kpp"; benchmarkStroke(presetFileName); } void KisStrokeBenchmark::roundMarkerRandomLinesHalfPixel() { // Quick Brush engine ( b) Basic - 1 brush, size 0.5px) QString presetFileName = "roundmarker05px.kpp"; benchmarkRandomLines(presetFileName); } void KisStrokeBenchmark::roundMarkerRectangleHalfPixel() { // Quick Brush engine ( b) Basic - 1 brush, size 0.5px) QString presetFileName = "roundmarker05px.kpp"; benchmarkStroke(presetFileName); } /* void KisStrokeBenchmark::predefinedBrush() { QString presetFileName = "deevad-slow-brush1.kpp"; benchmarkLine(presetFileName); } void KisStrokeBenchmark::predefinedBrushRL() { QString presetFileName = "deevad-slow-brush1.kpp"; KisPaintOpPresetSP preset = new KisPaintOpPreset(m_dataPath + presetFileName); preset->load(); preset->settings()->setNode(m_layer); m_painter->setPaintOpPreset(preset, m_image); sleep(3); QBENCHMARK{ for (int i = 0; i < LINES; i++){ KisPaintInformation pi1(m_startPoints[i], 0.0); KisPaintInformation pi2(m_endPoints[i], 1.0); m_painter->paintLine(pi1, pi2); } } //m_layer->paintDevice()->convertToQImage(0).save(m_outputPath + presetFileName + "_randomLines" + OUTPUT_FORMAT); } */ inline void KisStrokeBenchmark::benchmarkLine(QString presetFileName) { KisPaintOpPresetSP preset = new KisPaintOpPreset(m_dataPath + presetFileName); preset->load(); m_painter->setPaintOpPreset(preset, m_layer, m_image); QPointF startPoint(0.10 * TEST_IMAGE_WIDTH, 0.5 * TEST_IMAGE_HEIGHT); QPointF endPoint(0.90 * TEST_IMAGE_WIDTH, 0.5 * TEST_IMAGE_HEIGHT); KisDistanceInformation currentDistance; KisPaintInformation pi1(startPoint, 0.0); KisPaintInformation pi2(endPoint, 1.0); QBENCHMARK{ m_painter->paintLine(pi1, pi2, ¤tDistance); } #ifdef SAVE_OUTPUT m_layer->paintDevice()->convertToQImage(0).save(m_outputPath + presetFileName + "_line" + OUTPUT_FORMAT); #endif } void KisStrokeBenchmark::benchmarkCircle(QString presetFileName) { dbgKrita << "(circle)preset : " << presetFileName; KisPaintOpPresetSP preset = new KisPaintOpPreset(m_dataPath + presetFileName); if (!preset->load()){ dbgKrita << "Preset was not loaded"; return; } m_painter->setPaintOpPreset(preset, m_layer, m_image); QBENCHMARK{ qreal radius = 300; qreal randomOffset = 300 * 0.4; int rounds = 20; int steps = 20; qreal step = 1.0 / steps; QPointF center(m_image->width() * 0.5, m_image->height() * 0.5); QPointF first(center.x()+radius,center.y()); srand48(0); for (int k = 0; k < rounds; k++){ KisDistanceInformation currentDistance; m_painter->paintLine(center, first, ¤tDistance); QPointF prev = first; for (int i = 1; i < steps; i++) { qreal cx = cos(i * step * 2 * M_PI); qreal cy = sin(i * step * 2 * M_PI); cx *= (radius + drand48() * randomOffset); cy *= (radius + drand48() * randomOffset); cx += center.x(); cy += center.y(); m_painter->paintLine(prev, QPointF(cx,cy), ¤tDistance); prev = QPointF(cx,cy); } m_painter->paintLine(prev, first, ¤tDistance); } } #ifdef SAVE_OUTPUT m_layer->paintDevice()->convertToQImage(0).save(m_outputPath + presetFileName + "_circle" + OUTPUT_FORMAT); #endif } void KisStrokeBenchmark::benchmarkRandomLines(QString presetFileName) { KisPaintOpPresetSP preset = new KisPaintOpPreset(m_dataPath + presetFileName); bool loadedOk = preset->load(); if (!loadedOk){ dbgKrita << "The preset was not loaded correctly. Done."; return; }else{ dbgKrita << "preset : " << presetFileName; } m_painter->setPaintOpPreset(preset, m_layer, m_image); QBENCHMARK{ KisDistanceInformation currentDistance; for (int i = 0; i < LINES; i++){ KisPaintInformation pi1(m_startPoints[i], 0.0); KisPaintInformation pi2(m_endPoints[i], 1.0); m_painter->paintLine(pi1, pi2, ¤tDistance); } } #ifdef SAVE_OUTPUT m_layer->paintDevice()->convertToQImage(0).save(m_outputPath + presetFileName + "_randomLines" + OUTPUT_FORMAT); #endif } void KisStrokeBenchmark::benchmarkRectangle(QString presetFileName) { KisPaintOpPresetSP preset = new KisPaintOpPreset(m_dataPath + presetFileName); bool loadedOk = preset->load(); if (!loadedOk){ dbgKrita << "The preset was not loaded correctly. Done."; return; }else{ dbgKrita << "preset : " << presetFileName; } m_painter->setPaintOpPreset(preset, m_layer, m_image); int rectangleNumber = m_rectangleLeftLowerCorners.size(); // see initRectangles QBENCHMARK{ KisDistanceInformation currentDistance; for (int i = 0; i < rectangleNumber; i++){ QPainterPath path; QRect rect = QRect(m_rectangleLeftLowerCorners[i], m_rectangleRightUpperCorners[i]); path.addRect(rect); m_painter->paintPainterPath(path); } } #ifdef SAVE_OUTPUT m_layer->paintDevice()->convertToQImage(0).save(m_outputPath + presetFileName + "_rectangle" + OUTPUT_FORMAT); #endif } void KisStrokeBenchmark::benchmarkStroke(QString presetFileName) { KisPaintOpPresetSP preset = new KisPaintOpPreset(m_dataPath + presetFileName); bool loadedOk = preset->load(); if (!loadedOk){ dbgKrita << "The preset was not loaded correctly. Done."; return; } else { dbgKrita << "preset : " << presetFileName; } m_painter->setPaintOpPreset(preset, m_layer, m_image); QBENCHMARK{ KisDistanceInformation currentDistance; m_painter->paintBezierCurve(m_pi1, m_c1, m_c1, m_pi2, ¤tDistance); m_painter->paintBezierCurve(m_pi2, m_c2, m_c2, m_pi3, ¤tDistance); } #ifdef SAVE_OUTPUT dbgKrita << "Saving output " << m_outputPath + presetFileName + ".png"; m_layer->paintDevice()->convertToQImage(0).save(m_outputPath + presetFileName + OUTPUT_FORMAT); #endif } static const int COUNT = 1000000; void KisStrokeBenchmark::benchmarkRand48() { QBENCHMARK { for (int i = 0 ; i < COUNT; i++){ double result = drand48(); Q_UNUSED(result); } } } void KisStrokeBenchmark::benchmarkRand() { float j; QBENCHMARK{ for (int i = 0 ; i < COUNT; i++){ j = rand() / (float)RAND_MAX; } } Q_UNUSED(j); } void KisStrokeBenchmark::becnhmarkPresetCloning() { QString presetFileName = "spray_21_textures1.kpp"; KisPaintOpPresetSP preset = new KisPaintOpPreset(m_dataPath + presetFileName); bool loadedOk = preset->load(); KIS_ASSERT_RECOVER_RETURN(loadedOk); KIS_ASSERT_RECOVER_RETURN(preset->settings()); QBENCHMARK { KisPaintOpPresetSP other = preset->clone(); other->settings()->setPaintOpOpacity(0.3); } } QTEST_MAIN(KisStrokeBenchmark) diff --git a/libs/basicflakes/tools/KoCreatePathTool_p.h b/libs/basicflakes/tools/KoCreatePathTool_p.h index 386460403f..48f8a0c937 100644 --- a/libs/basicflakes/tools/KoCreatePathTool_p.h +++ b/libs/basicflakes/tools/KoCreatePathTool_p.h @@ -1,445 +1,447 @@ /* This file is part of the KDE project * * Copyright (C) 2006 Thorsten Zachmann * Copyright (C) 2008-2010 Jan Hambrecht * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KOCREATEPATHTOOL_P_H #define KOCREATEPATHTOOL_P_H +#include + #include "KoCreatePathTool.h" #include "KoPathPoint.h" #include "KoPathPointData.h" #include "KoPathPointMergeCommand.h" #include "KoParameterShape.h" #include "KoShapeManager.h" #include "KoSnapStrategy.h" #include "KoToolBase_p.h" #include #include "kis_config.h" #include "math.h" class KoStrokeConfigWidget; class KoConverter; /// Small helper to keep track of a path point and its parent path shape struct PathConnectionPoint { PathConnectionPoint() : path(0), point(0) { } // reset state to invalid void reset() { path = 0; point = 0; } PathConnectionPoint& operator =(KoPathPoint * pathPoint) { if (!pathPoint || ! pathPoint->parent()) { reset(); } else { path = pathPoint->parent(); point = pathPoint; } return *this; } bool operator != (const PathConnectionPoint &rhs) const { return rhs.path != path || rhs.point != point; } bool operator == (const PathConnectionPoint &rhs) const { return rhs.path == path && rhs.point == point; } bool isValid() const { return path && point; } // checks if the path and point are still valid void validate(KoCanvasBase *canvas) { // no point in validating an already invalid state if (!isValid()) { return; } // we need canvas to validate if (!canvas) { reset(); return; } // check if path is still part of the document if (!canvas->shapeManager()->shapes().contains(path)) { reset(); return; } // check if point is still part of the path if (path->pathPointIndex(point) == KoPathPointIndex(-1, -1)) { reset(); return; } } KoPathShape * path; KoPathPoint * point; }; inline qreal squareDistance(const QPointF &p1, const QPointF &p2) { qreal dx = p1.x() - p2.x(); qreal dy = p1.y() - p2.y(); return dx * dx + dy * dy; } class AngleSnapStrategy : public KoSnapStrategy { public: explicit AngleSnapStrategy(qreal angleStep, bool active) : KoSnapStrategy(KoSnapGuide::CustomSnapping), m_angleStep(angleStep), m_active(active) { } void setStartPoint(const QPointF &startPoint) { m_startPoint = startPoint; } void setAngleStep(qreal angleStep) { m_angleStep = qAbs(angleStep); } bool snap(const QPointF &mousePosition, KoSnapProxy * proxy, qreal maxSnapDistance) override { Q_UNUSED(proxy); if (!m_active) return false; QLineF line(m_startPoint, mousePosition); qreal currentAngle = line.angle(); int prevStep = qAbs(currentAngle / m_angleStep); int nextStep = prevStep + 1; qreal prevAngle = prevStep * m_angleStep; qreal nextAngle = nextStep * m_angleStep; if (qAbs(currentAngle - prevAngle) <= qAbs(currentAngle - nextAngle)) { line.setAngle(prevAngle); } else { line.setAngle(nextAngle); } qreal maxSquareSnapDistance = maxSnapDistance * maxSnapDistance; qreal snapDistance = squareDistance(mousePosition, line.p2()); if (snapDistance > maxSquareSnapDistance) return false; setSnappedPosition(line.p2()); return true; } QPainterPath decoration(const KoViewConverter &converter) const override { Q_UNUSED(converter); QPainterPath decoration; decoration.moveTo(m_startPoint); decoration.lineTo(snappedPosition()); return decoration; } void deactivate() { m_active = false; } void activate() { m_active = true; } private: QPointF m_startPoint; qreal m_angleStep; bool m_active; }; class KoCreatePathToolPrivate : public KoToolBasePrivate { KoCreatePathTool * const q; public: KoCreatePathToolPrivate(KoCreatePathTool * const qq, KoCanvasBase* canvas) : KoToolBasePrivate(qq, canvas), q(qq), shape(0), activePoint(0), firstPoint(0), handleRadius(3), mouseOverFirstPoint(false), pointIsDragged(false), finishAfterThisPoint(false), hoveredPoint(0), angleSnapStrategy(0), angleSnappingDelta(15), angleSnapStatus(false), enableClosePathShortcut(true) { } KoPathShape *shape; KoPathPoint *activePoint; KoPathPoint *firstPoint; int handleRadius; bool mouseOverFirstPoint; bool pointIsDragged; bool finishAfterThisPoint; PathConnectionPoint existingStartPoint; ///< an existing path point we started a new path at PathConnectionPoint existingEndPoint; ///< an existing path point we finished a new path at KoPathPoint *hoveredPoint; ///< an existing path end point the mouse is hovering on bool prevPointWasDragged = false; bool autoSmoothCurves = false; QPointF dragStartPoint; AngleSnapStrategy *angleSnapStrategy; int angleSnappingDelta; bool angleSnapStatus; bool enableClosePathShortcut; void repaintActivePoint() const { const bool isFirstPoint = (activePoint == firstPoint); if (!isFirstPoint && !pointIsDragged) return; QRectF rect = activePoint->boundingRect(false); // make sure that we have the second control point inside our // update rect, as KoPathPoint::boundingRect will not include // the second control point of the last path point if the path // is not closed const QPointF &point = activePoint->point(); const QPointF &controlPoint = activePoint->controlPoint2(); rect = rect.united(QRectF(point, controlPoint).normalized()); // when painting the first point we want the // first control point to be painted as well if (isFirstPoint) { const QPointF &controlPoint = activePoint->controlPoint1(); rect = rect.united(QRectF(point, controlPoint).normalized()); } QPointF border = q->canvas()->viewConverter() ->viewToDocument(QPointF(handleRadius, handleRadius)); rect.adjust(-border.x(), -border.y(), border.x(), border.y()); q->canvas()->updateCanvas(rect); } /// returns the nearest existing path point KoPathPoint* endPointAtPosition(const QPointF &position) const { QRectF roi = q->handleGrabRect(position); QList shapes = q->canvas()->shapeManager()->shapesAt(roi); KoPathPoint * nearestPoint = 0; qreal minDistance = HUGE_VAL; uint grabSensitivity = q->grabSensitivity(); qreal maxDistance = q->canvas()->viewConverter()->viewToDocumentX(grabSensitivity); Q_FOREACH(KoShape * s, shapes) { KoPathShape * path = dynamic_cast(s); if (!path) continue; KoParameterShape *paramShape = dynamic_cast(s); if (paramShape && paramShape->isParametricShape()) continue; KoPathPoint * p = 0; uint subpathCount = path->subpathCount(); for (uint i = 0; i < subpathCount; ++i) { if (path->isClosedSubpath(i)) continue; p = path->pointByIndex(KoPathPointIndex(i, 0)); // check start of subpath qreal d = squareDistance(position, path->shapeToDocument(p->point())); if (d < minDistance && d < maxDistance) { nearestPoint = p; minDistance = d; } // check end of subpath p = path->pointByIndex(KoPathPointIndex(i, path->subpathPointCount(i) - 1)); d = squareDistance(position, path->shapeToDocument(p->point())); if (d < minDistance && d < maxDistance) { nearestPoint = p; minDistance = d; } } } return nearestPoint; } /// Connects given path with the ones we hit when starting/finishing bool connectPaths(KoPathShape *pathShape, const PathConnectionPoint &pointAtStart, const PathConnectionPoint &pointAtEnd) const { KoPathShape * startShape = 0; KoPathShape * endShape = 0; KoPathPoint * startPoint = 0; KoPathPoint * endPoint = 0; if (pointAtStart.isValid()) { startShape = pointAtStart.path; startPoint = pointAtStart.point; } if (pointAtEnd.isValid()) { endShape = pointAtEnd.path; endPoint = pointAtEnd.point; } // at least one point must be valid if (!startPoint && !endPoint) return false; // do not allow connecting to the same point twice if (startPoint == endPoint) endPoint = 0; // we have hit an existing path point on start/finish // what we now do is: // 1. combine the new created path with the ones we hit on start/finish // 2. merge the endpoints of the corresponding subpaths uint newPointCount = pathShape->subpathPointCount(0); KoPathPointIndex newStartPointIndex(0, 0); KoPathPointIndex newEndPointIndex(0, newPointCount - 1); KoPathPoint * newStartPoint = pathShape->pointByIndex(newStartPointIndex); KoPathPoint * newEndPoint = pathShape->pointByIndex(newEndPointIndex); // combine with the path we hit on start KoPathPointIndex startIndex(-1, -1); if (startShape && startPoint) { startIndex = startShape->pathPointIndex(startPoint); pathShape->combine(startShape); pathShape->moveSubpath(0, pathShape->subpathCount() - 1); } // combine with the path we hit on finish KoPathPointIndex endIndex(-1, -1); if (endShape && endPoint) { endIndex = endShape->pathPointIndex(endPoint); if (endShape != startShape) { endIndex.first += pathShape->subpathCount(); pathShape->combine(endShape); } } // do we connect twice to a single subpath ? bool connectToSingleSubpath = (startShape == endShape && startIndex.first == endIndex.first); if (startIndex.second == 0 && !connectToSingleSubpath) { pathShape->reverseSubpath(startIndex.first); startIndex.second = pathShape->subpathPointCount(startIndex.first) - 1; } if (endIndex.second > 0 && !connectToSingleSubpath) { pathShape->reverseSubpath(endIndex.first); endIndex.second = 0; } // after combining we have a path where with the subpaths in the following // order: // 1. the subpaths of the pathshape we started the new path at // 2. the subpath we just created // 3. the subpaths of the pathshape we finished the new path at // get the path points we want to merge, as these are not going to // change while merging KoPathPoint * existingStartPoint = pathShape->pointByIndex(startIndex); KoPathPoint * existingEndPoint = pathShape->pointByIndex(endIndex); // merge first two points if (existingStartPoint) { KoPathPointData pd1(pathShape, pathShape->pathPointIndex(existingStartPoint)); KoPathPointData pd2(pathShape, pathShape->pathPointIndex(newStartPoint)); KoPathPointMergeCommand cmd1(pd1, pd2); cmd1.redo(); } // merge last two points if (existingEndPoint) { KoPathPointData pd3(pathShape, pathShape->pathPointIndex(newEndPoint)); KoPathPointData pd4(pathShape, pathShape->pathPointIndex(existingEndPoint)); KoPathPointMergeCommand cmd2(pd3, pd4); cmd2.redo(); } return true; } void addPathShape() { if (!shape) return; if (shape->pointCount() < 2) { cleanUp(); return; } // this is done so that nothing happens when the mouseReleaseEvent for the this event is received KoPathShape *pathShape = shape; shape = 0; q->addPathShape(pathShape); cleanUp(); return; } void cleanUp() { // reset snap guide q->canvas()->updateCanvas(q->canvas()->snapGuide()->boundingRect()); q->canvas()->snapGuide()->reset(); angleSnapStrategy = 0; delete shape; shape = 0; existingStartPoint = 0; existingEndPoint = 0; hoveredPoint = 0; } void angleDeltaChanged(int value) { angleSnappingDelta = value; if (angleSnapStrategy) angleSnapStrategy->setAngleStep(angleSnappingDelta); } void autoSmoothCurvesChanged(bool value) { autoSmoothCurves = value; KisConfig cfg(false); cfg.setAutoSmoothBezierCurves(value); } void loadAutoSmoothValueFromConfig() { KisConfig cfg(true); autoSmoothCurves = cfg.autoSmoothBezierCurves(); emit q->sigUpdateAutoSmoothCurvesGUI(autoSmoothCurves); } void angleSnapChanged(int angleSnap) { angleSnapStatus = ! angleSnapStatus; if (angleSnapStrategy) { if (angleSnap == Qt::Checked) angleSnapStrategy->activate(); else angleSnapStrategy->deactivate(); } } }; #endif // KOCREATEPATHTOOL_P_H diff --git a/libs/brush/kis_auto_brush.cpp b/libs/brush/kis_auto_brush.cpp index 67289b4660..a0c59ac96e 100644 --- a/libs/brush/kis_auto_brush.cpp +++ b/libs/brush/kis_auto_brush.cpp @@ -1,398 +1,399 @@ /* * Copyright (c) 2004,2007-2009 Cyrille Berger * Copyright (c) 2010 Lukáš Tvrdý * Copyright (c) 2012 Sven Langkamp * * 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 //MSVC requires that Vc come first #include "kis_auto_brush.h" #include #include +#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(_WIN32) || defined(_WIN64) #include #define srand48 srand inline double drand48() { return double(rand()) / RAND_MAX; } #endif struct KisAutoBrush::Private { Private() : randomness(0), density(1.0), idealThreadCountCached(1) {} Private(const Private &rhs) : shape(rhs.shape->clone()), randomness(rhs.randomness), density(rhs.density), idealThreadCountCached(rhs.idealThreadCountCached) { } QScopedPointer shape; qreal randomness; qreal density; int idealThreadCountCached; }; KisAutoBrush::KisAutoBrush(KisMaskGenerator* as, qreal angle, qreal randomness, qreal density) : KisBrush(), d(new Private) { d->shape.reset(as); d->randomness = randomness; d->density = density; d->idealThreadCountCached = QThread::idealThreadCount(); setBrushType(MASK); setWidth(qMax(qreal(1.0), d->shape->width())); setHeight(qMax(qreal(1.0), d->shape->height())); QImage image = createBrushPreview(); setBrushTipImage(image); // Set angle here so brush tip image is generated unrotated setAngle(angle); image = createBrushPreview(); setImage(image); } KisAutoBrush::~KisAutoBrush() { } qreal KisAutoBrush::userEffectiveSize() const { return d->shape->diameter(); } void KisAutoBrush::setUserEffectiveSize(qreal value) { d->shape->setDiameter(value); } KisAutoBrush::KisAutoBrush(const KisAutoBrush& rhs) : KisBrush(rhs), d(new Private(*rhs.d)) { } KisBrush* KisAutoBrush::clone() const { return new KisAutoBrush(*this); } /* It's difficult to predict the mask height when exaclty when there are * more than 2 spikes, so we return an upperbound instead. */ static KisDabShape lieAboutDabShape(KisDabShape const& shape, int spikes) { return spikes > 2 ? KisDabShape(shape.scale(), 1.0, shape.rotation()) : shape; } qint32 KisAutoBrush::maskHeight(KisDabShape const& shape, qreal subPixelX, qreal subPixelY, const KisPaintInformation& info) const { return KisBrush::maskHeight( lieAboutDabShape(shape, maskGenerator()->spikes()), subPixelX, subPixelY, info); } qint32 KisAutoBrush::maskWidth(KisDabShape const& shape, qreal subPixelX, qreal subPixelY, const KisPaintInformation& info) const { return KisBrush::maskWidth( lieAboutDabShape(shape, maskGenerator()->spikes()), subPixelX, subPixelY, info); } QSizeF KisAutoBrush::characteristicSize(KisDabShape const& shape) const { return KisBrush::characteristicSize(lieAboutDabShape(shape, maskGenerator()->spikes())); } inline void fillPixelOptimized_4bytes(quint8 *color, quint8 *buf, int size) { /** * This version of filling uses low granularity of data transfers * (32-bit chunks) and internal processor's parallelism. It reaches * 25% better performance in KisStrokeBenchmark in comparison to * per-pixel memcpy version (tested on Sandy Bridge). */ int block1 = size / 8; int block2 = size % 8; quint32 *src = reinterpret_cast(color); quint32 *dst = reinterpret_cast(buf); // check whether all buffers are 4 bytes aligned // (uncomment if experience some problems) // Q_ASSERT(((qint64)src & 3) == 0); // Q_ASSERT(((qint64)dst & 3) == 0); for (int i = 0; i < block1; i++) { *dst = *src; *(dst + 1) = *src; *(dst + 2) = *src; *(dst + 3) = *src; *(dst + 4) = *src; *(dst + 5) = *src; *(dst + 6) = *src; *(dst + 7) = *src; dst += 8; } for (int i = 0; i < block2; i++) { *dst = *src; dst++; } } inline void fillPixelOptimized_general(quint8 *color, quint8 *buf, int size, int pixelSize) { /** * This version uses internal processor's parallelism and gives * 20% better performance in KisStrokeBenchmark in comparison to * per-pixel memcpy version (tested on Sandy Bridge (+20%) and * on Merom (+10%)). */ int block1 = size / 8; int block2 = size % 8; for (int i = 0; i < block1; i++) { quint8 *d1 = buf; quint8 *d2 = buf + pixelSize; quint8 *d3 = buf + 2 * pixelSize; quint8 *d4 = buf + 3 * pixelSize; quint8 *d5 = buf + 4 * pixelSize; quint8 *d6 = buf + 5 * pixelSize; quint8 *d7 = buf + 6 * pixelSize; quint8 *d8 = buf + 7 * pixelSize; for (int j = 0; j < pixelSize; j++) { *(d1 + j) = color[j]; *(d2 + j) = color[j]; *(d3 + j) = color[j]; *(d4 + j) = color[j]; *(d5 + j) = color[j]; *(d6 + j) = color[j]; *(d7 + j) = color[j]; *(d8 + j) = color[j]; } buf += 8 * pixelSize; } for (int i = 0; i < block2; i++) { memcpy(buf, color, pixelSize); buf += pixelSize; } } void KisAutoBrush::generateMaskAndApplyMaskOrCreateDab(KisFixedPaintDeviceSP dst, KisBrush::ColoringInformation* coloringInformation, KisDabShape const& shape, const KisPaintInformation& info, double subPixelX , double subPixelY, qreal softnessFactor) const { Q_UNUSED(info); // Generate the paint device from the mask const KoColorSpace* cs = dst->colorSpace(); quint32 pixelSize = cs->pixelSize(); // mask dimension methods already includes KisBrush::angle() int dstWidth = maskWidth(shape, subPixelX, subPixelY, info); int dstHeight = maskHeight(shape, subPixelX, subPixelY, info); QPointF hotSpot = this->hotSpot(shape, info); // mask size and hotSpot function take the KisBrush rotation into account qreal angle = shape.rotation() + KisBrush::angle(); // if there's coloring information, we merely change the alpha: in that case, // the dab should be big enough! if (coloringInformation) { // new bounds. we don't care if there is some extra memory occcupied. dst->setRect(QRect(0, 0, dstWidth, dstHeight)); dst->lazyGrowBufferWithoutInitialization(); } else { KIS_SAFE_ASSERT_RECOVER_RETURN(dst->bounds().width() >= dstWidth && dst->bounds().height() >= dstHeight); } quint8* dabPointer = dst->data(); quint8* color = 0; if (coloringInformation) { if (dynamic_cast(coloringInformation)) { color = const_cast(coloringInformation->color()); } } double centerX = hotSpot.x() - 0.5 + subPixelX; double centerY = hotSpot.y() - 0.5 + subPixelY; d->shape->setSoftness(softnessFactor); // softness must be set first d->shape->setScale(shape.scaleX(), shape.scaleY()); if (coloringInformation) { if (color && pixelSize == 4) { fillPixelOptimized_4bytes(color, dabPointer, dstWidth * dstHeight); } else if (color) { fillPixelOptimized_general(color, dabPointer, dstWidth * dstHeight, pixelSize); } else { for (int y = 0; y < dstHeight; y++) { for (int x = 0; x < dstWidth; x++) { memcpy(dabPointer, coloringInformation->color(), pixelSize); coloringInformation->nextColumn(); dabPointer += pixelSize; } coloringInformation->nextRow(); } } } MaskProcessingData data(dst, cs, d->randomness, d->density, centerX, centerY, angle); KisBrushMaskApplicatorBase *applicator = d->shape->applicator(); applicator->initializeData(&data); int jobs = d->idealThreadCountCached; if (threadingAllowed() && dstHeight > 100 && jobs >= 4) { int splitter = dstHeight / jobs; QVector rects; for (int i = 0; i < jobs - 1; i++) { rects << QRect(0, i * splitter, dstWidth, splitter); } rects << QRect(0, (jobs - 1)*splitter, dstWidth, dstHeight - (jobs - 1)*splitter); OperatorWrapper wrapper(applicator); QtConcurrent::blockingMap(rects, wrapper); } else { QRect rect(0, 0, dstWidth, dstHeight); applicator->process(rect); } } void KisAutoBrush::toXML(QDomDocument& doc, QDomElement& e) const { QDomElement shapeElt = doc.createElement("MaskGenerator"); d->shape->toXML(doc, shapeElt); e.appendChild(shapeElt); e.setAttribute("type", "auto_brush"); e.setAttribute("spacing", QString::number(spacing())); e.setAttribute("useAutoSpacing", QString::number(autoSpacingActive())); e.setAttribute("autoSpacingCoeff", QString::number(autoSpacingCoeff())); e.setAttribute("angle", QString::number(KisBrush::angle())); e.setAttribute("randomness", QString::number(d->randomness)); e.setAttribute("density", QString::number(d->density)); KisBrush::toXML(doc, e); } QImage KisAutoBrush::createBrushPreview() { int width = maskWidth(KisDabShape(), 0.0, 0.0, KisPaintInformation()); int height = maskHeight(KisDabShape(), 0.0, 0.0, KisPaintInformation()); KisPaintInformation info(QPointF(width * 0.5, height * 0.5), 0.5, 0, 0, angle(), 0, 0, 0, 0); KisFixedPaintDeviceSP fdev = new KisFixedPaintDevice(KoColorSpaceRegistry::instance()->rgb8()); fdev->setRect(QRect(0, 0, width, height)); fdev->initialize(); mask(fdev, KoColor(Qt::black, fdev->colorSpace()), KisDabShape(), info); return fdev->convertToQImage(0); } const KisMaskGenerator* KisAutoBrush::maskGenerator() const { return d->shape.data(); } qreal KisAutoBrush::density() const { return d->density; } qreal KisAutoBrush::randomness() const { return d->randomness; } QPainterPath KisAutoBrush::outline() const { bool simpleOutline = (d->density < 1.0); if (simpleOutline) { QPainterPath path; QRectF brushBoundingbox(0, 0, width(), height()); if (maskGenerator()->type() == KisMaskGenerator::CIRCLE) { path.addEllipse(brushBoundingbox); } else { // if (maskGenerator()->type() == KisMaskGenerator::RECTANGLE) path.addRect(brushBoundingbox); } return path; } return KisBrush::boundary()->path(); } void KisAutoBrush::lodLimitations(KisPaintopLodLimitations *l) const { KisBrush::lodLimitations(l); if (!qFuzzyCompare(density(), 1.0)) { l->limitations << KoID("auto-brush-density", i18nc("PaintOp instant preview limitation", "Brush Density recommended value 100.0")); } if (!qFuzzyCompare(randomness(), 0.0)) { l->limitations << KoID("auto-brush-randomness", i18nc("PaintOp instant preview limitation", "Brush Randomness recommended value 0.0")); } } diff --git a/libs/brush/kis_boundary.cc b/libs/brush/kis_boundary.cc index a599d8e58b..56954289f4 100644 --- a/libs/brush/kis_boundary.cc +++ b/libs/brush/kis_boundary.cc @@ -1,76 +1,77 @@ /* * Copyright (c) 2005 Bart Coppens * * 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_boundary.h" #include +#include #include #include "KoColorSpace.h" #include "kis_fixed_paint_device.h" #include "kis_outline_generator.h" struct KisBoundary::Private { KisFixedPaintDeviceSP m_device; QVector m_boundary; QPainterPath path; }; KisBoundary::KisBoundary(KisFixedPaintDeviceSP dev) : d(new Private) { d->m_device = dev; } KisBoundary::~KisBoundary() { delete d; } void KisBoundary::generateBoundary() { if (!d->m_device) return; KisOutlineGenerator generator(d->m_device->colorSpace(), OPACITY_TRANSPARENT_U8); generator.setSimpleOutline(true); d->m_boundary = generator.outline(d->m_device->data(), 0, 0, d->m_device->bounds().width(), d->m_device->bounds().height()); d->path = QPainterPath(); Q_FOREACH (const QPolygon & polygon, d->m_boundary) { d->path.addPolygon(polygon); d->path.closeSubpath(); } } void KisBoundary::paint(QPainter& painter) const { QPen pen; pen.setWidth(0); pen.setBrush(Qt::black); painter.setPen(pen); Q_FOREACH (const QPolygon & polygon, d->m_boundary) { painter.drawPolygon(polygon); } } QPainterPath KisBoundary::path() const { return d->path; } diff --git a/libs/brush/kis_brush.cpp b/libs/brush/kis_brush.cpp index 8a0f787edd..956635d9d3 100644 --- a/libs/brush/kis_brush.cpp +++ b/libs/brush/kis_brush.cpp @@ -1,643 +1,644 @@ /* * Copyright (c) 1999 Matthias Elter * Copyright (c) 2003 Patrick Julien * Copyright (c) 2004-2008 Boudewijn Rempt * Copyright (c) 2004 Adrian Page * Copyright (c) 2005 Bart Coppens * Copyright (c) 2007 Cyrille Berger * * 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_brush.h" #include #include +#include #include #include #include #include #include #include #include #include #include "kis_datamanager.h" #include "kis_paint_device.h" #include "kis_global.h" #include "kis_boundary.h" #include "kis_image.h" #include "kis_iterator_ng.h" #include "kis_brush_registry.h" #include #include #include #include #include KisBrush::ColoringInformation::~ColoringInformation() { } KisBrush::PlainColoringInformation::PlainColoringInformation(const quint8* color) : m_color(color) { } KisBrush::PlainColoringInformation::~PlainColoringInformation() { } const quint8* KisBrush::PlainColoringInformation::color() const { return m_color; } void KisBrush::PlainColoringInformation::nextColumn() { } void KisBrush::PlainColoringInformation::nextRow() { } KisBrush::PaintDeviceColoringInformation::PaintDeviceColoringInformation(const KisPaintDeviceSP source, int width) : m_source(source) , m_iterator(m_source->createHLineConstIteratorNG(0, 0, width)) { } KisBrush::PaintDeviceColoringInformation::~PaintDeviceColoringInformation() { } const quint8* KisBrush::PaintDeviceColoringInformation::color() const { return m_iterator->oldRawData(); } void KisBrush::PaintDeviceColoringInformation::nextColumn() { m_iterator->nextPixel(); } void KisBrush::PaintDeviceColoringInformation::nextRow() { m_iterator->nextRow(); } struct KisBrush::Private { Private() : boundary(0) , angle(0) , scale(1.0) , hasColor(false) , brushType(INVALID) , autoSpacingActive(false) , autoSpacingCoeff(1.0) , threadingAllowed(true) {} ~Private() { delete boundary; } mutable KisBoundary* boundary; qreal angle; qreal scale; bool hasColor; enumBrushType brushType; qint32 width; qint32 height; double spacing; QPointF hotSpot; mutable QSharedPointer brushPyramid; QImage brushTipImage; bool autoSpacingActive; qreal autoSpacingCoeff; bool threadingAllowed; }; KisBrush::KisBrush() : KoResource(QString()) , d(new Private) { } KisBrush::KisBrush(const QString& filename) : KoResource(filename) , d(new Private) { } KisBrush::KisBrush(const KisBrush& rhs) : KoResource(QString()) , KisShared() , d(new Private) { setBrushTipImage(rhs.brushTipImage()); d->brushType = rhs.d->brushType; d->width = rhs.d->width; d->height = rhs.d->height; d->spacing = rhs.d->spacing; d->hotSpot = rhs.d->hotSpot; d->hasColor = rhs.d->hasColor; d->angle = rhs.d->angle; d->scale = rhs.d->scale; d->autoSpacingActive = rhs.d->autoSpacingActive; d->autoSpacingCoeff = rhs.d->autoSpacingCoeff; d->threadingAllowed = rhs.d->threadingAllowed; setFilename(rhs.filename()); /** * Be careful! The pyramid is shared between two brush objects, * therefore you cannot change it, only recreate! That i sthe * reason why it is defined as const! */ d->brushPyramid = rhs.d->brushPyramid; // don't copy the boundary, it will be regenerated -- see bug 291910 } KisBrush::~KisBrush() { delete d; } QImage KisBrush::brushTipImage() const { if (d->brushTipImage.isNull()) { const_cast(this)->load(); } return d->brushTipImage; } qint32 KisBrush::width() const { return d->width; } void KisBrush::setWidth(qint32 width) { d->width = width; } qint32 KisBrush::height() const { return d->height; } void KisBrush::setHeight(qint32 height) { d->height = height; } void KisBrush::setHotSpot(QPointF pt) { double x = pt.x(); double y = pt.y(); if (x < 0) x = 0; else if (x >= width()) x = width() - 1; if (y < 0) y = 0; else if (y >= height()) y = height() - 1; d->hotSpot = QPointF(x, y); } QPointF KisBrush::hotSpot(KisDabShape const& shape, const KisPaintInformation& info) const { Q_UNUSED(info); QSizeF metric = characteristicSize(shape); qreal w = metric.width(); qreal h = metric.height(); // The smallest brush we can produce is a single pixel. if (w < 1) { w = 1; } if (h < 1) { h = 1; } // XXX: This should take d->hotSpot into account, though it // isn't specified by gimp brushes so it would default to the center // anyway. QPointF p(w / 2, h / 2); return p; } bool KisBrush::hasColor() const { return d->hasColor; } void KisBrush::setHasColor(bool hasColor) { d->hasColor = hasColor; } bool KisBrush::isPiercedApprox() const { QImage image = brushTipImage(); qreal w = image.width(); qreal h = image.height(); qreal xPortion = qMin(0.1, 5.0 / w); qreal yPortion = qMin(0.1, 5.0 / h); int x0 = std::floor((0.5 - xPortion) * w); int x1 = std::ceil((0.5 + xPortion) * w); int y0 = std::floor((0.5 - yPortion) * h); int y1 = std::ceil((0.5 + yPortion) * h); const int maxNumSamples = (x1 - x0 + 1) * (y1 - y0 + 1); const int failedPixelsThreshold = 0.1 * maxNumSamples; const int thresholdValue = 0.95 * 255; int failedPixels = 0; for (int y = y0; y <= y1; y++) { for (int x = x0; x <= x1; x++) { QRgb pixel = image.pixel(x,y); if (qRed(pixel) > thresholdValue) { failedPixels++; } } } return failedPixels > failedPixelsThreshold; } bool KisBrush::canPaintFor(const KisPaintInformation& /*info*/) { return true; } void KisBrush::setBrushTipImage(const QImage& image) { d->brushTipImage = image; if (!image.isNull()) { if (image.width() > 128 || image.height() > 128) { KoResource::setImage(image.scaled(128, 128, Qt::KeepAspectRatio, Qt::SmoothTransformation)); } else { KoResource::setImage(image); } setWidth(image.width()); setHeight(image.height()); } clearBrushPyramid(); } void KisBrush::setBrushType(enumBrushType type) { d->brushType = type; } enumBrushType KisBrush::brushType() const { return d->brushType; } void KisBrush::predefinedBrushToXML(const QString &type, QDomElement& e) const { e.setAttribute("type", type); e.setAttribute("filename", shortFilename()); e.setAttribute("spacing", QString::number(spacing())); e.setAttribute("useAutoSpacing", QString::number(autoSpacingActive())); e.setAttribute("autoSpacingCoeff", QString::number(autoSpacingCoeff())); e.setAttribute("angle", QString::number(angle())); e.setAttribute("scale", QString::number(scale())); } void KisBrush::toXML(QDomDocument& /*document*/ , QDomElement& element) const { element.setAttribute("BrushVersion", "2"); } KisBrushSP KisBrush::fromXML(const QDomElement& element) { KisBrushSP brush = KisBrushRegistry::instance()->createBrush(element); if (brush && element.attribute("BrushVersion", "1") == "1") { brush->setScale(brush->scale() * 2.0); } return brush; } QSizeF KisBrush::characteristicSize(KisDabShape const& shape) const { KisDabShape normalizedShape( shape.scale() * d->scale, shape.ratio(), normalizeAngle(shape.rotation() + d->angle)); return KisQImagePyramid::characteristicSize( QSize(width(), height()), normalizedShape); } qint32 KisBrush::maskWidth(KisDabShape const& shape, qreal subPixelX, qreal subPixelY, const KisPaintInformation& info) const { Q_UNUSED(info); qreal angle = normalizeAngle(shape.rotation() + d->angle); qreal scale = shape.scale() * d->scale; return KisQImagePyramid::imageSize(QSize(width(), height()), KisDabShape(scale, shape.ratio(), angle), subPixelX, subPixelY).width(); } qint32 KisBrush::maskHeight(KisDabShape const& shape, qreal subPixelX, qreal subPixelY, const KisPaintInformation& info) const { Q_UNUSED(info); qreal angle = normalizeAngle(shape.rotation() + d->angle); qreal scale = shape.scale() * d->scale; return KisQImagePyramid::imageSize(QSize(width(), height()), KisDabShape(scale, shape.ratio(), angle), subPixelX, subPixelY).height(); } double KisBrush::maskAngle(double angle) const { return normalizeAngle(angle + d->angle); } quint32 KisBrush::brushIndex(const KisPaintInformation& info) const { Q_UNUSED(info); return 0; } void KisBrush::setSpacing(double s) { if (s < 0.02) s = 0.02; d->spacing = s; } double KisBrush::spacing() const { return d->spacing; } void KisBrush::setAutoSpacing(bool active, qreal coeff) { d->autoSpacingCoeff = coeff; d->autoSpacingActive = active; } bool KisBrush::autoSpacingActive() const { return d->autoSpacingActive; } qreal KisBrush::autoSpacingCoeff() const { return d->autoSpacingCoeff; } void KisBrush::notifyStrokeStarted() { } void KisBrush::notifyCachedDabPainted(const KisPaintInformation& info) { Q_UNUSED(info); } void KisBrush::prepareForSeqNo(const KisPaintInformation &info, int seqNo) { Q_UNUSED(info); Q_UNUSED(seqNo); } void KisBrush::setThreadingAllowed(bool value) { d->threadingAllowed = value; } bool KisBrush::threadingAllowed() const { return d->threadingAllowed; } void KisBrush::clearBrushPyramid() { d->brushPyramid.reset(new KisSharedQImagePyramid()); } void KisBrush::mask(KisFixedPaintDeviceSP dst, const KoColor& color, KisDabShape const& shape, const KisPaintInformation& info, double subPixelX, double subPixelY, qreal softnessFactor) const { PlainColoringInformation pci(color.data()); generateMaskAndApplyMaskOrCreateDab(dst, &pci, shape, info, subPixelX, subPixelY, softnessFactor); } void KisBrush::mask(KisFixedPaintDeviceSP dst, const KisPaintDeviceSP src, KisDabShape const& shape, const KisPaintInformation& info, double subPixelX, double subPixelY, qreal softnessFactor) const { PaintDeviceColoringInformation pdci(src, maskWidth(shape, subPixelX, subPixelY, info)); generateMaskAndApplyMaskOrCreateDab(dst, &pdci, shape, info, subPixelX, subPixelY, softnessFactor); } void KisBrush::generateMaskAndApplyMaskOrCreateDab(KisFixedPaintDeviceSP dst, ColoringInformation* coloringInformation, KisDabShape const& shape, const KisPaintInformation& info_, double subPixelX, double subPixelY, qreal softnessFactor) const { KIS_SAFE_ASSERT_RECOVER_RETURN(valid()); Q_UNUSED(info_); Q_UNUSED(softnessFactor); QImage outputImage = d->brushPyramid->pyramid(this)->createImage(KisDabShape( shape.scale() * d->scale, shape.ratio(), -normalizeAngle(shape.rotation() + d->angle)), subPixelX, subPixelY); qint32 maskWidth = outputImage.width(); qint32 maskHeight = outputImage.height(); dst->setRect(QRect(0, 0, maskWidth, maskHeight)); dst->lazyGrowBufferWithoutInitialization(); quint8* color = 0; if (coloringInformation) { if (dynamic_cast(coloringInformation)) { color = const_cast(coloringInformation->color()); } } const KoColorSpace *cs = dst->colorSpace(); qint32 pixelSize = cs->pixelSize(); quint8 *dabPointer = dst->data(); quint8 *rowPointer = dabPointer; quint8 *alphaArray = new quint8[maskWidth]; bool hasColor = this->hasColor(); for (int y = 0; y < maskHeight; y++) { const quint8* maskPointer = outputImage.constScanLine(y); if (coloringInformation) { for (int x = 0; x < maskWidth; x++) { if (color) { memcpy(dabPointer, color, pixelSize); } else { memcpy(dabPointer, coloringInformation->color(), pixelSize); coloringInformation->nextColumn(); } dabPointer += pixelSize; } } if (hasColor) { const quint8 *src = maskPointer; quint8 *dst = alphaArray; for (int x = 0; x < maskWidth; x++) { const QRgb *c = reinterpret_cast(src); *dst = KoColorSpaceMaths::multiply(255 - qGray(*c), qAlpha(*c)); src += 4; dst++; } } else { const quint8 *src = maskPointer; quint8 *dst = alphaArray; for (int x = 0; x < maskWidth; x++) { const QRgb *c = reinterpret_cast(src); *dst = KoColorSpaceMaths::multiply(255 - *src, qAlpha(*c)); src += 4; dst++; } } cs->applyAlphaU8Mask(rowPointer, alphaArray, maskWidth); rowPointer += maskWidth * pixelSize; dabPointer = rowPointer; if (!color && coloringInformation) { coloringInformation->nextRow(); } } delete[] alphaArray; } KisFixedPaintDeviceSP KisBrush::paintDevice(const KoColorSpace * colorSpace, KisDabShape const& shape, const KisPaintInformation& info, double subPixelX, double subPixelY) const { Q_ASSERT(valid()); Q_UNUSED(info); double angle = normalizeAngle(shape.rotation() + d->angle); double scale = shape.scale() * d->scale; QImage outputImage = d->brushPyramid->pyramid(this)->createImage( KisDabShape(scale, shape.ratio(), -angle), subPixelX, subPixelY); KisFixedPaintDeviceSP dab = new KisFixedPaintDevice(colorSpace); Q_CHECK_PTR(dab); dab->convertFromQImage(outputImage, ""); return dab; } void KisBrush::resetBoundary() { delete d->boundary; d->boundary = 0; } void KisBrush::generateBoundary() const { KisFixedPaintDeviceSP dev; KisDabShape inverseTransform(1.0 / scale(), 1.0, -angle()); if (brushType() == IMAGE || brushType() == PIPE_IMAGE) { dev = paintDevice(KoColorSpaceRegistry::instance()->rgb8(), inverseTransform, KisPaintInformation()); } else { const KoColorSpace* cs = KoColorSpaceRegistry::instance()->rgb8(); dev = new KisFixedPaintDevice(cs); mask(dev, KoColor(Qt::black, cs), inverseTransform, KisPaintInformation()); } d->boundary = new KisBoundary(dev); d->boundary->generateBoundary(); } const KisBoundary* KisBrush::boundary() const { if (!d->boundary) generateBoundary(); return d->boundary; } void KisBrush::setScale(qreal _scale) { d->scale = _scale; } qreal KisBrush::scale() const { return d->scale; } void KisBrush::setAngle(qreal _rotation) { d->angle = _rotation; } qreal KisBrush::angle() const { return d->angle; } QPainterPath KisBrush::outline() const { return boundary()->path(); } void KisBrush::lodLimitations(KisPaintopLodLimitations *l) const { if (spacing() > 0.5) { l->limitations << KoID("huge-spacing", i18nc("PaintOp instant preview limitation", "Spacing > 0.5, consider disabling Instant Preview")); } } diff --git a/libs/flake/KoClipMask.cpp b/libs/flake/KoClipMask.cpp index 8a21648f32..364d731464 100644 --- a/libs/flake/KoClipMask.cpp +++ b/libs/flake/KoClipMask.cpp @@ -1,176 +1,177 @@ /* * 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. */ #include "KoClipMask.h" #include #include #include +#include #include #include "kis_algebra_2d.h" #include #include struct Q_DECL_HIDDEN KoClipMask::Private { Private() {} Private(const Private &rhs) : coordinates(rhs.coordinates), contentCoordinates(rhs.contentCoordinates), maskRect(rhs.maskRect), extraShapeTransform(rhs.extraShapeTransform) { Q_FOREACH (KoShape *shape, rhs.shapes) { KoShape *clonedShape = shape->cloneShape(); KIS_ASSERT_RECOVER(clonedShape) { continue; } shapes << clonedShape; } } ~Private() { qDeleteAll(shapes); shapes.clear(); } KoFlake::CoordinateSystem coordinates = KoFlake::ObjectBoundingBox; KoFlake::CoordinateSystem contentCoordinates = KoFlake::UserSpaceOnUse; QRectF maskRect = QRectF(-0.1, -0.1, 1.2, 1.2); QList shapes; QTransform extraShapeTransform; // TODO: not used anymore, use direct shape transform instead }; KoClipMask::KoClipMask() : m_d(new Private) { } KoClipMask::~KoClipMask() { } KoClipMask::KoClipMask(const KoClipMask &rhs) : m_d(new Private(*rhs.m_d)) { } KoClipMask *KoClipMask::clone() const { return new KoClipMask(*this); } KoFlake::CoordinateSystem KoClipMask::coordinates() const { return m_d->coordinates; } void KoClipMask::setCoordinates(KoFlake::CoordinateSystem value) { m_d->coordinates = value; } KoFlake::CoordinateSystem KoClipMask::contentCoordinates() const { return m_d->contentCoordinates; } void KoClipMask::setContentCoordinates(KoFlake::CoordinateSystem value) { m_d->contentCoordinates = value; } QRectF KoClipMask::maskRect() const { return m_d->maskRect; } void KoClipMask::setMaskRect(const QRectF &value) { m_d->maskRect = value; } QList KoClipMask::shapes() const { return m_d->shapes; } void KoClipMask::setShapes(const QList &value) { m_d->shapes = value; } bool KoClipMask::isEmpty() const { return m_d->shapes.isEmpty(); } void KoClipMask::setExtraShapeOffset(const QPointF &value) { /** * TODO: when we implement source shapes sharing, please wrap the shapes * into a group and apply this transform to the group instead */ if (m_d->contentCoordinates == KoFlake::UserSpaceOnUse) { const QTransform t = QTransform::fromTranslate(value.x(), value.y()); Q_FOREACH (KoShape *shape, m_d->shapes) { shape->applyAbsoluteTransformation(t); } } if (m_d->coordinates == KoFlake::UserSpaceOnUse) { m_d->maskRect.translate(value); } } void KoClipMask::drawMask(QPainter *painter, KoShape *shape) { painter->save(); QPainterPath clipPathInShapeSpace; if (m_d->coordinates == KoFlake::ObjectBoundingBox) { QTransform relativeToShape = KisAlgebra2D::mapToRect(shape->outlineRect()); clipPathInShapeSpace.addPolygon(relativeToShape.map(m_d->maskRect)); } else { clipPathInShapeSpace.addRect(m_d->maskRect); clipPathInShapeSpace = m_d->extraShapeTransform.map(clipPathInShapeSpace); } painter->setClipPath(clipPathInShapeSpace, Qt::IntersectClip); if (m_d->contentCoordinates == KoFlake::ObjectBoundingBox) { QTransform relativeToShape = KisAlgebra2D::mapToRect(shape->outlineRect()); painter->setTransform(relativeToShape, true); } else { painter->setTransform(m_d->extraShapeTransform, true); } KoViewConverter converter; KoShapePainter p; p.setShapes(m_d->shapes); p.paint(*painter, converter); painter->restore(); } diff --git a/libs/flake/KoClipMaskPainter.cpp b/libs/flake/KoClipMaskPainter.cpp index 4fa456c825..e7ea496c8a 100644 --- a/libs/flake/KoClipMaskPainter.cpp +++ b/libs/flake/KoClipMaskPainter.cpp @@ -1,134 +1,135 @@ /* * 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. */ #include "KoClipMaskPainter.h" #include +#include #include #include "kis_assert.h" struct Q_DECL_HIDDEN KoClipMaskPainter::Private { QPainter *globalPainter; QImage shapeImage; QImage maskImage; QPainter shapePainter; QPainter maskPainter; QRect alignedGlobalClipRect; }; KoClipMaskPainter::KoClipMaskPainter(QPainter *painter, const QRectF &globalClipRect) : m_d(new Private) { m_d->globalPainter = painter; m_d->alignedGlobalClipRect = globalClipRect.toAlignedRect(); m_d->shapeImage = QImage(m_d->alignedGlobalClipRect.size(), QImage::Format_ARGB32); m_d->maskImage = QImage(m_d->alignedGlobalClipRect.size(), QImage::Format_ARGB32); m_d->shapeImage.fill(0); m_d->maskImage.fill(0); QTransform moveToBufferTransform = QTransform::fromTranslate(-m_d->alignedGlobalClipRect.x(), -m_d->alignedGlobalClipRect.y()); m_d->shapePainter.begin(&m_d->shapeImage); m_d->shapePainter.setTransform(moveToBufferTransform); m_d->shapePainter.setTransform(painter->transform(), true); if (painter->hasClipping()) { m_d->shapePainter.setClipPath(painter->clipPath()); } m_d->shapePainter.setOpacity(painter->opacity()); m_d->shapePainter.setBrush(painter->brush()); m_d->shapePainter.setPen(painter->pen()); m_d->maskPainter.begin(&m_d->maskImage); m_d->maskPainter.setTransform(moveToBufferTransform); m_d->maskPainter.setTransform(painter->transform(), true); if (painter->hasClipping()) { m_d->maskPainter.setClipPath(painter->clipPath()); } m_d->maskPainter.setOpacity(painter->opacity()); m_d->maskPainter.setBrush(painter->brush()); m_d->maskPainter.setPen(painter->pen()); } KoClipMaskPainter::~KoClipMaskPainter() { } QPainter *KoClipMaskPainter::shapePainter() { return &m_d->shapePainter; } QPainter *KoClipMaskPainter::maskPainter() { return &m_d->maskPainter; } void KoClipMaskPainter::renderOnGlobalPainter() { KIS_ASSERT_RECOVER_RETURN(m_d->maskImage.size() == m_d->shapeImage.size()); for (int y = 0; y < m_d->maskImage.height(); y++) { QRgb *shapeData = reinterpret_cast(m_d->shapeImage.scanLine(y)); QRgb *maskData = reinterpret_cast(m_d->maskImage.scanLine(y)); for (int x = 0; x < m_d->maskImage.width(); x++) { const qreal normCoeff = 1.0 / 255.0 * 255.0; qreal maskValue = qreal(qAlpha(*maskData)) * (0.2125 * qRed(*maskData) + 0.7154 * qGreen(*maskData) + 0.0721 * qBlue(*maskData)); int alpha = qRound(maskValue * qAlpha(*shapeData) * normCoeff); *shapeData = (alpha << 24) | (*shapeData & 0x00ffffff); shapeData++; maskData++; } } KIS_ASSERT_RECOVER_RETURN(m_d->shapeImage.size() == m_d->alignedGlobalClipRect.size()); QPainterPath globalClipPath; if (m_d->globalPainter->hasClipping()) { globalClipPath = m_d->globalPainter->transform().map(m_d->globalPainter->clipPath()); } m_d->globalPainter->save(); m_d->globalPainter->setTransform(QTransform()); if (!globalClipPath.isEmpty()) { m_d->globalPainter->setClipPath(globalClipPath); } m_d->globalPainter->drawImage(m_d->alignedGlobalClipRect.topLeft(), m_d->shapeImage); m_d->globalPainter->restore(); } diff --git a/libs/flake/KoConnectionShape.cpp b/libs/flake/KoConnectionShape.cpp index e8b5c8003a..d8dfee328f 100644 --- a/libs/flake/KoConnectionShape.cpp +++ b/libs/flake/KoConnectionShape.cpp @@ -1,780 +1,781 @@ /* This file is part of the KDE project * Copyright (C) 2007 Boudewijn Rempt * Copyright (C) 2007,2009 Thorsten Zachmann * Copyright (C) 2007,2009,2010 Jan Hambrecht * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "KoConnectionShape.h" #include "KoConnectionShape_p.h" #include "KoViewConverter.h" #include "KoShapeLoadingContext.h" #include "KoShapeSavingContext.h" #include "KoConnectionShapeLoadingUpdater.h" #include "KoPathShapeLoader.h" #include "KoPathPoint.h" #include "KoShapeBackground.h" #include #include #include #include #include +#include #include KoConnectionShapePrivate::KoConnectionShapePrivate(KoConnectionShape *q) : KoParameterShapePrivate(q), shape1(0), shape2(0), connectionPointId1(-1), connectionPointId2(-1), connectionType(KoConnectionShape::Standard), forceUpdate(false), hasCustomPath(false) { } KoConnectionShapePrivate::KoConnectionShapePrivate(const KoConnectionShapePrivate &rhs, KoConnectionShape *q) : KoParameterShapePrivate(rhs, q), path(rhs.path), shape1(0), // FIXME: it should point to the new shapes!!! shape2(0), // FIXME: it should point to the new shapes!!! connectionPointId1(rhs.connectionPointId1), connectionPointId2(rhs.connectionPointId2), connectionType(rhs.connectionType), forceUpdate(rhs.forceUpdate), hasCustomPath(rhs.hasCustomPath) { } QPointF KoConnectionShapePrivate::escapeDirection(int handleId) const { Q_Q(const KoConnectionShape); QPointF direction; if (handleConnected(handleId)) { KoShape *attachedShape = handleId == KoConnectionShape::StartHandle ? shape1 : shape2; int connectionPointId = handleId == KoConnectionShape::StartHandle ? connectionPointId1 : connectionPointId2; KoConnectionPoint::EscapeDirection ed = attachedShape->connectionPoint(connectionPointId).escapeDirection; if (ed == KoConnectionPoint::AllDirections) { QPointF handlePoint = q->shapeToDocument(handles[handleId]); QPointF centerPoint = attachedShape->absolutePosition(KoFlake::Center); /* * Determine the best escape direction from the position of the handle point * and the position and orientation of the attached shape. * The idea is to define 4 sectors, one for each edge of the attached shape. * Each sector starts at the center point of the attached shape and has it * left and right edge going through the two points which define the edge. * Then we check which sector contains our handle point, for which we can * simply calculate the corresponding direction which is orthogonal to the * corresponding bounding box edge. * From that we derive the escape direction from looking at the main coordinate * of the orthogonal direction. */ // define our edge points in the right order const KoFlake::AnchorPosition corners[4] = { KoFlake::BottomRight, KoFlake::BottomLeft, KoFlake::TopLeft, KoFlake::TopRight }; QPointF vHandle = handlePoint-centerPoint; for (int i = 0; i < 4; ++i) { // first point of bounding box edge QPointF p1 = attachedShape->absolutePosition(corners[i]); // second point of bounding box edge QPointF p2 = attachedShape->absolutePosition(corners[(i+1)%4]); // check on which side of the first sector edge our second sector edge is const qreal c0 = crossProd(p1-centerPoint, p2-centerPoint); // check on which side of the first sector edge our handle point is const qreal c1 = crossProd(p1-centerPoint, vHandle); // second edge and handle point must be on the same side of first edge if ((c0 < 0 && c1 > 0) || (c0 > 0 && c1 < 0)) continue; // check on which side of the handle point our second sector edge is const qreal c2 = crossProd(vHandle, p2-centerPoint); // second edge must be on the same side of the handle point as on first edge if ((c0 < 0 && c2 > 0) || (c0 > 0 && c2 < 0)) continue; // now we found the correct edge QPointF vDir = 0.5 *(p1+p2) - centerPoint; // look at coordinate with the greatest absolute value // and construct our escape direction accordingly const qreal xabs = qAbs(vDir.x()); const qreal yabs = qAbs(vDir.y()); if (xabs > yabs) { direction.rx() = vDir.x() > 0 ? 1.0 : -1.0; direction.ry() = 0.0; } else { direction.rx() = 0.0; direction.ry() = vDir.y() > 0 ? 1.0 : -1.0; } break; } } else if (ed == KoConnectionPoint::HorizontalDirections) { QPointF handlePoint = q->shapeToDocument(handles[handleId]); QPointF centerPoint = attachedShape->absolutePosition(KoFlake::Center); // use horizontal direction pointing away from center point if (handlePoint.x() < centerPoint.x()) direction = QPointF(-1.0, 0.0); else direction = QPointF(1.0, 0.0); } else if (ed == KoConnectionPoint::VerticalDirections) { QPointF handlePoint = q->shapeToDocument(handles[handleId]); QPointF centerPoint = attachedShape->absolutePosition(KoFlake::Center); // use vertical direction pointing away from center point if (handlePoint.y() < centerPoint.y()) direction = QPointF(0.0, -1.0); else direction = QPointF(0.0, 1.0); } else if (ed == KoConnectionPoint::LeftDirection) { direction = QPointF(-1.0, 0.0); } else if (ed == KoConnectionPoint::RightDirection) { direction = QPointF(1.0, 0.0); } else if (ed == KoConnectionPoint::UpDirection) { direction = QPointF(0.0, -1.0); } else if (ed == KoConnectionPoint::DownDirection) { direction = QPointF(0.0, 1.0); } // transform escape direction by using our own transformation matrix QTransform invMatrix = q->absoluteTransformation(0).inverted(); direction = invMatrix.map(direction) - invMatrix.map(QPointF()); direction /= sqrt(direction.x() * direction.x() + direction.y() * direction.y()); } return direction; } bool KoConnectionShapePrivate::intersects(const QPointF &p1, const QPointF &d1, const QPointF &p2, const QPointF &d2, QPointF &isect) { qreal sp1 = scalarProd(d1, p2 - p1); if (sp1 < 0.0) return false; qreal sp2 = scalarProd(d2, p1 - p2); if (sp2 < 0.0) return false; // use cross product to check if rays intersects at all qreal cp = crossProd(d1, d2); if (cp == 0.0) { // rays are parallel or coincident if (p1.x() == p2.x() && d1.x() == 0.0 && d1.y() != d2.y()) { // vertical, coincident isect = 0.5 * (p1 + p2); } else if (p1.y() == p2.y() && d1.y() == 0.0 && d1.x() != d2.x()) { // horizontal, coincident isect = 0.5 * (p1 + p2); } else { return false; } } else { // they are intersecting normally isect = p1 + sp1 * d1; } return true; } QPointF KoConnectionShapePrivate::perpendicularDirection(const QPointF &p1, const QPointF &d1, const QPointF &p2) { QPointF perpendicular(d1.y(), -d1.x()); qreal sp = scalarProd(perpendicular, p2 - p1); if (sp < 0.0) perpendicular *= -1.0; return perpendicular; } void KoConnectionShapePrivate::normalPath(const qreal MinimumEscapeLength) { // Clear the path to build it again. path.clear(); path.append(handles[KoConnectionShape::StartHandle]); QList edges1; QList edges2; QPointF direction1 = escapeDirection(KoConnectionShape::StartHandle); QPointF direction2 = escapeDirection(KoConnectionShape::EndHandle); QPointF edgePoint1 = handles[KoConnectionShape::StartHandle] + MinimumEscapeLength * direction1; QPointF edgePoint2 = handles[KoConnectionShape::EndHandle] + MinimumEscapeLength * direction2; edges1.append(edgePoint1); edges2.prepend(edgePoint2); if (handleConnected(KoConnectionShape::StartHandle) && handleConnected(KoConnectionShape::EndHandle)) { QPointF intersection; // TODO: check if this loop actually ever exits? (DK) while (true) { // first check if directions from current edge points intersect if (intersects(edgePoint1, direction1, edgePoint2, direction2, intersection)) { // directions intersect, we have another edge point and be done edges1.append(intersection); break; } // check if we are going toward the other handle qreal sp = scalarProd(direction1, edgePoint2 - edgePoint1); if (sp >= 0.0) { // if we are having the same direction, go all the way toward // the other handle, else only go half the way if (direction1 == direction2) edgePoint1 += sp * direction1; else edgePoint1 += 0.5 * sp * direction1; edges1.append(edgePoint1); // switch direction direction1 = perpendicularDirection(edgePoint1, direction1, edgePoint2); } else { // we are not going into the same direction, so switch direction direction1 = perpendicularDirection(edgePoint1, direction1, edgePoint2); } } } path.append(edges1); path.append(edges2); path.append(handles[KoConnectionShape::EndHandle]); } qreal KoConnectionShapePrivate::scalarProd(const QPointF &v1, const QPointF &v2) const { return v1.x() * v2.x() + v1.y() * v2.y(); } qreal KoConnectionShapePrivate::crossProd(const QPointF &v1, const QPointF &v2) const { return v1.x() * v2.y() - v1.y() * v2.x(); } bool KoConnectionShapePrivate::handleConnected(int handleId) const { if (handleId == KoConnectionShape::StartHandle && shape1 && connectionPointId1 >= 0) return true; if (handleId == KoConnectionShape::EndHandle && shape2 && connectionPointId2 >= 0) return true; return false; } void KoConnectionShape::updateConnections() { Q_D(KoConnectionShape); bool updateHandles = false; if (d->handleConnected(StartHandle)) { if (d->shape1->hasConnectionPoint(d->connectionPointId1)) { // map connection point into our shape coordinates QPointF p = documentToShape(d->shape1->absoluteTransformation(0).map(d->shape1->connectionPoint(d->connectionPointId1).position)); if (d->handles[StartHandle] != p) { d->handles[StartHandle] = p; updateHandles = true; } } } if (d->handleConnected(EndHandle)) { if (d->shape2->hasConnectionPoint(d->connectionPointId2)) { // map connection point into our shape coordinates QPointF p = documentToShape(d->shape2->absoluteTransformation(0).map(d->shape2->connectionPoint(d->connectionPointId2).position)); if (d->handles[EndHandle] != p) { d->handles[EndHandle] = p; updateHandles = true; } } } if (updateHandles || d->forceUpdate) { update(); // ugly, for repainting the connection we just changed updatePath(QSizeF()); update(); // ugly, for repainting the connection we just changed d->forceUpdate = false; } } KoConnectionShape::KoConnectionShape() : KoParameterShape(new KoConnectionShapePrivate(this)) { Q_D(KoConnectionShape); d->handles.push_back(QPointF(0, 0)); d->handles.push_back(QPointF(140, 140)); moveTo(d->handles[StartHandle]); lineTo(d->handles[EndHandle]); updatePath(QSizeF(140, 140)); clearConnectionPoints(); } KoConnectionShape::KoConnectionShape(const KoConnectionShape &rhs) : KoParameterShape(new KoConnectionShapePrivate(*rhs.d_func(), this)) { } KoConnectionShape::~KoConnectionShape() { Q_D(KoConnectionShape); if (d->shape1) d->shape1->removeDependee(this); if (d->shape2) d->shape2->removeDependee(this); } KoShape *KoConnectionShape::cloneShape() const { return new KoConnectionShape(*this); } void KoConnectionShape::saveOdf(KoShapeSavingContext & context) const { Q_D(const KoConnectionShape); context.xmlWriter().startElement("draw:connector"); saveOdfAttributes(context, OdfMandatories | OdfAdditionalAttributes); switch (d->connectionType) { case Lines: context.xmlWriter().addAttribute("draw:type", "lines"); break; case Straight: context.xmlWriter().addAttribute("draw:type", "line"); break; case Curve: context.xmlWriter().addAttribute("draw:type", "curve"); break; default: context.xmlWriter().addAttribute("draw:type", "standard"); break; } if (d->shape1) { context.xmlWriter().addAttribute("draw:start-shape", context.xmlid(d->shape1, "shape", KoElementReference::Counter).toString()); context.xmlWriter().addAttribute("draw:start-glue-point", d->connectionPointId1); } else { QPointF p(shapeToDocument(d->handles[StartHandle]) * context.shapeOffset(this)); context.xmlWriter().addAttribute("svg:x1", p.x()); context.xmlWriter().addAttribute("svg:y1", p.y()); } if (d->shape2) { context.xmlWriter().addAttribute("draw:end-shape", context.xmlid(d->shape2, "shape", KoElementReference::Counter).toString()); context.xmlWriter().addAttribute("draw:end-glue-point", d->connectionPointId2); } else { QPointF p(shapeToDocument(d->handles[EndHandle]) * context.shapeOffset(this)); context.xmlWriter().addAttribute("svg:x2", p.x()); context.xmlWriter().addAttribute("svg:y2", p.y()); } // write the path data context.xmlWriter().addAttribute("svg:d", toString()); saveOdfAttributes(context, OdfViewbox); saveOdfCommonChildElements(context); saveText(context); context.xmlWriter().endElement(); } bool KoConnectionShape::loadOdf(const KoXmlElement & element, KoShapeLoadingContext &context) { Q_D(KoConnectionShape); loadOdfAttributes(element, context, OdfMandatories | OdfCommonChildElements | OdfAdditionalAttributes); QString type = element.attributeNS(KoXmlNS::draw, "type", "standard"); if (type == "lines") d->connectionType = Lines; else if (type == "line") d->connectionType = Straight; else if (type == "curve") d->connectionType = Curve; else d->connectionType = Standard; // reset connection point indices d->connectionPointId1 = -1; d->connectionPointId2 = -1; // reset connected shapes d->shape1 = 0; d->shape2 = 0; if (element.hasAttributeNS(KoXmlNS::draw, "start-shape")) { d->connectionPointId1 = element.attributeNS(KoXmlNS::draw, "start-glue-point", QString()).toInt(); QString shapeId1 = element.attributeNS(KoXmlNS::draw, "start-shape", QString()); debugFlake << "references start-shape" << shapeId1 << "at glue-point" << d->connectionPointId1; d->shape1 = context.shapeById(shapeId1); if (d->shape1) { debugFlake << "start-shape was already loaded"; d->shape1->addDependee(this); if (d->shape1->hasConnectionPoint(d->connectionPointId1)) { debugFlake << "connecting to start-shape"; d->handles[StartHandle] = d->shape1->absoluteTransformation(0).map(d->shape1->connectionPoint(d->connectionPointId1).position); debugFlake << "start handle position =" << d->handles[StartHandle]; } } else { debugFlake << "start-shape not loaded yet, deferring connection"; context.updateShape(shapeId1, new KoConnectionShapeLoadingUpdater(this, KoConnectionShapeLoadingUpdater::First)); } } else { d->handles[StartHandle].setX(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "x1", QString()))); d->handles[StartHandle].setY(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "y1", QString()))); } if (element.hasAttributeNS(KoXmlNS::draw, "end-shape")) { d->connectionPointId2 = element.attributeNS(KoXmlNS::draw, "end-glue-point", "").toInt(); QString shapeId2 = element.attributeNS(KoXmlNS::draw, "end-shape", ""); debugFlake << "references end-shape " << shapeId2 << "at glue-point" << d->connectionPointId2; d->shape2 = context.shapeById(shapeId2); if (d->shape2) { debugFlake << "end-shape was already loaded"; d->shape2->addDependee(this); if (d->shape2->hasConnectionPoint(d->connectionPointId2)) { debugFlake << "connecting to end-shape"; d->handles[EndHandle] = d->shape2->absoluteTransformation(0).map(d->shape2->connectionPoint(d->connectionPointId2).position); debugFlake << "end handle position =" << d->handles[EndHandle]; } } else { debugFlake << "end-shape not loaded yet, deferring connection"; context.updateShape(shapeId2, new KoConnectionShapeLoadingUpdater(this, KoConnectionShapeLoadingUpdater::Second)); } } else { d->handles[EndHandle].setX(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "x2", QString()))); d->handles[EndHandle].setY(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "y2", QString()))); } QString skew = element.attributeNS(KoXmlNS::draw, "line-skew", QString()); QStringList skewValues = skew.simplified().split(' ', QString::SkipEmptyParts); // TODO apply skew values once we support them // load the path data if there is any d->hasCustomPath = element.hasAttributeNS(KoXmlNS::svg, "d"); if (d->hasCustomPath) { KoPathShapeLoader loader(this); loader.parseSvg(element.attributeNS(KoXmlNS::svg, "d"), true); if (d->subpaths.size() > 0) { QRectF viewBox = loadOdfViewbox(element); if (viewBox.isEmpty()) { // there should be a viewBox to transform the path data // if there is none, use the bounding rectangle of the parsed path viewBox = outline().boundingRect(); } // convert path to viewbox coordinates to have a bounding rect of (0,0 1x1) // which can later be fitted back into the target rect once we have all // the required information QTransform viewMatrix; viewMatrix.scale(viewBox.width() ? static_cast(1.0) / viewBox.width() : 1.0, viewBox.height() ? static_cast(1.0) / viewBox.height() : 1.0); viewMatrix.translate(-viewBox.left(), -viewBox.top()); d->map(viewMatrix); // trigger finishing the connections in case we have all data // otherwise it gets called again once the shapes we are // connected to are loaded } else { d->hasCustomPath = false; } finishLoadingConnection(); } else { d->forceUpdate = true; updateConnections(); } loadText(element, context); return true; } void KoConnectionShape::finishLoadingConnection() { Q_D(KoConnectionShape); if (d->hasCustomPath) { const bool loadingFinished1 = d->connectionPointId1 >= 0 ? d->shape1 != 0 : true; const bool loadingFinished2 = d->connectionPointId2 >= 0 ? d->shape2 != 0 : true; if (loadingFinished1 && loadingFinished2) { QPointF p1, p2; if (d->handleConnected(StartHandle)) { if (d->shape1->hasConnectionPoint(d->connectionPointId1)) { p1 = d->shape1->absoluteTransformation(0).map(d->shape1->connectionPoint(d->connectionPointId1).position); } } else { p1 = d->handles[StartHandle]; } if (d->handleConnected(EndHandle)) { if (d->shape2->hasConnectionPoint(d->connectionPointId2)) { p2 = d->shape2->absoluteTransformation(0).map(d->shape2->connectionPoint(d->connectionPointId2).position); } } else { p2 = d->handles[EndHandle]; } QPointF relativeBegin = d->subpaths.first()->first()->point(); QPointF relativeEnd = d->subpaths.last()->last()->point(); QPointF diffRelative(relativeBegin - relativeEnd); QPointF diffAbsolute(p1 - p2); qreal factorX = diffRelative.x() ? diffAbsolute.x() / diffRelative.x(): 1.0; qreal factorY = diffRelative.y() ? diffAbsolute.y() / diffRelative.y(): 1.0; p1.setX(p1.x() - relativeBegin.x() * factorX); p1.setY(p1.y() - relativeBegin.y() * factorY); p2.setX(p2.x() + (1 - relativeEnd.x()) * factorX); p2.setY(p2.y() + (1 - relativeEnd.y()) * factorY); QRectF targetRect = QRectF(p1, p2).normalized(); // transform the normalized coordinates back to our target rectangle QTransform viewMatrix; viewMatrix.translate(targetRect.x(), targetRect.y()); viewMatrix.scale(targetRect.width(), targetRect.height()); d->map(viewMatrix); // pretend we are during a forced update, so normalize() // will not trigger an updateConnections() call d->forceUpdate = true; normalize(); d->forceUpdate = false; } } else { updateConnections(); } } void KoConnectionShape::moveHandleAction(int handleId, const QPointF &point, Qt::KeyboardModifiers modifiers) { Q_UNUSED(modifiers); Q_D(KoConnectionShape); if (handleId >= d->handles.size()) return; d->handles[handleId] = point; } void KoConnectionShape::updatePath(const QSizeF &size) { Q_UNUSED(size); Q_D(KoConnectionShape); const qreal MinimumEscapeLength = (qreal)20.; clear(); switch (d->connectionType) { case Standard: { d->normalPath(MinimumEscapeLength); if (d->path.count() != 0){ moveTo(d->path[0]); for (int index = 1; index < d->path.count(); ++index) lineTo(d->path[index]); } break; } case Lines: { QPointF direction1 = d->escapeDirection(0); QPointF direction2 = d->escapeDirection(d->handles.count() - 1); moveTo(d->handles[StartHandle]); if (! direction1.isNull()) lineTo(d->handles[StartHandle] + MinimumEscapeLength * direction1); if (! direction2.isNull()) lineTo(d->handles[EndHandle] + MinimumEscapeLength * direction2); lineTo(d->handles[EndHandle]); break; } case Straight: moveTo(d->handles[StartHandle]); lineTo(d->handles[EndHandle]); break; case Curve: // TODO QPointF direction1 = d->escapeDirection(0); QPointF direction2 = d->escapeDirection(d->handles.count() - 1); moveTo(d->handles[StartHandle]); if (! direction1.isNull() && ! direction2.isNull()) { QPointF curvePoint1 = d->handles[StartHandle] + 5.0 * MinimumEscapeLength * direction1; QPointF curvePoint2 = d->handles[EndHandle] + 5.0 * MinimumEscapeLength * direction2; curveTo(curvePoint1, curvePoint2, d->handles[EndHandle]); } else { lineTo(d->handles[EndHandle]); } break; } normalize(); } bool KoConnectionShape::connectFirst(KoShape * shape1, int connectionPointId) { Q_D(KoConnectionShape); // refuse to connect to a shape that depends on us (e.g. a artistic text shape) if (hasDependee(shape1)) return false; if (shape1) { // check if the connection point does exist if (!shape1->hasConnectionPoint(connectionPointId)) return false; // do not connect to the same connection point twice if (d->shape2 == shape1 && d->connectionPointId2 == connectionPointId) return false; } if (d->shape1) d->shape1->removeDependee(this); d->shape1 = shape1; if (d->shape1) d->shape1->addDependee(this); d->connectionPointId1 = connectionPointId; return true; } bool KoConnectionShape::connectSecond(KoShape * shape2, int connectionPointId) { Q_D(KoConnectionShape); // refuse to connect to a shape that depends on us (e.g. a artistic text shape) if (hasDependee(shape2)) return false; if (shape2) { // check if the connection point does exist if (!shape2->hasConnectionPoint(connectionPointId)) return false; // do not connect to the same connection point twice if (d->shape1 == shape2 && d->connectionPointId1 == connectionPointId) return false; } if (d->shape2) d->shape2->removeDependee(this); d->shape2 = shape2; if (d->shape2) d->shape2->addDependee(this); d->connectionPointId2 = connectionPointId; return true; } KoShape *KoConnectionShape::firstShape() const { Q_D(const KoConnectionShape); return d->shape1; } int KoConnectionShape::firstConnectionId() const { Q_D(const KoConnectionShape); return d->connectionPointId1; } KoShape *KoConnectionShape::secondShape() const { Q_D(const KoConnectionShape); return d->shape2; } int KoConnectionShape::secondConnectionId() const { Q_D(const KoConnectionShape); return d->connectionPointId2; } KoConnectionShape::Type KoConnectionShape::type() const { Q_D(const KoConnectionShape); return d->connectionType; } void KoConnectionShape::setType(Type connectionType) { Q_D(KoConnectionShape); d->connectionType = connectionType; updatePath(size()); } void KoConnectionShape::shapeChanged(ChangeType type, KoShape *shape) { Q_D(KoConnectionShape); KoTosContainer::shapeChanged(type, shape); // check if we are during a forced update const bool updateIsActive = d->forceUpdate; switch (type) { case PositionChanged: case RotationChanged: case ShearChanged: case ScaleChanged: case GenericMatrixChange: case ParameterChanged: if (isParametricShape() && shape == 0) d->forceUpdate = true; break; case Deleted: if (shape != d->shape1 && shape != d->shape2) return; if (shape == d->shape1) connectFirst(0, -1); if (shape == d->shape2) connectSecond(0, -1); break; case ConnectionPointChanged: if (shape == d->shape1 && !shape->hasConnectionPoint(d->connectionPointId1)) { connectFirst(0, -1); } else if ( shape == d->shape2 && !shape->hasConnectionPoint(d->connectionPointId2)){ connectSecond(0, -1); } else { d->forceUpdate = true; } break; case BackgroundChanged: { // connection shape should not have a background QSharedPointer fill = background(); if (fill) { setBackground(QSharedPointer(0)); } return; } default: return; } // the connection was moved while it is connected to some other shapes const bool connectionChanged = !shape && (d->shape1 || d->shape2); // one of the connected shape has moved const bool connectedShapeChanged = shape && (shape == d->shape1 || shape == d->shape2); if (!updateIsActive && (connectionChanged || connectedShapeChanged) && isParametricShape()) updateConnections(); // reset the forced update flag d->forceUpdate = false; } QString KoConnectionShape::pathShapeId() const { return KOCONNECTIONSHAPEID; } diff --git a/libs/flake/KoGradientBackground.cpp b/libs/flake/KoGradientBackground.cpp index 3f69791c82..0630d68260 100644 --- a/libs/flake/KoGradientBackground.cpp +++ b/libs/flake/KoGradientBackground.cpp @@ -1,191 +1,192 @@ /* This file is part of the KDE project * Copyright (C) 2008 Jan Hambrecht * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "KoGradientBackground.h" #include "KoShapeBackground_p.h" #include "KoFlake.h" #include #include #include #include #include #include #include #include #include +#include class KoGradientBackgroundPrivate : public KoShapeBackgroundPrivate { public: KoGradientBackgroundPrivate() : gradient(0) {} QGradient *gradient; QTransform matrix; }; KoGradientBackground::KoGradientBackground(QGradient * gradient, const QTransform &matrix) : KoShapeBackground(*(new KoGradientBackgroundPrivate())) { Q_D(KoGradientBackground); d->gradient = gradient; d->matrix = matrix; Q_ASSERT(d->gradient); } KoGradientBackground::KoGradientBackground(const QGradient & gradient, const QTransform &matrix) : KoShapeBackground(*(new KoGradientBackgroundPrivate())) { Q_D(KoGradientBackground); d->gradient = KoFlake::cloneGradient(&gradient); d->matrix = matrix; Q_ASSERT(d->gradient); } KoGradientBackground::~KoGradientBackground() { Q_D(KoGradientBackground); delete d->gradient; } bool KoGradientBackground::compareTo(const KoShapeBackground *other) const { Q_D(const KoGradientBackground); const KoGradientBackground *otherGradient = dynamic_cast(other); return otherGradient && d->matrix == otherGradient->d_func()->matrix && *d->gradient == *otherGradient->d_func()->gradient; } void KoGradientBackground::setTransform(const QTransform &matrix) { Q_D(KoGradientBackground); d->matrix = matrix; } QTransform KoGradientBackground::transform() const { Q_D(const KoGradientBackground); return d->matrix; } void KoGradientBackground::setGradient(const QGradient &gradient) { Q_D(KoGradientBackground); delete d->gradient; d->gradient = KoFlake::cloneGradient(&gradient); Q_ASSERT(d->gradient); } const QGradient * KoGradientBackground::gradient() const { Q_D(const KoGradientBackground); return d->gradient; } void KoGradientBackground::paint(QPainter &painter, const KoViewConverter &/*converter*/, KoShapePaintingContext &/*context*/, const QPainterPath &fillPath) const { Q_D(const KoGradientBackground); if (!d->gradient) return; if (d->gradient->coordinateMode() == QGradient::ObjectBoundingMode) { /** * NOTE: important hack! * * Qt has different notation of QBrush::setTransform() in comparison * to what SVG defines. SVG defines gradientToUser matrix to be postmultiplied * by QBrush::transform(), but Qt does exactly reverse! * * That most probably has beed caused by the fact that Qt uses transposed * matrices and someone just mistyped the stuff long ago :( * * So here we basically emulate this feature by converting the gradient into * QGradient::LogicalMode and doing transformations manually. */ const QRectF boundingRect = fillPath.boundingRect(); QTransform gradientToUser(boundingRect.width(), 0, 0, boundingRect.height(), boundingRect.x(), boundingRect.y()); // TODO: how about slicing the object? QGradient g = *d->gradient; g.setCoordinateMode(QGradient::LogicalMode); QBrush b(g); b.setTransform(d->matrix * gradientToUser); painter.setBrush(b); } else { QBrush b(*d->gradient); b.setTransform(d->matrix); painter.setBrush(b); } painter.drawPath(fillPath); } void KoGradientBackground::fillStyle(KoGenStyle &style, KoShapeSavingContext &context) { Q_D(KoGradientBackground); if (!d->gradient) return; QBrush brush(*d->gradient); brush.setTransform(d->matrix); KoOdfGraphicStyles::saveOdfFillStyle(style, context.mainStyles(), brush); } bool KoGradientBackground::loadStyle(KoOdfLoadingContext &context, const QSizeF &shapeSize) { Q_D(KoGradientBackground); KoStyleStack &styleStack = context.styleStack(); if (! styleStack.hasProperty(KoXmlNS::draw, "fill")) return false; QString fillStyle = styleStack.property(KoXmlNS::draw, "fill"); if (fillStyle == "gradient") { QBrush brush = KoOdfGraphicStyles::loadOdfGradientStyle(styleStack, context.stylesReader(), shapeSize); const QGradient * gradient = brush.gradient(); if (gradient) { d->gradient = KoFlake::cloneGradient(gradient); d->matrix = brush.transform(); //Gopalakrishna Bhat: If the brush has transparency then we ignore the draw:opacity property and use the brush transparency. // Brush will have transparency if the svg:linearGradient stop point has stop-opacity property otherwise it is opaque if (brush.isOpaque() && styleStack.hasProperty(KoXmlNS::draw, "opacity")) { QString opacityPercent = styleStack.property(KoXmlNS::draw, "opacity"); if (! opacityPercent.isEmpty() && opacityPercent.right(1) == "%") { float opacity = qMin(opacityPercent.left(opacityPercent.length() - 1).toDouble(), 100.0) / 100; QGradientStops stops; Q_FOREACH (QGradientStop stop, d->gradient->stops()) { stop.second.setAlphaF(opacity); stops << stop; } d->gradient->setStops(stops); } } return true; } } return false; } diff --git a/libs/flake/KoHatchBackground.cpp b/libs/flake/KoHatchBackground.cpp index f556f127b5..23a0b48a98 100644 --- a/libs/flake/KoHatchBackground.cpp +++ b/libs/flake/KoHatchBackground.cpp @@ -1,235 +1,236 @@ /* This file is part of the KDE project * * Copyright (C) 2012 Thorsten Zachmann * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "KoHatchBackground.h" #include "KoColorBackground_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include +#include class KoHatchBackgroundPrivate : public KoColorBackgroundPrivate { public: KoHatchBackgroundPrivate() : angle(0.0) , distance(1.0) , style(KoHatchBackground::Single) {} QColor lineColor; int angle; qreal distance; KoHatchBackground::HatchStyle style; QString name; }; KoHatchBackground::KoHatchBackground() : KoColorBackground(*(new KoHatchBackgroundPrivate())) { } void KoHatchBackground::paint(QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &context, const QPainterPath &fillPath) const { Q_D(const KoHatchBackground); if (d->color.isValid()) { // paint background color if set by using the color background KoColorBackground::paint(painter, converter, context, fillPath); } const QRectF targetRect = fillPath.boundingRect(); painter.save(); painter.setClipPath(fillPath); QPen pen(d->lineColor); // we set the pen width to 0.5 pt for the hatch. This is not defined in the spec. pen.setWidthF(0.5); painter.setPen(pen); QVector lines; // The different styles are handled by painting the lines multiple times with a different // angel offset as basically it just means we paint the lines also at a different angle. // This are the angle offsets we need to apply to the different lines of a style. // -90 is for single, 0 for the 2nd line in double and -45 for the 3th line in triple. const int angleOffset[] = {-90, 0, -45 }; // The number of loops is defined by the style. int loops = (d->style == Single) ? 1 : (d->style == Double) ? 2 : 3; for (int i = 0; i < loops; ++i) { int angle = d->angle - angleOffset[i]; qreal cosAngle = ::cos(angle/180.0*M_PI); // if cos is nearly 0 the lines are horizontal. Use a special case for that if (qAbs(cosAngle) > 0.00001) { qreal xDiff = tan(angle/180.0*M_PI) * targetRect.height(); // calculate the distance we need to increase x when creating the lines so that the // distance between the lines is also correct for rotated lines. qreal xOffset = qAbs(d->distance / cosAngle); // if the lines go to the right we need to start more to the left. Get the correct start. qreal xStart = 0; while (-xDiff < xStart) { xStart -= xOffset; } // if the lines go to the left we need to stop more at the right. Get the correct end offset qreal xEndOffset = 0; if (xDiff < 0) { while (xDiff < -xEndOffset) { xEndOffset += xOffset; } } // create line objects. lines.reserve(lines.size() + int((targetRect.width() + xEndOffset - xStart) / xOffset) + 1); for (qreal x = xStart; x < targetRect.width() + xEndOffset; x += xOffset) { lines.append(QLineF(x, 0, x + xDiff, targetRect.height())); } } else { // horizontal lines lines.reserve(lines.size() + int(targetRect.height()/d->distance) + 1); for (qreal y = 0; y < targetRect.height(); y += d->distance) { lines.append(QLineF(0, y, targetRect.width(), y)); } } } painter.drawLines(lines); painter.restore(); } void KoHatchBackground::fillStyle(KoGenStyle &style, KoShapeSavingContext &context) { Q_D(KoHatchBackground); KoGenStyle::Type type = style.type(); KoGenStyle::PropertyType propertyType = (type == KoGenStyle::GraphicStyle || type == KoGenStyle::GraphicAutoStyle || type == KoGenStyle::DrawingPageStyle || type == KoGenStyle::DrawingPageAutoStyle ) ? KoGenStyle::DefaultType : KoGenStyle::GraphicType; style.addProperty("draw:fill", "hatch", propertyType); style.addProperty("draw:fill-hatch-name", saveHatchStyle(context), propertyType); bool fillHatchSolid = d->color.isValid(); style.addProperty("draw:fill-hatch-solid", fillHatchSolid, propertyType); if (fillHatchSolid) { style.addProperty("draw:fill-color", d->color.name(), propertyType); } } QString KoHatchBackground::saveHatchStyle(KoShapeSavingContext &context) const { Q_D(const KoHatchBackground); KoGenStyle hatchStyle(KoGenStyle::HatchStyle /*no family name*/); hatchStyle.addAttribute("draw:display-name", d->name); hatchStyle.addAttribute("draw:color", d->lineColor.name()); hatchStyle.addAttribute("draw:distance", d->distance); hatchStyle.addAttribute("draw:rotation", QString("%1").arg(d->angle * 10)); switch (d->style) { case Single: hatchStyle.addAttribute("draw:style", "single"); break; case Double: hatchStyle.addAttribute("draw:style", "double"); break; case Triple: hatchStyle.addAttribute("draw:style", "triple"); break; } return context.mainStyles().insert(hatchStyle, "hatch"); } bool KoHatchBackground::loadStyle(KoOdfLoadingContext &context, const QSizeF &shapeSize) { // Q_D(KoHatchBackground); Q_UNUSED(shapeSize); KoStyleStack &styleStack = context.styleStack(); QString fillStyle = styleStack.property(KoXmlNS::draw, "fill"); if (fillStyle == "hatch") { QString style = styleStack.property(KoXmlNS::draw, "fill-hatch-name"); debugFlake << " hatch style is :" << style; KoXmlElement* draw = context.stylesReader().drawStyles("hatch")[style]; if (draw) { debugFlake << "Hatch style found for:" << style; QString angle = draw->attributeNS(KoXmlNS::draw, "rotation", QString("0")); if (angle.at(angle.size()-1).isLetter()) { d->angle = KoUnit::parseAngle(angle); } else { // OO saves the angle value without unit and multiplied by a factor of 10 d->angle = int(angle.toInt() / 10); } debugFlake << "angle :" << d->angle; d->name = draw->attributeNS(KoXmlNS::draw, "display-name"); // use 2mm as default, just in case it is not given in a document so we show something sensible. d->distance = KoUnit::parseValue(draw->attributeNS(KoXmlNS::draw, "distance", "2mm")); bool fillHatchSolid = styleStack.property(KoXmlNS::draw, "fill-hatch-solid") == QLatin1String("true"); if (fillHatchSolid) { QString fillColor = styleStack.property(KoXmlNS::draw, "fill-color"); if (!fillColor.isEmpty()) { d->color.setNamedColor(fillColor); } else { d->color =QColor(); } } else { d->color = QColor(); } d->lineColor.setNamedColor(draw->attributeNS(KoXmlNS::draw, "color", QString("#000000"))); QString style = draw->attributeNS(KoXmlNS::draw, "style", QString()); if (style == "double") { d->style = Double; } else if (style == "triple") { d->style = Triple; } else { d->style = Single; } } return true; } return false; } diff --git a/libs/flake/KoOdfGradientBackground.cpp b/libs/flake/KoOdfGradientBackground.cpp index 20f7aa727d..3a90a8a98a 100644 --- a/libs/flake/KoOdfGradientBackground.cpp +++ b/libs/flake/KoOdfGradientBackground.cpp @@ -1,386 +1,387 @@ /* This file is part of the KDE project * * Copyright (C) 2011 Lukáš Tvrdý * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "KoOdfGradientBackground.h" #include "KoShapeBackground_p.h" #include "KoShapeSavingContext.h" #include #include #include #include #include #include #include #include +#include #include #include #include #include "FlakeDebug.h" class KoOdfGradientBackgroundPrivate : public KoShapeBackgroundPrivate { public: KoOdfGradientBackgroundPrivate() : style() , cx(0) , cy(0) , startColor() , endColor() , angle(0) , border(0) , opacity(1.0) {} ~KoOdfGradientBackgroundPrivate() override{}; //data QString style; int cx; int cy; QColor startColor; QColor endColor; qreal angle; qreal border; qreal opacity; }; KoOdfGradientBackground::KoOdfGradientBackground() : KoShapeBackground(*(new KoOdfGradientBackgroundPrivate())) { } KoOdfGradientBackground::~KoOdfGradientBackground() { } bool KoOdfGradientBackground::compareTo(const KoShapeBackground *other) const { Q_UNUSED(other); return false; } bool KoOdfGradientBackground::loadOdf(const KoXmlElement& e) { Q_D(KoOdfGradientBackground); d->style = e.attributeNS(KoXmlNS::draw, "style", QString()); //TODO: support ellipsoid here too if ((d->style != "rectangular") && (d->style != "square")) { return false; } d->cx = KoUnit::parseValue(e.attributeNS(KoXmlNS::draw, "cx", QString()).remove('%')); d->cy = KoUnit::parseValue(e.attributeNS(KoXmlNS::draw, "cy", QString()).remove('%')); d->border = qBound(0.0,0.01 * e.attributeNS(KoXmlNS::draw, "border", "0").remove('%').toDouble(),1.0); d->startColor = QColor(e.attributeNS(KoXmlNS::draw, "start-color", QString())); d->startColor.setAlphaF((0.01 * e.attributeNS(KoXmlNS::draw, "start-intensity", "100").remove('%').toDouble())); d->endColor = QColor(e.attributeNS(KoXmlNS::draw, "end-color", QString())); d->endColor.setAlphaF(0.01 * e.attributeNS(KoXmlNS::draw, "end-intensity", "100").remove('%').toDouble()); d->angle = e.attributeNS(KoXmlNS::draw, "angle", "0").toDouble() / 10; return true; } void KoOdfGradientBackground::saveOdf(KoGenStyle& styleFill, KoGenStyles& mainStyles) const { Q_D(const KoOdfGradientBackground); KoGenStyle::Type type = styleFill.type(); KoGenStyle::PropertyType propertyType = (type == KoGenStyle::GraphicStyle || type == KoGenStyle::GraphicAutoStyle || type == KoGenStyle::DrawingPageStyle || type == KoGenStyle::DrawingPageAutoStyle ) ? KoGenStyle::DefaultType : KoGenStyle::GraphicType; KoGenStyle gradientStyle(KoGenStyle::GradientStyle); gradientStyle.addAttribute("draw:style", d->style); // draw:style="square" gradientStyle.addAttribute("draw:cx", QString("%1%").arg(d->cx)); gradientStyle.addAttribute("draw:cy", QString("%1%").arg(d->cy)); gradientStyle.addAttribute("draw:start-color", d->startColor.name()); gradientStyle.addAttribute("draw:end-color", d->endColor.name()); gradientStyle.addAttribute("draw:start-intensity", QString("%1%").arg(qRound(d->startColor.alphaF() * 100)) ); gradientStyle.addAttribute("draw:end-intensity", QString("%1%").arg(qRound(d->endColor.alphaF() * 100)) ); gradientStyle.addAttribute("draw:angle", QString("%1").arg(d->angle * 10)); gradientStyle.addAttribute("draw:border", QString("%1%").arg(qRound(d->border * 100.0))); QString gradientStyleName = mainStyles.insert(gradientStyle, "gradient"); styleFill.addProperty("draw:fill", "gradient", propertyType); styleFill.addProperty("draw:fill-gradient-name", gradientStyleName, propertyType); if (d->opacity <= 1.0) { styleFill.addProperty("draw:opacity", QString("%1%").arg(d->opacity * 100.0), propertyType); } } void KoOdfGradientBackground::paint(QPainter& painter, const KoViewConverter &/*converter*/, KoShapePaintingContext &/*context*/, const QPainterPath& fillPath) const { Q_D(const KoOdfGradientBackground); QImage buffer; QRectF targetRect = fillPath.boundingRect(); QRectF pixels = painter.transform().mapRect(QRectF(0,0,targetRect.width(), targetRect.height())); QSize currentSize( qCeil(pixels.size().width()), qCeil(pixels.size().height()) ); if (buffer.isNull() || buffer.size() != currentSize){ buffer = QImage(currentSize, QImage::Format_ARGB32_Premultiplied); if (d->style == "square") { renderSquareGradient(buffer); } else { renderRectangleGradient(buffer); } } painter.setClipPath(fillPath); painter.setOpacity(d->opacity); painter.drawImage(targetRect, buffer, QRectF(QPointF(0,0), buffer.size())); } void KoOdfGradientBackground::fillStyle(KoGenStyle& style, KoShapeSavingContext& context) { saveOdf(style, context.mainStyles()); } bool KoOdfGradientBackground::loadStyle(KoOdfLoadingContext& context, const QSizeF& shapeSize) { Q_UNUSED(shapeSize); Q_D(KoOdfGradientBackground); KoStyleStack &styleStack = context.styleStack(); if (!styleStack.hasProperty(KoXmlNS::draw, "fill")) { return false; } QString fillStyle = styleStack.property(KoXmlNS::draw, "fill"); if (fillStyle == "gradient") { if (styleStack.hasProperty(KoXmlNS::draw, "opacity")) { QString opacity = styleStack.property(KoXmlNS::draw, "opacity"); if (! opacity.isEmpty() && opacity.right(1) == "%") { d->opacity = qMin(opacity.left(opacity.length() - 1).toDouble(), 100.0) / 100; } } QString styleName = styleStack.property(KoXmlNS::draw, "fill-gradient-name"); KoXmlElement * e = context.stylesReader().drawStyles("gradient")[styleName]; return loadOdf(*e); } return false; } void KoOdfGradientBackground::renderSquareGradient(QImage& buffer) const { Q_D(const KoOdfGradientBackground); buffer.fill(d->startColor.rgba()); QPainter painter(&buffer); painter.setPen(Qt::NoPen); painter.setRenderHint(QPainter::Antialiasing, false); int width = buffer.width(); int height = buffer.height(); qreal gradientCenterX = qRound(width * d->cx * 0.01); qreal gradientCenterY = qRound(height * d->cy * 0.01); qreal centerX = width * 0.5; qreal centerY = height * 0.5; qreal areaCenterX = qRound(centerX); qreal areaCenterY = qRound(centerY); QTransform m; m.translate(gradientCenterX, gradientCenterY); m.rotate(-d->angle); m.scale(1.0 - d->border, 1.0 - d->border); m.translate(-gradientCenterX, -gradientCenterY); m.translate(gradientCenterX - areaCenterX,gradientCenterY - areaCenterY); painter.setTransform(m); QLinearGradient linearGradient; linearGradient.setColorAt(1, d->startColor); linearGradient.setColorAt(0, d->endColor); // from center going North linearGradient.setStart(centerX, centerY); linearGradient.setFinalStop(centerX, 0); painter.setBrush(linearGradient); painter.drawRect(0, 0, width, centerY); // from center going South linearGradient.setFinalStop(centerX, height); painter.setBrush(linearGradient); painter.drawRect(0, centerY, width, centerY); // clip the East and West portion QPainterPath clip; clip.moveTo(width, 0); clip.lineTo(width, height); clip.lineTo(0, 0); clip.lineTo(0, height); clip.closeSubpath(); painter.setClipPath(clip); // from center going East linearGradient.setFinalStop(width, centerY); painter.setBrush(linearGradient); painter.drawRect(centerX, 0, width, height); // from center going West linearGradient.setFinalStop( 0, centerY); painter.setBrush(linearGradient); painter.drawRect(0, 0, centerX, height); } void KoOdfGradientBackground::renderRectangleGradient(QImage& buffer) const { Q_D(const KoOdfGradientBackground); buffer.fill(d->startColor.rgba()); QPainter painter(&buffer); painter.setPen(Qt::NoPen); painter.setRenderHint(QPainter::Antialiasing, false); int width = buffer.width(); int height = buffer.height(); qreal gradientCenterX = qRound(width * d->cx * 0.01); qreal gradientCenterY = qRound(height * d->cy * 0.01); qreal centerX = width * 0.5; qreal centerY = height * 0.5; qreal areaCenterY = qRound(centerY); qreal areaCenterX = qRound(centerX); QTransform m; m.translate(gradientCenterX, gradientCenterY); // m.rotate(-d->angle); // OOo rotates the gradient differently m.scale(1.0 - d->border, 1.0 - d->border); m.translate(-gradientCenterX, -gradientCenterY); m.translate(gradientCenterX - areaCenterX,gradientCenterY - areaCenterY); painter.setTransform(m); QLinearGradient linearGradient; linearGradient.setColorAt(1, d->startColor); linearGradient.setColorAt(0, d->endColor); // render background QPainterPath clipPath; if (width < height) { QRectF west(0,0,centerX, height); QRectF east(centerX, 0, centerX, height); linearGradient.setStart(centerX, centerY); linearGradient.setFinalStop(0, centerY); painter.setBrush(linearGradient); painter.drawRect(west); linearGradient.setFinalStop(width, centerY); painter.setBrush(linearGradient); painter.drawRect(east); QRectF north(0,0,width, centerX); QRectF south(0,height - centerX, width, centerX); clipPath.moveTo(0,0); clipPath.lineTo(width, 0); clipPath.lineTo(centerX, centerX); clipPath.closeSubpath(); clipPath.moveTo(width, height); clipPath.lineTo(0, height); clipPath.lineTo(centerX, south.y()); clipPath.closeSubpath(); linearGradient.setStart(centerX, centerX); linearGradient.setFinalStop(centerX, 0); painter.setClipPath(clipPath); painter.setBrush(linearGradient); painter.drawRect(north); linearGradient.setStart(centerX, south.y()); linearGradient.setFinalStop(centerX, height); painter.setBrush(linearGradient); painter.drawRect(south); } else { QRectF north(0,0,width, centerY); QRectF south(0, centerY, width, centerY); linearGradient.setStart(centerX, centerY); linearGradient.setFinalStop(centerX, 0); painter.setBrush(linearGradient); painter.drawRect(north); linearGradient.setFinalStop(centerX, height); painter.setBrush(linearGradient); painter.drawRect(south); QRectF west(0,0,centerY, height); QRectF east(width - centerY, 0, centerY, height); clipPath.moveTo(0,0); clipPath.lineTo(centerY, centerY); clipPath.lineTo(0,height); clipPath.closeSubpath(); clipPath.moveTo(width, height); clipPath.lineTo(east.x(), centerY); clipPath.lineTo(width,0); clipPath.closeSubpath(); linearGradient.setStart(centerY, centerY); linearGradient.setFinalStop(0, centerY); painter.setClipPath(clipPath); painter.setBrush(linearGradient); painter.drawRect(west); linearGradient.setStart(east.x(), centerY); linearGradient.setFinalStop(width, centerY); painter.setBrush(linearGradient); painter.drawRect(east); } } void KoOdfGradientBackground::debug() const { Q_D(const KoOdfGradientBackground); debugFlake << "cx,cy: "<< d->cx << d->cy; debugFlake << "style" << d->style; debugFlake << "colors" << d->startColor << d->endColor; debugFlake << "angle:" << d->angle; debugFlake << "border" << d->border; } diff --git a/libs/flake/KoPathShape.cpp b/libs/flake/KoPathShape.cpp index 2768a0e92e..9069877fe9 100644 --- a/libs/flake/KoPathShape.cpp +++ b/libs/flake/KoPathShape.cpp @@ -1,1726 +1,1727 @@ /* This file is part of the KDE project Copyright (C) 2006-2008, 2010-2011 Thorsten Zachmann Copyright (C) 2006-2011 Jan Hambrecht Copyright (C) 2007-2009 Thomas Zander Copyright (C) 2011 Jean-Nicolas Artaud This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "KoPathShape.h" #include "KoPathShape_p.h" #include "KoPathSegment.h" #include "KoOdfWorkaround.h" #include "KoPathPoint.h" #include "KoShapeStrokeModel.h" #include "KoViewConverter.h" #include "KoPathShapeLoader.h" #include "KoShapeSavingContext.h" #include "KoShapeLoadingContext.h" #include "KoShapeShadow.h" #include "KoShapeBackground.h" #include "KoShapeContainer.h" #include "KoFilterEffectStack.h" #include "KoMarker.h" #include "KoShapeStroke.h" #include "KoInsets.h" #include #include #include #include #include #include #include #include "KisQPainterStateSaver.h" #include #include +#include #include "kis_global.h" #include // for qIsNaN static bool qIsNaNPoint(const QPointF &p) { return qIsNaN(p.x()) || qIsNaN(p.y()); } KoPathShapePrivate::KoPathShapePrivate(KoPathShape *q) : KoTosContainerPrivate(q), fillRule(Qt::OddEvenFill), autoFillMarkers(false) { } KoPathShapePrivate::KoPathShapePrivate(const KoPathShapePrivate &rhs, KoPathShape *q) : KoTosContainerPrivate(rhs, q), fillRule(rhs.fillRule), markersNew(rhs.markersNew), autoFillMarkers(rhs.autoFillMarkers) { Q_FOREACH (KoSubpath *subPath, rhs.subpaths) { KoSubpath *clonedSubPath = new KoSubpath(); Q_FOREACH (KoPathPoint *point, *subPath) { *clonedSubPath << new KoPathPoint(*point, q); } subpaths << clonedSubPath; } } QRectF KoPathShapePrivate::handleRect(const QPointF &p, qreal radius) const { return QRectF(p.x() - radius, p.y() - radius, 2*radius, 2*radius); } void KoPathShapePrivate::applyViewboxTransformation(const KoXmlElement &element) { // apply viewbox transformation const QRect viewBox = KoPathShape::loadOdfViewbox(element); if (! viewBox.isEmpty()) { // load the desired size QSizeF size; size.setWidth(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "width", QString()))); size.setHeight(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "height", QString()))); // load the desired position QPointF pos; pos.setX(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "x", QString()))); pos.setY(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "y", QString()))); // create matrix to transform original path data into desired size and position QTransform viewMatrix; viewMatrix.translate(-viewBox.left(), -viewBox.top()); viewMatrix.scale(size.width() / viewBox.width(), size.height() / viewBox.height()); viewMatrix.translate(pos.x(), pos.y()); // transform the path data map(viewMatrix); } } KoPathShape::KoPathShape() :KoTosContainer(new KoPathShapePrivate(this)) { } KoPathShape::KoPathShape(KoPathShapePrivate *dd) : KoTosContainer(dd) { } KoPathShape::KoPathShape(const KoPathShape &rhs) : KoTosContainer(new KoPathShapePrivate(*rhs.d_func(), this)) { } KoPathShape::~KoPathShape() { clear(); } KoShape *KoPathShape::cloneShape() const { return new KoPathShape(*this); } void KoPathShape::saveContourOdf(KoShapeSavingContext &context, const QSizeF &scaleFactor) const { Q_D(const KoPathShape); if (d->subpaths.length() <= 1) { QTransform matrix; matrix.scale(scaleFactor.width(), scaleFactor.height()); QString points; KoSubpath *subPath = d->subpaths.first(); KoSubpath::const_iterator pointIt(subPath->constBegin()); KoPathPoint *currPoint= 0; // iterate over all points for (; pointIt != subPath->constEnd(); ++pointIt) { currPoint = *pointIt; if (currPoint->activeControlPoint1() || currPoint->activeControlPoint2()) { break; } const QPointF p = matrix.map(currPoint->point()); points += QString("%1,%2 ").arg(qRound(1000*p.x())).arg(qRound(1000*p.y())); } if (currPoint && !(currPoint->activeControlPoint1() || currPoint->activeControlPoint2())) { context.xmlWriter().startElement("draw:contour-polygon"); context.xmlWriter().addAttribute("svg:width", size().width()); context.xmlWriter().addAttribute("svg:height", size().height()); const QSizeF s(size()); QString viewBox = QString("0 0 %1 %2").arg(qRound(1000*s.width())).arg(qRound(1000*s.height())); context.xmlWriter().addAttribute("svg:viewBox", viewBox); context.xmlWriter().addAttribute("draw:points", points); context.xmlWriter().addAttribute("draw:recreate-on-edit", "true"); context.xmlWriter().endElement(); return; } } // if we get here we couldn't save as polygon - let-s try contour-path context.xmlWriter().startElement("draw:contour-path"); saveOdfAttributes(context, OdfViewbox); context.xmlWriter().addAttribute("svg:d", toString()); context.xmlWriter().addAttribute("calligra:nodeTypes", d->nodeTypes()); context.xmlWriter().addAttribute("draw:recreate-on-edit", "true"); context.xmlWriter().endElement(); } void KoPathShape::saveOdf(KoShapeSavingContext & context) const { Q_D(const KoPathShape); context.xmlWriter().startElement("draw:path"); saveOdfAttributes(context, OdfAllAttributes | OdfViewbox); context.xmlWriter().addAttribute("svg:d", toString()); context.xmlWriter().addAttribute("calligra:nodeTypes", d->nodeTypes()); saveOdfCommonChildElements(context); saveText(context); context.xmlWriter().endElement(); } bool KoPathShape::loadContourOdf(const KoXmlElement &element, KoShapeLoadingContext &, const QSizeF &scaleFactor) { Q_D(KoPathShape); // first clear the path data from the default path clear(); if (element.localName() == "contour-polygon") { QString points = element.attributeNS(KoXmlNS::draw, "points").simplified(); points.replace(',', ' '); points.remove('\r'); points.remove('\n'); bool firstPoint = true; const QStringList coordinateList = points.split(' '); for (QStringList::ConstIterator it = coordinateList.constBegin(); it != coordinateList.constEnd(); ++it) { QPointF point; point.setX((*it).toDouble()); ++it; point.setY((*it).toDouble()); if (firstPoint) { moveTo(point); firstPoint = false; } else lineTo(point); } close(); } else if (element.localName() == "contour-path") { KoPathShapeLoader loader(this); loader.parseSvg(element.attributeNS(KoXmlNS::svg, "d"), true); d->loadNodeTypes(element); } // apply viewbox transformation const QRect viewBox = KoPathShape::loadOdfViewbox(element); if (! viewBox.isEmpty()) { QSizeF size; size.setWidth(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "width", QString()))); size.setHeight(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "height", QString()))); // create matrix to transform original path data into desired size and position QTransform viewMatrix; viewMatrix.translate(-viewBox.left(), -viewBox.top()); viewMatrix.scale(scaleFactor.width(), scaleFactor.height()); viewMatrix.scale(size.width() / viewBox.width(), size.height() / viewBox.height()); // transform the path data d->map(viewMatrix); } setTransformation(QTransform()); return true; } bool KoPathShape::loadOdf(const KoXmlElement & element, KoShapeLoadingContext &context) { Q_D(KoPathShape); loadOdfAttributes(element, context, OdfMandatories | OdfAdditionalAttributes | OdfCommonChildElements); // first clear the path data from the default path clear(); if (element.localName() == "line") { QPointF start; start.setX(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "x1", ""))); start.setY(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "y1", ""))); QPointF end; end.setX(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "x2", ""))); end.setY(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "y2", ""))); moveTo(start); lineTo(end); } else if (element.localName() == "polyline" || element.localName() == "polygon") { QString points = element.attributeNS(KoXmlNS::draw, "points").simplified(); points.replace(',', ' '); points.remove('\r'); points.remove('\n'); bool firstPoint = true; const QStringList coordinateList = points.split(' '); for (QStringList::ConstIterator it = coordinateList.constBegin(); it != coordinateList.constEnd(); ++it) { QPointF point; point.setX((*it).toDouble()); ++it; point.setY((*it).toDouble()); if (firstPoint) { moveTo(point); firstPoint = false; } else lineTo(point); } if (element.localName() == "polygon") close(); } else { // path loading KoPathShapeLoader loader(this); loader.parseSvg(element.attributeNS(KoXmlNS::svg, "d"), true); d->loadNodeTypes(element); } d->applyViewboxTransformation(element); QPointF pos = normalize(); setTransformation(QTransform()); if (element.hasAttributeNS(KoXmlNS::svg, "x") || element.hasAttributeNS(KoXmlNS::svg, "y")) { pos.setX(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "x", QString()))); pos.setY(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "y", QString()))); } setPosition(pos); loadOdfAttributes(element, context, OdfTransformation); // now that the correct transformation is set up // apply that matrix to the path geometry so that // we don't transform the stroke d->map(transformation()); setTransformation(QTransform()); normalize(); loadText(element, context); return true; } QString KoPathShape::saveStyle(KoGenStyle &style, KoShapeSavingContext &context) const { Q_D(const KoPathShape); style.addProperty("svg:fill-rule", d->fillRule == Qt::OddEvenFill ? "evenodd" : "nonzero"); QSharedPointer lineBorder = qSharedPointerDynamicCast(stroke()); qreal lineWidth = 0; if (lineBorder) { lineWidth = lineBorder->lineWidth(); } Q_UNUSED(lineWidth) return KoTosContainer::saveStyle(style, context); } void KoPathShape::loadStyle(const KoXmlElement & element, KoShapeLoadingContext &context) { Q_D(KoPathShape); KoTosContainer::loadStyle(element, context); KoStyleStack &styleStack = context.odfLoadingContext().styleStack(); styleStack.setTypeProperties("graphic"); if (styleStack.hasProperty(KoXmlNS::svg, "fill-rule")) { QString rule = styleStack.property(KoXmlNS::svg, "fill-rule"); d->fillRule = (rule == "nonzero") ? Qt::WindingFill : Qt::OddEvenFill; } else { d->fillRule = Qt::WindingFill; #ifndef NWORKAROUND_ODF_BUGS KoOdfWorkaround::fixMissingFillRule(d->fillRule, context); #endif } QSharedPointer lineBorder = qSharedPointerDynamicCast(stroke()); qreal lineWidth = 0; if (lineBorder) { lineWidth = lineBorder->lineWidth(); } Q_UNUSED(lineWidth); } QRect KoPathShape::loadOdfViewbox(const KoXmlElement & element) { QRect viewbox; QString data = element.attributeNS(KoXmlNS::svg, QLatin1String("viewBox")); if (! data.isEmpty()) { data.replace(QLatin1Char(','), QLatin1Char(' ')); const QStringList coordinates = data.simplified().split(QLatin1Char(' '), QString::SkipEmptyParts); if (coordinates.count() == 4) { viewbox.setRect(coordinates.at(0).toInt(), coordinates.at(1).toInt(), coordinates.at(2).toInt(), coordinates.at(3).toInt()); } } return viewbox; } void KoPathShape::clear() { Q_D(KoPathShape); Q_FOREACH (KoSubpath *subpath, d->subpaths) { Q_FOREACH (KoPathPoint *point, *subpath) delete point; delete subpath; } d->subpaths.clear(); notifyPointsChanged(); } void KoPathShape::paint(QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintContext) { Q_D(KoPathShape); KisQPainterStateSaver saver(&painter); applyConversion(painter, converter); QPainterPath path(outline()); path.setFillRule(d->fillRule); if (background()) { background()->paint(painter, converter, paintContext, path); } //d->paintDebug(painter); } #ifndef NDEBUG void KoPathShapePrivate::paintDebug(QPainter &painter) { Q_Q(KoPathShape); KoSubpathList::const_iterator pathIt(subpaths.constBegin()); int i = 0; QPen pen(Qt::black, 0); painter.save(); painter.setPen(pen); for (; pathIt != subpaths.constEnd(); ++pathIt) { KoSubpath::const_iterator it((*pathIt)->constBegin()); for (; it != (*pathIt)->constEnd(); ++it) { ++i; KoPathPoint *point = (*it); QRectF r(point->point(), QSizeF(5, 5)); r.translate(-2.5, -2.5); QPen pen(Qt::black, 0); painter.setPen(pen); if (point->activeControlPoint1() && point->activeControlPoint2()) { QBrush b(Qt::red); painter.setBrush(b); } else if (point->activeControlPoint1()) { QBrush b(Qt::yellow); painter.setBrush(b); } else if (point->activeControlPoint2()) { QBrush b(Qt::darkYellow); painter.setBrush(b); } painter.drawEllipse(r); } } painter.restore(); debugFlake << "nop =" << i; } void KoPathShapePrivate::debugPath() const { Q_Q(const KoPathShape); KoSubpathList::const_iterator pathIt(subpaths.constBegin()); for (; pathIt != subpaths.constEnd(); ++pathIt) { KoSubpath::const_iterator it((*pathIt)->constBegin()); for (; it != (*pathIt)->constEnd(); ++it) { debugFlake << "p:" << (*pathIt) << "," << *it << "," << (*it)->point() << "," << (*it)->properties(); } } } #endif void KoPathShape::paintPoints(KisHandlePainterHelper &handlesHelper) { Q_D(KoPathShape); KoSubpathList::const_iterator pathIt(d->subpaths.constBegin()); for (; pathIt != d->subpaths.constEnd(); ++pathIt) { KoSubpath::const_iterator it((*pathIt)->constBegin()); for (; it != (*pathIt)->constEnd(); ++it) (*it)->paint(handlesHelper, KoPathPoint::Node); } } QRectF KoPathShape::outlineRect() const { return outline().boundingRect(); } QPainterPath KoPathShape::outline() const { Q_D(const KoPathShape); QPainterPath path; for (auto subpathIt = d->subpaths.constBegin(); subpathIt != d->subpaths.constEnd(); ++subpathIt) { const KoSubpath * subpath = *subpathIt; const KoPathPoint * lastPoint = subpath->constFirst(); bool activeCP = false; for (auto pointIt = subpath->constBegin(); pointIt != subpath->constEnd(); ++pointIt) { const KoPathPoint * currPoint = *pointIt; KoPathPoint::PointProperties currProperties = currPoint->properties(); if (currPoint == subpath->constFirst()) { if (currProperties & KoPathPoint::StartSubpath) { Q_ASSERT(!qIsNaNPoint(currPoint->point())); path.moveTo(currPoint->point()); } } else if (activeCP && currPoint->activeControlPoint1()) { Q_ASSERT(!qIsNaNPoint(lastPoint->controlPoint2())); Q_ASSERT(!qIsNaNPoint(currPoint->controlPoint1())); Q_ASSERT(!qIsNaNPoint(currPoint->point())); path.cubicTo( lastPoint->controlPoint2(), currPoint->controlPoint1(), currPoint->point()); } else if (activeCP || currPoint->activeControlPoint1()) { Q_ASSERT(!qIsNaNPoint(lastPoint->controlPoint2())); Q_ASSERT(!qIsNaNPoint(currPoint->controlPoint1())); path.quadTo( activeCP ? lastPoint->controlPoint2() : currPoint->controlPoint1(), currPoint->point()); } else { Q_ASSERT(!qIsNaNPoint(currPoint->point())); path.lineTo(currPoint->point()); } if (currProperties & KoPathPoint::CloseSubpath && currProperties & KoPathPoint::StopSubpath) { // add curve when there is a curve on the way to the first point KoPathPoint * firstPoint = subpath->first(); Q_ASSERT(!qIsNaNPoint(firstPoint->point())); if (currPoint->activeControlPoint2() && firstPoint->activeControlPoint1()) { path.cubicTo( currPoint->controlPoint2(), firstPoint->controlPoint1(), firstPoint->point()); } else if (currPoint->activeControlPoint2() || firstPoint->activeControlPoint1()) { Q_ASSERT(!qIsNaNPoint(currPoint->point())); Q_ASSERT(!qIsNaNPoint(currPoint->controlPoint1())); path.quadTo( currPoint->activeControlPoint2() ? currPoint->controlPoint2() : firstPoint->controlPoint1(), firstPoint->point()); } path.closeSubpath(); } if (currPoint->activeControlPoint2()) { activeCP = true; } else { activeCP = false; } lastPoint = currPoint; } } return path; } QRectF KoPathShape::boundingRect() const { const QTransform transform = absoluteTransformation(0); /** * First we approximate the insets of the stroke by rendering a fat bezier curve * with width set to the maximum inset of miters and markers. The are swept by this * curve will be a good approximation of the real curve bounding rect. */ qreal outlineSweepWidth = 0; const QSharedPointer lineBorder = qSharedPointerDynamicCast(stroke()); if (lineBorder) { outlineSweepWidth = lineBorder->lineWidth(); } if (stroke()) { KoInsets inset; stroke()->strokeInsets(this, inset); const qreal maxInset = std::max({inset.left, inset.top, inset.right, inset.bottom}); // insets extend outside the shape, but width extends both inside and outside, // so we should multiply insets by 2.0 outlineSweepWidth = std::max({outlineSweepWidth, 2.0 * maxInset, 2.0 * stroke()->strokeMaxMarkersInset(this)}); } QPen pen(Qt::black, outlineSweepWidth); // select round joins and caps to ensure it sweeps exactly // 'outlineSweepWidth' pixels in every possible pen.setJoinStyle(Qt::RoundJoin); pen.setCapStyle(Qt::RoundCap); QRectF bb = transform.map(pathStroke(pen)).boundingRect(); if (shadow()) { KoInsets insets; shadow()->insets(insets); bb.adjust(-insets.left, -insets.top, insets.right, insets.bottom); } if (filterEffectStack()) { QRectF clipRect = filterEffectStack()->clipRectForBoundingRect(QRectF(QPointF(), size())); bb |= transform.mapRect(clipRect); } return bb; } QSizeF KoPathShape::size() const { // don't call boundingRect here as it uses absoluteTransformation // which itself uses size() -> leads to infinite recursion return outlineRect().size(); } void KoPathShape::setSize(const QSizeF &newSize) { Q_D(KoPathShape); QTransform matrix(resizeMatrix(newSize)); KoShape::setSize(newSize); d->map(matrix); } QTransform KoPathShape::resizeMatrix(const QSizeF & newSize) const { QSizeF oldSize = size(); if (oldSize.width() == 0.0) { oldSize.setWidth(0.000001); } if (oldSize.height() == 0.0) { oldSize.setHeight(0.000001); } QSizeF sizeNew(newSize); if (sizeNew.width() == 0.0) { sizeNew.setWidth(0.000001); } if (sizeNew.height() == 0.0) { sizeNew.setHeight(0.000001); } return QTransform(sizeNew.width() / oldSize.width(), 0, 0, sizeNew.height() / oldSize.height(), 0, 0); } KoPathPoint * KoPathShape::moveTo(const QPointF &p) { Q_D(KoPathShape); KoPathPoint * point = new KoPathPoint(this, p, KoPathPoint::StartSubpath | KoPathPoint::StopSubpath); KoSubpath * path = new KoSubpath; path->push_back(point); d->subpaths.push_back(path); notifyPointsChanged(); return point; } KoPathPoint * KoPathShape::lineTo(const QPointF &p) { Q_D(KoPathShape); if (d->subpaths.empty()) { moveTo(QPointF(0, 0)); } KoPathPoint * point = new KoPathPoint(this, p, KoPathPoint::StopSubpath); KoPathPoint * lastPoint = d->subpaths.last()->last(); d->updateLast(&lastPoint); d->subpaths.last()->push_back(point); notifyPointsChanged(); return point; } KoPathPoint * KoPathShape::curveTo(const QPointF &c1, const QPointF &c2, const QPointF &p) { Q_D(KoPathShape); if (d->subpaths.empty()) { moveTo(QPointF(0, 0)); } KoPathPoint * lastPoint = d->subpaths.last()->last(); d->updateLast(&lastPoint); lastPoint->setControlPoint2(c1); KoPathPoint * point = new KoPathPoint(this, p, KoPathPoint::StopSubpath); point->setControlPoint1(c2); d->subpaths.last()->push_back(point); notifyPointsChanged(); return point; } KoPathPoint * KoPathShape::curveTo(const QPointF &c, const QPointF &p) { Q_D(KoPathShape); if (d->subpaths.empty()) moveTo(QPointF(0, 0)); KoPathPoint * lastPoint = d->subpaths.last()->last(); d->updateLast(&lastPoint); lastPoint->setControlPoint2(c); KoPathPoint * point = new KoPathPoint(this, p, KoPathPoint::StopSubpath); d->subpaths.last()->push_back(point); notifyPointsChanged(); return point; } KoPathPoint * KoPathShape::arcTo(qreal rx, qreal ry, qreal startAngle, qreal sweepAngle) { Q_D(KoPathShape); if (d->subpaths.empty()) { moveTo(QPointF(0, 0)); } KoPathPoint * lastPoint = d->subpaths.last()->last(); if (lastPoint->properties() & KoPathPoint::CloseSubpath) { lastPoint = d->subpaths.last()->first(); } QPointF startpoint(lastPoint->point()); KoPathPoint * newEndPoint = lastPoint; QPointF curvePoints[12]; int pointCnt = arcToCurve(rx, ry, startAngle, sweepAngle, startpoint, curvePoints); for (int i = 0; i < pointCnt; i += 3) { newEndPoint = curveTo(curvePoints[i], curvePoints[i+1], curvePoints[i+2]); } return newEndPoint; } int KoPathShape::arcToCurve(qreal rx, qreal ry, qreal startAngle, qreal sweepAngle, const QPointF & offset, QPointF * curvePoints) const { int pointCnt = 0; // check Parameters if (sweepAngle == 0.0) return pointCnt; sweepAngle = qBound(-360.0, sweepAngle, 360.0); if (rx == 0 || ry == 0) { //TODO } // split angles bigger than 90° so that it gives a good approximation to the circle qreal parts = ceil(qAbs(sweepAngle / 90.0)); qreal sa_rad = startAngle * M_PI / 180.0; qreal partangle = sweepAngle / parts; qreal endangle = startAngle + partangle; qreal se_rad = endangle * M_PI / 180.0; qreal sinsa = sin(sa_rad); qreal cossa = cos(sa_rad); qreal kappa = 4.0 / 3.0 * tan((se_rad - sa_rad) / 4); // startpoint is at the last point is the path but when it is closed // it is at the first point QPointF startpoint(offset); //center berechnen QPointF center(startpoint - QPointF(cossa * rx, -sinsa * ry)); //debugFlake <<"kappa" << kappa <<"parts" << parts; for (int part = 0; part < parts; ++part) { // start tangent curvePoints[pointCnt++] = QPointF(startpoint - QPointF(sinsa * rx * kappa, cossa * ry * kappa)); qreal sinse = sin(se_rad); qreal cosse = cos(se_rad); // end point QPointF endpoint(center + QPointF(cosse * rx, -sinse * ry)); // end tangent curvePoints[pointCnt++] = QPointF(endpoint - QPointF(-sinse * rx * kappa, -cosse * ry * kappa)); curvePoints[pointCnt++] = endpoint; // set the endpoint as next start point startpoint = endpoint; sinsa = sinse; cossa = cosse; endangle += partangle; se_rad = endangle * M_PI / 180.0; } return pointCnt; } void KoPathShape::close() { Q_D(KoPathShape); if (d->subpaths.empty()) { return; } d->closeSubpath(d->subpaths.last()); } void KoPathShape::closeMerge() { Q_D(KoPathShape); if (d->subpaths.empty()) { return; } d->closeMergeSubpath(d->subpaths.last()); } QPointF KoPathShape::normalize() { Q_D(KoPathShape); QPointF tl(outline().boundingRect().topLeft()); QTransform matrix; matrix.translate(-tl.x(), -tl.y()); d->map(matrix); // keep the top left point of the object applyTransformation(matrix.inverted()); d->shapeChanged(ContentChanged); return tl; } void KoPathShapePrivate::map(const QTransform &matrix) { KoSubpathList::const_iterator pathIt(subpaths.constBegin()); for (; pathIt != subpaths.constEnd(); ++pathIt) { KoSubpath::const_iterator it((*pathIt)->constBegin()); for (; it != (*pathIt)->constEnd(); ++it) { // It's possible there are null points in the map... if (*it) { (*it)->map(matrix); } } } } void KoPathShapePrivate::updateLast(KoPathPoint **lastPoint) { Q_Q(KoPathShape); // check if we are about to add a new point to a closed subpath if ((*lastPoint)->properties() & KoPathPoint::StopSubpath && (*lastPoint)->properties() & KoPathPoint::CloseSubpath) { // get the first point of the subpath KoPathPoint *subpathStart = subpaths.last()->first(); // clone the first point of the subpath... KoPathPoint * newLastPoint = new KoPathPoint(*subpathStart, q); // ... and make it a normal point newLastPoint->setProperties(KoPathPoint::Normal); // now start a new subpath with the cloned start point KoSubpath *path = new KoSubpath; path->push_back(newLastPoint); subpaths.push_back(path); *lastPoint = newLastPoint; } else { // the subpath was not closed so the formerly last point // of the subpath is no end point anymore (*lastPoint)->unsetProperty(KoPathPoint::StopSubpath); } (*lastPoint)->unsetProperty(KoPathPoint::CloseSubpath); } QList KoPathShape::pointsAt(const QRectF &r) const { Q_D(const KoPathShape); QList result; KoSubpathList::const_iterator pathIt(d->subpaths.constBegin()); for (; pathIt != d->subpaths.constEnd(); ++pathIt) { KoSubpath::const_iterator it((*pathIt)->constBegin()); for (; it != (*pathIt)->constEnd(); ++it) { if (r.contains((*it)->point())) result.append(*it); else if ((*it)->activeControlPoint1() && r.contains((*it)->controlPoint1())) result.append(*it); else if ((*it)->activeControlPoint2() && r.contains((*it)->controlPoint2())) result.append(*it); } } return result; } QList KoPathShape::segmentsAt(const QRectF &r) const { Q_D(const KoPathShape); QList segments; int subpathCount = d->subpaths.count(); for (int subpathIndex = 0; subpathIndex < subpathCount; ++subpathIndex) { KoSubpath * subpath = d->subpaths[subpathIndex]; int pointCount = subpath->count(); bool subpathClosed = isClosedSubpath(subpathIndex); for (int pointIndex = 0; pointIndex < pointCount; ++pointIndex) { if (pointIndex == (pointCount - 1) && ! subpathClosed) break; KoPathSegment s(subpath->at(pointIndex), subpath->at((pointIndex + 1) % pointCount)); QRectF controlRect = s.controlPointRect(); if (! r.intersects(controlRect) && ! controlRect.contains(r)) continue; QRectF bound = s.boundingRect(); if (! r.intersects(bound) && ! bound.contains(r)) continue; segments.append(s); } } return segments; } KoPathPointIndex KoPathShape::pathPointIndex(const KoPathPoint *point) const { Q_D(const KoPathShape); for (int subpathIndex = 0; subpathIndex < d->subpaths.size(); ++subpathIndex) { KoSubpath * subpath = d->subpaths.at(subpathIndex); for (int pointPos = 0; pointPos < subpath->size(); ++pointPos) { if (subpath->at(pointPos) == point) { return KoPathPointIndex(subpathIndex, pointPos); } } } return KoPathPointIndex(-1, -1); } KoPathPoint * KoPathShape::pointByIndex(const KoPathPointIndex &pointIndex) const { Q_D(const KoPathShape); KoSubpath *subpath = d->subPath(pointIndex.first); if (subpath == 0 || pointIndex.second < 0 || pointIndex.second >= subpath->size()) return 0; return subpath->at(pointIndex.second); } KoPathSegment KoPathShape::segmentByIndex(const KoPathPointIndex &pointIndex) const { Q_D(const KoPathShape); KoPathSegment segment(0, 0); KoSubpath *subpath = d->subPath(pointIndex.first); if (subpath != 0 && pointIndex.second >= 0 && pointIndex.second < subpath->size()) { KoPathPoint * point = subpath->at(pointIndex.second); int index = pointIndex.second; // check if we have a (closing) segment starting from the last point if ((index == subpath->size() - 1) && point->properties() & KoPathPoint::CloseSubpath) index = 0; else ++index; if (index < subpath->size()) { segment = KoPathSegment(point, subpath->at(index)); } } return segment; } int KoPathShape::pointCount() const { Q_D(const KoPathShape); int i = 0; KoSubpathList::const_iterator pathIt(d->subpaths.constBegin()); for (; pathIt != d->subpaths.constEnd(); ++pathIt) { i += (*pathIt)->size(); } return i; } int KoPathShape::subpathCount() const { Q_D(const KoPathShape); return d->subpaths.count(); } int KoPathShape::subpathPointCount(int subpathIndex) const { Q_D(const KoPathShape); KoSubpath *subpath = d->subPath(subpathIndex); if (subpath == 0) return -1; return subpath->size(); } bool KoPathShape::isClosedSubpath(int subpathIndex) const { Q_D(const KoPathShape); KoSubpath *subpath = d->subPath(subpathIndex); if (subpath == 0) return false; const bool firstClosed = subpath->first()->properties() & KoPathPoint::CloseSubpath; const bool lastClosed = subpath->last()->properties() & KoPathPoint::CloseSubpath; return firstClosed && lastClosed; } bool KoPathShape::insertPoint(KoPathPoint* point, const KoPathPointIndex &pointIndex) { Q_D(KoPathShape); KoSubpath *subpath = d->subPath(pointIndex.first); if (subpath == 0 || pointIndex.second < 0 || pointIndex.second > subpath->size()) return false; KoPathPoint::PointProperties properties = point->properties(); properties &= ~KoPathPoint::StartSubpath; properties &= ~KoPathPoint::StopSubpath; properties &= ~KoPathPoint::CloseSubpath; // check if new point starts subpath if (pointIndex.second == 0) { properties |= KoPathPoint::StartSubpath; // subpath was closed if (subpath->last()->properties() & KoPathPoint::CloseSubpath) { // keep the path closed properties |= KoPathPoint::CloseSubpath; } // old first point does not start the subpath anymore subpath->first()->unsetProperty(KoPathPoint::StartSubpath); } // check if new point stops subpath else if (pointIndex.second == subpath->size()) { properties |= KoPathPoint::StopSubpath; // subpath was closed if (subpath->last()->properties() & KoPathPoint::CloseSubpath) { // keep the path closed properties = properties | KoPathPoint::CloseSubpath; } // old last point does not end subpath anymore subpath->last()->unsetProperty(KoPathPoint::StopSubpath); } point->setProperties(properties); point->setParent(this); subpath->insert(pointIndex.second , point); notifyPointsChanged(); return true; } KoPathPoint * KoPathShape::removePoint(const KoPathPointIndex &pointIndex) { Q_D(KoPathShape); KoSubpath *subpath = d->subPath(pointIndex.first); if (subpath == 0 || pointIndex.second < 0 || pointIndex.second >= subpath->size()) return 0; KoPathPoint * point = subpath->takeAt(pointIndex.second); point->setParent(0); //don't do anything (not even crash), if there was only one point if (pointCount()==0) { return point; } // check if we removed the first point else if (pointIndex.second == 0) { // first point removed, set new StartSubpath subpath->first()->setProperty(KoPathPoint::StartSubpath); // check if path was closed if (subpath->last()->properties() & KoPathPoint::CloseSubpath) { // keep path closed subpath->first()->setProperty(KoPathPoint::CloseSubpath); } } // check if we removed the last point else if (pointIndex.second == subpath->size()) { // use size as point is already removed // last point removed, set new StopSubpath subpath->last()->setProperty(KoPathPoint::StopSubpath); // check if path was closed if (point->properties() & KoPathPoint::CloseSubpath) { // keep path closed subpath->last()->setProperty(KoPathPoint::CloseSubpath); } } notifyPointsChanged(); return point; } bool KoPathShape::breakAfter(const KoPathPointIndex &pointIndex) { Q_D(KoPathShape); KoSubpath *subpath = d->subPath(pointIndex.first); if (!subpath || pointIndex.second < 0 || pointIndex.second > subpath->size() - 2 || isClosedSubpath(pointIndex.first)) return false; KoSubpath * newSubpath = new KoSubpath; int size = subpath->size(); for (int i = pointIndex.second + 1; i < size; ++i) { newSubpath->append(subpath->takeAt(pointIndex.second + 1)); } // now make the first point of the new subpath a starting node newSubpath->first()->setProperty(KoPathPoint::StartSubpath); // the last point of the old subpath is now an ending node subpath->last()->setProperty(KoPathPoint::StopSubpath); // insert the new subpath after the broken one d->subpaths.insert(pointIndex.first + 1, newSubpath); notifyPointsChanged(); return true; } bool KoPathShape::join(int subpathIndex) { Q_D(KoPathShape); KoSubpath *subpath = d->subPath(subpathIndex); KoSubpath *nextSubpath = d->subPath(subpathIndex + 1); if (!subpath || !nextSubpath || isClosedSubpath(subpathIndex) || isClosedSubpath(subpathIndex+1)) return false; // the last point of the subpath does not end the subpath anymore subpath->last()->unsetProperty(KoPathPoint::StopSubpath); // the first point of the next subpath does not start a subpath anymore nextSubpath->first()->unsetProperty(KoPathPoint::StartSubpath); // append the second subpath to the first Q_FOREACH (KoPathPoint * p, *nextSubpath) subpath->append(p); // remove the nextSubpath from path d->subpaths.removeAt(subpathIndex + 1); // delete it as it is no longer possible to use it delete nextSubpath; notifyPointsChanged(); return true; } bool KoPathShape::moveSubpath(int oldSubpathIndex, int newSubpathIndex) { Q_D(KoPathShape); KoSubpath *subpath = d->subPath(oldSubpathIndex); if (subpath == 0 || newSubpathIndex >= d->subpaths.size()) return false; if (oldSubpathIndex == newSubpathIndex) return true; d->subpaths.removeAt(oldSubpathIndex); d->subpaths.insert(newSubpathIndex, subpath); notifyPointsChanged(); return true; } KoPathPointIndex KoPathShape::openSubpath(const KoPathPointIndex &pointIndex) { Q_D(KoPathShape); KoSubpath *subpath = d->subPath(pointIndex.first); if (!subpath || pointIndex.second < 0 || pointIndex.second >= subpath->size() || !isClosedSubpath(pointIndex.first)) return KoPathPointIndex(-1, -1); KoPathPoint * oldStartPoint = subpath->first(); // the old starting node no longer starts the subpath oldStartPoint->unsetProperty(KoPathPoint::StartSubpath); // the old end node no longer closes the subpath subpath->last()->unsetProperty(KoPathPoint::StopSubpath); // reorder the subpath for (int i = 0; i < pointIndex.second; ++i) { subpath->append(subpath->takeFirst()); } // make the first point a start node subpath->first()->setProperty(KoPathPoint::StartSubpath); // make the last point an end node subpath->last()->setProperty(KoPathPoint::StopSubpath); notifyPointsChanged(); return pathPointIndex(oldStartPoint); } KoPathPointIndex KoPathShape::closeSubpath(const KoPathPointIndex &pointIndex) { Q_D(KoPathShape); KoSubpath *subpath = d->subPath(pointIndex.first); if (!subpath || pointIndex.second < 0 || pointIndex.second >= subpath->size() || isClosedSubpath(pointIndex.first)) return KoPathPointIndex(-1, -1); KoPathPoint * oldStartPoint = subpath->first(); // the old starting node no longer starts the subpath oldStartPoint->unsetProperty(KoPathPoint::StartSubpath); // the old end node no longer ends the subpath subpath->last()->unsetProperty(KoPathPoint::StopSubpath); // reorder the subpath for (int i = 0; i < pointIndex.second; ++i) { subpath->append(subpath->takeFirst()); } subpath->first()->setProperty(KoPathPoint::StartSubpath); subpath->last()->setProperty(KoPathPoint::StopSubpath); d->closeSubpath(subpath); notifyPointsChanged(); return pathPointIndex(oldStartPoint); } bool KoPathShape::reverseSubpath(int subpathIndex) { Q_D(KoPathShape); KoSubpath *subpath = d->subPath(subpathIndex); if (subpath == 0) return false; int size = subpath->size(); for (int i = 0; i < size; ++i) { KoPathPoint *p = subpath->takeAt(i); p->reverse(); subpath->prepend(p); } // adjust the position dependent properties KoPathPoint *first = subpath->first(); KoPathPoint *last = subpath->last(); KoPathPoint::PointProperties firstProps = first->properties(); KoPathPoint::PointProperties lastProps = last->properties(); firstProps |= KoPathPoint::StartSubpath; firstProps &= ~KoPathPoint::StopSubpath; lastProps |= KoPathPoint::StopSubpath; lastProps &= ~KoPathPoint::StartSubpath; if (firstProps & KoPathPoint::CloseSubpath) { firstProps |= KoPathPoint::CloseSubpath; lastProps |= KoPathPoint::CloseSubpath; } first->setProperties(firstProps); last->setProperties(lastProps); notifyPointsChanged(); return true; } KoSubpath * KoPathShape::removeSubpath(int subpathIndex) { Q_D(KoPathShape); KoSubpath *subpath = d->subPath(subpathIndex); if (subpath != 0) { Q_FOREACH (KoPathPoint* point, *subpath) { point->setParent(this); } d->subpaths.removeAt(subpathIndex); } notifyPointsChanged(); return subpath; } bool KoPathShape::addSubpath(KoSubpath * subpath, int subpathIndex) { Q_D(KoPathShape); if (subpathIndex < 0 || subpathIndex > d->subpaths.size()) return false; Q_FOREACH (KoPathPoint* point, *subpath) { point->setParent(this); } d->subpaths.insert(subpathIndex, subpath); notifyPointsChanged(); return true; } int KoPathShape::combine(KoPathShape *path) { Q_D(KoPathShape); int insertSegmentPosition = -1; if (!path) return insertSegmentPosition; QTransform pathMatrix = path->absoluteTransformation(0); QTransform myMatrix = absoluteTransformation(0).inverted(); Q_FOREACH (KoSubpath* subpath, path->d_func()->subpaths) { KoSubpath *newSubpath = new KoSubpath(); Q_FOREACH (KoPathPoint* point, *subpath) { KoPathPoint *newPoint = new KoPathPoint(*point, this); newPoint->map(pathMatrix); newPoint->map(myMatrix); newSubpath->append(newPoint); } d->subpaths.append(newSubpath); if (insertSegmentPosition < 0) { insertSegmentPosition = d->subpaths.size() - 1; } } normalize(); notifyPointsChanged(); return insertSegmentPosition; } bool KoPathShape::separate(QList & separatedPaths) { Q_D(KoPathShape); if (! d->subpaths.size()) return false; QTransform myMatrix = absoluteTransformation(0); Q_FOREACH (KoSubpath* subpath, d->subpaths) { KoPathShape *shape = new KoPathShape(); shape->setStroke(stroke()); shape->setBackground(background()); shape->setShapeId(shapeId()); shape->setZIndex(zIndex()); KoSubpath *newSubpath = new KoSubpath(); Q_FOREACH (KoPathPoint* point, *subpath) { KoPathPoint *newPoint = new KoPathPoint(*point, shape); newPoint->map(myMatrix); newSubpath->append(newPoint); } shape->d_func()->subpaths.append(newSubpath); shape->normalize(); // NOTE: shape cannot have any listeners yet, so no notification about // points modification is needed separatedPaths.append(shape); } return true; } void KoPathShapePrivate::closeSubpath(KoSubpath *subpath) { Q_Q(KoPathShape); if (! subpath) return; subpath->last()->setProperty(KoPathPoint::CloseSubpath); subpath->first()->setProperty(KoPathPoint::CloseSubpath); q->notifyPointsChanged(); } void KoPathShapePrivate::closeMergeSubpath(KoSubpath *subpath) { Q_Q(KoPathShape); if (! subpath || subpath->size() < 2) return; KoPathPoint * lastPoint = subpath->last(); KoPathPoint * firstPoint = subpath->first(); // check if first and last points are coincident if (lastPoint->point() == firstPoint->point()) { // we are removing the current last point and // reuse its first control point if active firstPoint->setProperty(KoPathPoint::StartSubpath); firstPoint->setProperty(KoPathPoint::CloseSubpath); if (lastPoint->activeControlPoint1()) firstPoint->setControlPoint1(lastPoint->controlPoint1()); // remove last point delete subpath->takeLast(); // the new last point closes the subpath now lastPoint = subpath->last(); lastPoint->setProperty(KoPathPoint::StopSubpath); lastPoint->setProperty(KoPathPoint::CloseSubpath); q->notifyPointsChanged(); } else { closeSubpath(subpath); } } KoSubpath *KoPathShapePrivate::subPath(int subpathIndex) const { if (subpathIndex < 0 || subpathIndex >= subpaths.size()) return 0; return subpaths.at(subpathIndex); } QString KoPathShape::pathShapeId() const { return KoPathShapeId; } QString KoPathShape::toString(const QTransform &matrix) const { Q_D(const KoPathShape); QString pathString; // iterate over all subpaths KoSubpathList::const_iterator pathIt(d->subpaths.constBegin()); for (; pathIt != d->subpaths.constEnd(); ++pathIt) { KoSubpath::const_iterator pointIt((*pathIt)->constBegin()); // keep a pointer to the first point of the subpath KoPathPoint *firstPoint(*pointIt); // keep a pointer to the previous point of the subpath KoPathPoint *lastPoint = firstPoint; // keep track if the previous point has an active control point 2 bool activeControlPoint2 = false; // iterate over all points of the current subpath for (; pointIt != (*pathIt)->constEnd(); ++pointIt) { KoPathPoint *currPoint(*pointIt); if (!currPoint) { qWarning() << "Found a zero point in the shape's path!"; continue; } // first point of subpath ? if (currPoint == firstPoint) { // are we starting a subpath ? if (currPoint->properties() & KoPathPoint::StartSubpath) { const QPointF p = matrix.map(currPoint->point()); pathString += QString("M%1 %2").arg(p.x()).arg(p.y()); } } // end point of curve segment ? else if (activeControlPoint2 || currPoint->activeControlPoint1()) { // check if we have a cubic or quadratic curve const bool isCubic = activeControlPoint2 && currPoint->activeControlPoint1(); KoPathSegment cubicSeg = isCubic ? KoPathSegment(lastPoint, currPoint) : KoPathSegment(lastPoint, currPoint).toCubic(); if (cubicSeg.first() && cubicSeg.second()) { const QPointF cp1 = matrix.map(cubicSeg.first()->controlPoint2()); const QPointF cp2 = matrix.map(cubicSeg.second()->controlPoint1()); const QPointF p = matrix.map(cubicSeg.second()->point()); pathString += QString("C%1 %2 %3 %4 %5 %6") .arg(cp1.x()).arg(cp1.y()) .arg(cp2.x()).arg(cp2.y()) .arg(p.x()).arg(p.y()); } } // end point of line segment! else { const QPointF p = matrix.map(currPoint->point()); pathString += QString("L%1 %2").arg(p.x()).arg(p.y()); } // last point closes subpath ? if (currPoint->properties() & KoPathPoint::StopSubpath && currPoint->properties() & KoPathPoint::CloseSubpath) { // add curve when there is a curve on the way to the first point if (currPoint->activeControlPoint2() || firstPoint->activeControlPoint1()) { // check if we have a cubic or quadratic curve const bool isCubic = currPoint->activeControlPoint2() && firstPoint->activeControlPoint1(); KoPathSegment cubicSeg = isCubic ? KoPathSegment(currPoint, firstPoint) : KoPathSegment(currPoint, firstPoint).toCubic(); if (cubicSeg.first() && cubicSeg.second()) { const QPointF cp1 = matrix.map(cubicSeg.first()->controlPoint2()); const QPointF cp2 = matrix.map(cubicSeg.second()->controlPoint1()); const QPointF p = matrix.map(cubicSeg.second()->point()); pathString += QString("C%1 %2 %3 %4 %5 %6") .arg(cp1.x()).arg(cp1.y()) .arg(cp2.x()).arg(cp2.y()) .arg(p.x()).arg(p.y()); } } pathString += QString("Z"); } activeControlPoint2 = currPoint->activeControlPoint2(); lastPoint = currPoint; } } return pathString; } char nodeType(const KoPathPoint * point) { if (point->properties() & KoPathPoint::IsSmooth) { return 's'; } else if (point->properties() & KoPathPoint::IsSymmetric) { return 'z'; } else { return 'c'; } } QString KoPathShapePrivate::nodeTypes() const { QString types; KoSubpathList::const_iterator pathIt(subpaths.constBegin()); for (; pathIt != subpaths.constEnd(); ++pathIt) { KoSubpath::const_iterator it((*pathIt)->constBegin()); for (; it != (*pathIt)->constEnd(); ++it) { if (it == (*pathIt)->constBegin()) { types.append('c'); } else { types.append(nodeType(*it)); } if ((*it)->properties() & KoPathPoint::StopSubpath && (*it)->properties() & KoPathPoint::CloseSubpath) { KoPathPoint * firstPoint = (*pathIt)->first(); types.append(nodeType(firstPoint)); } } } return types; } void updateNodeType(KoPathPoint * point, const QChar & nodeType) { if (nodeType == 's') { point->setProperty(KoPathPoint::IsSmooth); } else if (nodeType == 'z') { point->setProperty(KoPathPoint::IsSymmetric); } } void KoPathShapePrivate::loadNodeTypes(const KoXmlElement &element) { if (element.hasAttributeNS(KoXmlNS::calligra, "nodeTypes")) { QString nodeTypes = element.attributeNS(KoXmlNS::calligra, "nodeTypes"); QString::const_iterator nIt(nodeTypes.constBegin()); KoSubpathList::const_iterator pathIt(subpaths.constBegin()); for (; pathIt != subpaths.constEnd(); ++pathIt) { KoSubpath::const_iterator it((*pathIt)->constBegin()); for (; it != (*pathIt)->constEnd(); ++it, nIt++) { // be sure not to crash if there are not enough nodes in nodeTypes if (nIt == nodeTypes.constEnd()) { warnFlake << "not enough nodes in calligra:nodeTypes"; return; } // the first node is always of type 'c' if (it != (*pathIt)->constBegin()) { updateNodeType(*it, *nIt); } if ((*it)->properties() & KoPathPoint::StopSubpath && (*it)->properties() & KoPathPoint::CloseSubpath) { ++nIt; updateNodeType((*pathIt)->first(), *nIt); } } } } } Qt::FillRule KoPathShape::fillRule() const { Q_D(const KoPathShape); return d->fillRule; } void KoPathShape::setFillRule(Qt::FillRule fillRule) { Q_D(KoPathShape); d->fillRule = fillRule; } KoPathShape * KoPathShape::createShapeFromPainterPath(const QPainterPath &path) { KoPathShape * shape = new KoPathShape(); int elementCount = path.elementCount(); for (int i = 0; i < elementCount; i++) { QPainterPath::Element element = path.elementAt(i); switch (element.type) { case QPainterPath::MoveToElement: shape->moveTo(QPointF(element.x, element.y)); break; case QPainterPath::LineToElement: shape->lineTo(QPointF(element.x, element.y)); break; case QPainterPath::CurveToElement: shape->curveTo(QPointF(element.x, element.y), QPointF(path.elementAt(i + 1).x, path.elementAt(i + 1).y), QPointF(path.elementAt(i + 2).x, path.elementAt(i + 2).y)); break; default: continue; } } shape->setShapeId(KoPathShapeId); //shape->normalize(); return shape; } bool KoPathShape::hitTest(const QPointF &position) const { if (parent() && parent()->isClipped(this) && ! parent()->hitTest(position)) return false; QPointF point = absoluteTransformation(0).inverted().map(position); const QPainterPath outlinePath = outline(); if (stroke()) { KoInsets insets; stroke()->strokeInsets(this, insets); QRectF roi(QPointF(-insets.left, -insets.top), QPointF(insets.right, insets.bottom)); roi.moveCenter(point); if (outlinePath.intersects(roi) || outlinePath.contains(roi)) return true; } else { if (outlinePath.contains(point)) return true; } // if there is no shadow we can as well just leave if (! shadow()) return false; // the shadow has an offset to the shape, so we simply // check if the position minus the shadow offset hits the shape point = absoluteTransformation(0).inverted().map(position - shadow()->offset()); return outlinePath.contains(point); } void KoPathShape::setMarker(KoMarker *marker, KoFlake::MarkerPosition pos) { Q_D(KoPathShape); if (!marker && d->markersNew.contains(pos)) { d->markersNew.remove(pos); } else { d->markersNew[pos] = marker; } } KoMarker *KoPathShape::marker(KoFlake::MarkerPosition pos) const { Q_D(const KoPathShape); return d->markersNew[pos].data(); } bool KoPathShape::hasMarkers() const { Q_D(const KoPathShape); return !d->markersNew.isEmpty(); } bool KoPathShape::autoFillMarkers() const { Q_D(const KoPathShape); return d->autoFillMarkers; } void KoPathShape::setAutoFillMarkers(bool value) { Q_D(KoPathShape); d->autoFillMarkers = value; } void KoPathShape::recommendPointSelectionChange(const QList &newSelection) { Q_D(KoShape); Q_FOREACH (KoShape::ShapeChangeListener *listener, d->listeners) { PointSelectionChangeListener *pointListener = dynamic_cast(listener); if (pointListener) { pointListener->recommendPointSelectionChange(this, newSelection); } } } void KoPathShape::notifyPointsChanged() { Q_D(KoShape); Q_FOREACH (KoShape::ShapeChangeListener *listener, d->listeners) { PointSelectionChangeListener *pointListener = dynamic_cast(listener); if (pointListener) { pointListener->notifyPathPointsChanged(this); } } } QPainterPath KoPathShape::pathStroke(const QPen &pen) const { Q_D(const KoPathShape); if (d->subpaths.isEmpty()) { return QPainterPath(); } QPainterPath pathOutline; QPainterPathStroker stroker; stroker.setWidth(0); stroker.setJoinStyle(Qt::MiterJoin); stroker.setWidth(pen.widthF()); stroker.setJoinStyle(pen.joinStyle()); stroker.setMiterLimit(pen.miterLimit()); stroker.setCapStyle(pen.capStyle()); stroker.setDashOffset(pen.dashOffset()); stroker.setDashPattern(pen.dashPattern()); QPainterPath path = stroker.createStroke(outline()); pathOutline.addPath(path); pathOutline.setFillRule(Qt::WindingFill); return pathOutline; } void KoPathShape::PointSelectionChangeListener::notifyShapeChanged(KoShape::ChangeType type, KoShape *shape) { Q_UNUSED(type); Q_UNUSED(shape); } diff --git a/libs/flake/KoPatternBackground.cpp b/libs/flake/KoPatternBackground.cpp index d98ecf8632..46e3f80dea 100644 --- a/libs/flake/KoPatternBackground.cpp +++ b/libs/flake/KoPatternBackground.cpp @@ -1,500 +1,501 @@ /* This file is part of the KDE project * Copyright (C) 2008 Jan Hambrecht * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "KoPatternBackground.h" #include "KoShapeBackground_p.h" #include "KoShapeSavingContext.h" #include "KoImageData.h" #include "KoImageCollection.h" #include #include #include #include #include #include #include #include #include #include #include #include #include +#include #include class KoPatternBackgroundPrivate : public KoShapeBackgroundPrivate { public: KoPatternBackgroundPrivate() : repeat(KoPatternBackground::Tiled) , refPoint(KoPatternBackground::TopLeft) , imageCollection(0) , imageData(0) { } ~KoPatternBackgroundPrivate() override { delete imageData; } QSizeF targetSize() const { QSizeF size = imageData->imageSize(); if (targetImageSizePercent.width() > 0.0) size.setWidth(0.01 * targetImageSizePercent.width() * size.width()); else if (targetImageSize.width() > 0.0) size.setWidth(targetImageSize.width()); if (targetImageSizePercent.height() > 0.0) size.setHeight(0.01 * targetImageSizePercent.height() * size.height()); else if (targetImageSize.height() > 0.0) size.setHeight(targetImageSize.height()); return size; } QPointF offsetFromRect(const QRectF &fillRect, const QSizeF &imageSize) const { QPointF offset; switch (refPoint) { case KoPatternBackground::TopLeft: offset = fillRect.topLeft(); break; case KoPatternBackground::Top: offset.setX(fillRect.center().x() - 0.5 * imageSize.width()); offset.setY(fillRect.top()); break; case KoPatternBackground::TopRight: offset.setX(fillRect.right() - imageSize.width()); offset.setY(fillRect.top()); break; case KoPatternBackground::Left: offset.setX(fillRect.left()); offset.setY(fillRect.center().y() - 0.5 * imageSize.height()); break; case KoPatternBackground::Center: offset.setX(fillRect.center().x() - 0.5 * imageSize.width()); offset.setY(fillRect.center().y() - 0.5 * imageSize.height()); break; case KoPatternBackground::Right: offset.setX(fillRect.right() - imageSize.width()); offset.setY(fillRect.center().y() - 0.5 * imageSize.height()); break; case KoPatternBackground::BottomLeft: offset.setX(fillRect.left()); offset.setY(fillRect.bottom() - imageSize.height()); break; case KoPatternBackground::Bottom: offset.setX(fillRect.center().x() - 0.5 * imageSize.width()); offset.setY(fillRect.bottom() - imageSize.height()); break; case KoPatternBackground::BottomRight: offset.setX(fillRect.right() - imageSize.width()); offset.setY(fillRect.bottom() - imageSize.height()); break; default: break; } if (refPointOffsetPercent.x() > 0.0) offset += QPointF(0.01 * refPointOffsetPercent.x() * imageSize.width(), 0); if (refPointOffsetPercent.y() > 0.0) offset += QPointF(0, 0.01 * refPointOffsetPercent.y() * imageSize.height()); return offset; } QTransform matrix; KoPatternBackground::PatternRepeat repeat; KoPatternBackground::ReferencePoint refPoint; QSizeF targetImageSize; QSizeF targetImageSizePercent; QPointF refPointOffsetPercent; QPointF tileRepeatOffsetPercent; QPointer imageCollection; KoImageData * imageData; }; // ---------------------------------------------------------------- KoPatternBackground::KoPatternBackground(KoImageCollection *imageCollection) : KoShapeBackground(*(new KoPatternBackgroundPrivate())) { Q_D(KoPatternBackground); d->imageCollection = imageCollection; Q_ASSERT(d->imageCollection); } KoPatternBackground::~KoPatternBackground() { } bool KoPatternBackground::compareTo(const KoShapeBackground *other) const { Q_UNUSED(other); return false; } void KoPatternBackground::setTransform(const QTransform &matrix) { Q_D(KoPatternBackground); d->matrix = matrix; } QTransform KoPatternBackground::transform() const { Q_D(const KoPatternBackground); return d->matrix; } void KoPatternBackground::setPattern(const QImage &pattern) { Q_D(KoPatternBackground); delete d->imageData; if (d->imageCollection) { d->imageData = d->imageCollection->createImageData(pattern); } } void KoPatternBackground::setPattern(KoImageData *imageData) { Q_D(KoPatternBackground); delete d->imageData; d->imageData = imageData; } QImage KoPatternBackground::pattern() const { Q_D(const KoPatternBackground); if (d->imageData) return d->imageData->image(); return QImage(); } void KoPatternBackground::setRepeat(PatternRepeat repeat) { Q_D(KoPatternBackground); d->repeat = repeat; } KoPatternBackground::PatternRepeat KoPatternBackground::repeat() const { Q_D(const KoPatternBackground); return d->repeat; } KoPatternBackground::ReferencePoint KoPatternBackground::referencePoint() const { Q_D(const KoPatternBackground); return d->refPoint; } void KoPatternBackground::setReferencePoint(ReferencePoint referencePoint) { Q_D(KoPatternBackground); d->refPoint = referencePoint; } QPointF KoPatternBackground::referencePointOffset() const { Q_D(const KoPatternBackground); return d->refPointOffsetPercent; } void KoPatternBackground::setReferencePointOffset(const QPointF &offset) { Q_D(KoPatternBackground); qreal ox = qMax(qreal(0.0), qMin(qreal(100.0), offset.x())); qreal oy = qMax(qreal(0.0), qMin(qreal(100.0), offset.y())); d->refPointOffsetPercent = QPointF(ox, oy); } QPointF KoPatternBackground::tileRepeatOffset() const { Q_D(const KoPatternBackground); return d->tileRepeatOffsetPercent; } void KoPatternBackground::setTileRepeatOffset(const QPointF &offset) { Q_D(KoPatternBackground); d->tileRepeatOffsetPercent = offset; } QSizeF KoPatternBackground::patternDisplaySize() const { Q_D(const KoPatternBackground); return d->targetSize(); } void KoPatternBackground::setPatternDisplaySize(const QSizeF &size) { Q_D(KoPatternBackground); d->targetImageSizePercent = QSizeF(); d->targetImageSize = size; } QSizeF KoPatternBackground::patternOriginalSize() const { Q_D(const KoPatternBackground); return d->imageData->imageSize(); } void KoPatternBackground::paint(QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &/*context*/, const QPainterPath &fillPath) const { Q_D(const KoPatternBackground); if (! d->imageData) return; painter.save(); if (d->repeat == Tiled) { // calculate scaling of pixmap QSizeF targetSize = d->targetSize(); QSizeF imageSize = d->imageData->imageSize(); qreal scaleX = targetSize.width() / imageSize.width(); qreal scaleY = targetSize.height() / imageSize.height(); QRectF targetRect = fillPath.boundingRect(); // undo scaling on target rectangle targetRect.setWidth(targetRect.width() / scaleX); targetRect.setHeight(targetRect.height() / scaleY); // determine pattern offset QPointF offset = d->offsetFromRect(targetRect, imageSize); // create matrix for pixmap scaling QTransform matrix; matrix.scale(scaleX, scaleY); painter.setClipPath(fillPath); painter.setWorldTransform(matrix, true); painter.drawTiledPixmap(targetRect, d->imageData->pixmap(imageSize.toSize()), -offset); } else if (d->repeat == Original) { QRectF sourceRect(QPointF(0, 0), d->imageData->imageSize()); QRectF targetRect(QPoint(0, 0), d->targetSize()); targetRect.moveCenter(fillPath.boundingRect().center()); painter.setClipPath(fillPath); painter.drawPixmap(targetRect, d->imageData->pixmap(sourceRect.size().toSize()), sourceRect); } else if (d->repeat == Stretched) { painter.setClipPath(fillPath); // undo conversion of the scaling so that we can use a nicely scaled image of the correct size qreal zoomX, zoomY; converter.zoom(&zoomX, &zoomY); zoomX = zoomX ? 1 / zoomX : zoomX; zoomY = zoomY ? 1 / zoomY : zoomY; painter.scale(zoomX, zoomY); QRectF targetRect = converter.documentToView(fillPath.boundingRect()); painter.drawPixmap(targetRect.topLeft(), d->imageData->pixmap(targetRect.size().toSize())); } painter.restore(); } void KoPatternBackground::fillStyle(KoGenStyle &style, KoShapeSavingContext &context) { Q_D(KoPatternBackground); if (! d->imageData) return; switch (d->repeat) { case Original: style.addProperty("style:repeat", "no-repeat"); break; case Tiled: style.addProperty("style:repeat", "repeat"); break; case Stretched: style.addProperty("style:repeat", "stretch"); break; } if (d->repeat == Tiled) { QString refPointId = "top-left"; switch (d->refPoint) { case TopLeft: refPointId = "top-left"; break; case Top: refPointId = "top"; break; case TopRight: refPointId = "top-right"; break; case Left: refPointId = "left"; break; case Center: refPointId = "center"; break; case Right: refPointId = "right"; break; case BottomLeft: refPointId = "bottom-left"; break; case Bottom: refPointId = "bottom"; break; case BottomRight: refPointId = "bottom-right"; break; } style.addProperty("draw:fill-image-ref-point", refPointId); if (d->refPointOffsetPercent.x() > 0.0) style.addProperty("draw:fill-image-ref-point-x", QString("%1%").arg(d->refPointOffsetPercent.x())); if (d->refPointOffsetPercent.y() > 0.0) style.addProperty("draw:fill-image-ref-point-y", QString("%1%").arg(d->refPointOffsetPercent.y())); } if (d->repeat != Stretched) { QSizeF targetSize = d->targetSize(); QSizeF imageSize = d->imageData->imageSize(); if (targetSize.height() != imageSize.height()) style.addPropertyPt("draw:fill-image-height", targetSize.height()); if (targetSize.width() != imageSize.width()) style.addPropertyPt("draw:fill-image-width", targetSize.width()); } KoGenStyle patternStyle(KoGenStyle::FillImageStyle /*no family name*/); patternStyle.addAttribute("xlink:show", "embed"); patternStyle.addAttribute("xlink:actuate", "onLoad"); patternStyle.addAttribute("xlink:type", "simple"); patternStyle.addAttribute("xlink:href", context.imageHref(d->imageData)); QString patternStyleName = context.mainStyles().insert(patternStyle, "picture"); style.addProperty("draw:fill", "bitmap"); style.addProperty("draw:fill-image-name", patternStyleName); if (d->imageCollection) { context.addDataCenter(d->imageCollection); } } bool KoPatternBackground::loadStyle(KoOdfLoadingContext &context, const QSizeF &) { Q_D(KoPatternBackground); KoStyleStack &styleStack = context.styleStack(); if (! styleStack.hasProperty(KoXmlNS::draw, "fill")) return false; QString fillStyle = styleStack.property(KoXmlNS::draw, "fill"); if (fillStyle != "bitmap") return false; QString styleName = styleStack.property(KoXmlNS::draw, "fill-image-name"); KoXmlElement* e = context.stylesReader().drawStyles("fill-image")[styleName]; if (! e) return false; const QString href = e->attributeNS(KoXmlNS::xlink, "href", QString()); if (href.isEmpty()) return false; delete d->imageData; d->imageData = 0; if (d->imageCollection) { d->imageData = d->imageCollection->createImageData(href, context.store()); } if (! d->imageData) { return false; } // read the pattern repeat style QString style = styleStack.property(KoXmlNS::style, "repeat"); if (style == "stretch") d->repeat = Stretched; else if (style == "no-repeat") d->repeat = Original; else d->repeat = Tiled; if (style != "stretch") { // optional attributes which can override original image size if (styleStack.hasProperty(KoXmlNS::draw, "fill-image-height")) { QString height = styleStack.property(KoXmlNS::draw, "fill-image-height"); if (height.endsWith('%')) d->targetImageSizePercent.setHeight(height.remove('%').toDouble()); else d->targetImageSize.setHeight(KoUnit::parseValue(height)); } if (styleStack.hasProperty(KoXmlNS::draw, "fill-image-width")) { QString width = styleStack.property(KoXmlNS::draw, "fill-image-width"); if (width.endsWith('%')) d->targetImageSizePercent.setWidth(width.remove('%').toDouble()); else d->targetImageSize.setWidth(KoUnit::parseValue(width)); } } if (style == "repeat") { if (styleStack.hasProperty(KoXmlNS::draw, "fill-image-ref-point")) { // align pattern to the given size QString align = styleStack.property(KoXmlNS::draw, "fill-image-ref-point"); if (align == "top-left") d->refPoint = TopLeft; else if (align == "top") d->refPoint = Top; else if (align == "top-right") d->refPoint = TopRight; else if (align == "left") d->refPoint = Left; else if (align == "center") d->refPoint = Center; else if (align == "right") d->refPoint = Right; else if (align == "bottom-left") d->refPoint = BottomLeft; else if (align == "bottom") d->refPoint = Bottom; else if (align == "bottom-right") d->refPoint = BottomRight; } if (styleStack.hasProperty(KoXmlNS::draw, "fill-image-ref-point-x")) { QString pointX = styleStack.property(KoXmlNS::draw, "fill-image-ref-point-x"); d->refPointOffsetPercent.setX(pointX.remove('%').toDouble()); } if (styleStack.hasProperty(KoXmlNS::draw, "fill-image-ref-point-y")) { QString pointY = styleStack.property(KoXmlNS::draw, "fill-image-ref-point-y"); d->refPointOffsetPercent.setY(pointY.remove('%').toDouble()); } if (styleStack.hasProperty(KoXmlNS::draw, "tile-repeat-offset")) { QString repeatOffset = styleStack.property(KoXmlNS::draw, "tile-repeat-offset"); QStringList tokens = repeatOffset.split('%'); if (tokens.count() == 2) { QString direction = tokens[1].simplified(); if (direction == "horizontal") d->tileRepeatOffsetPercent.setX(tokens[0].toDouble()); else if (direction == "vertical") d->tileRepeatOffsetPercent.setY(tokens[0].toDouble()); } } } return true; } QRectF KoPatternBackground::patternRectFromFillSize(const QSizeF &size) { Q_D(KoPatternBackground); QRectF rect; switch (d->repeat) { case Tiled: rect.setTopLeft(d->offsetFromRect(QRectF(QPointF(), size), d->targetSize())); rect.setSize(d->targetSize()); break; case Original: rect.setLeft(0.5 * (size.width() - d->targetSize().width())); rect.setTop(0.5 * (size.height() - d->targetSize().height())); rect.setSize(d->targetSize()); break; case Stretched: rect.setTopLeft(QPointF(0.0, 0.0)); rect.setSize(size); break; } return rect; } diff --git a/libs/flake/KoShapeManager.cpp b/libs/flake/KoShapeManager.cpp index 43bacd5e31..845c0bb8e5 100644 --- a/libs/flake/KoShapeManager.cpp +++ b/libs/flake/KoShapeManager.cpp @@ -1,725 +1,726 @@ /* This file is part of the KDE project Copyright (C) 2006-2008 Thorsten Zachmann Copyright (C) 2006-2010 Thomas Zander Copyright (C) 2009-2010 Jan Hambrecht This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "KoShapeManager.h" #include "KoShapeManager_p.h" #include "KoSelection.h" #include "KoToolManager.h" #include "KoPointerEvent.h" #include "KoShape.h" #include "KoShape_p.h" #include "KoCanvasBase.h" #include "KoShapeContainer.h" #include "KoShapeStrokeModel.h" #include "KoShapeGroup.h" #include "KoToolProxy.h" #include "KoShapeShadow.h" #include "KoShapeLayer.h" #include "KoFilterEffect.h" #include "KoFilterEffectStack.h" #include "KoFilterEffectRenderContext.h" #include "KoShapeBackground.h" #include #include "KoClipPath.h" #include "KoClipMaskPainter.h" #include "KoShapePaintingContext.h" #include "KoViewConverter.h" #include "KisQPainterStateSaver.h" #include "KoSvgTextChunkShape.h" #include "KoSvgTextShape.h" #include #include +#include #include #include #include "kis_painting_tweaks.h" bool KoShapeManager::Private::shapeUsedInRenderingTree(KoShape *shape) { // FIXME: make more general! return !dynamic_cast(shape) && !dynamic_cast(shape) && !(dynamic_cast(shape) && !dynamic_cast(shape)); } void KoShapeManager::Private::updateTree() { bool selectionModified = false; bool anyModified = false; { QMutexLocker l(&this->treeMutex); Q_FOREACH (KoShape *shape, aggregate4update) { selectionModified = selectionModified || selection->isSelected(shape); anyModified = true; } foreach (KoShape *shape, aggregate4update) { if (!shapeUsedInRenderingTree(shape)) continue; tree.remove(shape); QRectF br(shape->boundingRect()); tree.insert(br, shape); } aggregate4update.clear(); shapeIndexesBeforeUpdate.clear(); } if (selectionModified) { emit q->selectionContentChanged(); } if (anyModified) { emit q->contentChanged(); } } void KoShapeManager::Private::forwardCompressedUdpate() { bool shouldUpdateDecorations = false; QRectF scheduledUpdate; { QMutexLocker l(&shapesMutex); if (!compressedUpdate.isEmpty()) { scheduledUpdate = compressedUpdate; compressedUpdate = QRect(); } Q_FOREACH (const KoShape *shape, compressedUpdatedShapes) { if (selection->isSelected(shape)) { shouldUpdateDecorations = true; break; } } compressedUpdatedShapes.clear(); } if (shouldUpdateDecorations && canvas->toolProxy()) { canvas->toolProxy()->repaintDecorations(); } canvas->updateCanvas(scheduledUpdate); } void KoShapeManager::Private::paintGroup(KoShapeGroup *group, QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintContext) { QList shapes = group->shapes(); std::sort(shapes.begin(), shapes.end(), KoShape::compareShapeZIndex); Q_FOREACH (KoShape *child, shapes) { // we paint recursively here, so we do not have to check recursively for visibility if (!child->isVisible(false)) continue; KoShapeGroup *childGroup = dynamic_cast(child); if (childGroup) { paintGroup(childGroup, painter, converter, paintContext); } else { painter.save(); KoShapeManager::renderSingleShape(child, painter, converter, paintContext); painter.restore(); } } } KoShapeManager::KoShapeManager(KoCanvasBase *canvas, const QList &shapes) : d(new Private(this, canvas)) { Q_ASSERT(d->canvas); // not optional. connect(d->selection, SIGNAL(selectionChanged()), this, SIGNAL(selectionChanged())); setShapes(shapes); /** * Shape manager uses signal compressors with timers, therefore * it might handle queued signals, therefore it should belong * to the GUI thread. */ this->moveToThread(qApp->thread()); connect(&d->updateCompressor, SIGNAL(timeout()), this, SLOT(forwardCompressedUdpate())); } KoShapeManager::KoShapeManager(KoCanvasBase *canvas) : d(new Private(this, canvas)) { Q_ASSERT(d->canvas); // not optional. connect(d->selection, SIGNAL(selectionChanged()), this, SIGNAL(selectionChanged())); // see a comment in another constructor this->moveToThread(qApp->thread()); connect(&d->updateCompressor, SIGNAL(timeout()), this, SLOT(forwardCompressedUdpate())); } void KoShapeManager::Private::unlinkFromShapesRecursively(const QList &shapes) { Q_FOREACH (KoShape *shape, shapes) { shape->priv()->removeShapeManager(q); KoShapeContainer *container = dynamic_cast(shape); if (container) { unlinkFromShapesRecursively(container->shapes()); } } } KoShapeManager::~KoShapeManager() { d->unlinkFromShapesRecursively(d->shapes); d->shapes.clear(); delete d; } void KoShapeManager::setShapes(const QList &shapes, Repaint repaint) { { QMutexLocker l1(&d->shapesMutex); QMutexLocker l2(&d->treeMutex); //clear selection d->selection->deselectAll(); d->unlinkFromShapesRecursively(d->shapes); d->compressedUpdate = QRect(); d->compressedUpdatedShapes.clear(); d->aggregate4update.clear(); d->shapeIndexesBeforeUpdate.clear(); d->tree.clear(); d->shapes.clear(); } Q_FOREACH (KoShape *shape, shapes) { addShape(shape, repaint); } } void KoShapeManager::addShape(KoShape *shape, Repaint repaint) { { QMutexLocker l1(&d->shapesMutex); if (d->shapes.contains(shape)) return; shape->priv()->addShapeManager(this); d->shapes.append(shape); if (d->shapeUsedInRenderingTree(shape)) { QMutexLocker l2(&d->treeMutex); QRectF br(shape->boundingRect()); d->tree.insert(br, shape); } } if (repaint == PaintShapeOnAdd) { shape->update(); } // add the children of a KoShapeContainer KoShapeContainer *container = dynamic_cast(shape); if (container) { foreach (KoShape *containerShape, container->shapes()) { addShape(containerShape, repaint); } } } void KoShapeManager::remove(KoShape *shape) { QRectF dirtyRect; { QMutexLocker l1(&d->shapesMutex); QMutexLocker l2(&d->treeMutex); dirtyRect = shape->absoluteOutlineRect(); shape->priv()->removeShapeManager(this); d->selection->deselect(shape); d->aggregate4update.remove(shape); d->compressedUpdatedShapes.remove(shape); if (d->shapeUsedInRenderingTree(shape)) { d->tree.remove(shape); } d->shapes.removeAll(shape); } if (!dirtyRect.isEmpty()) { d->canvas->updateCanvas(dirtyRect); } // remove the children of a KoShapeContainer KoShapeContainer *container = dynamic_cast(shape); if (container) { foreach (KoShape *containerShape, container->shapes()) { remove(containerShape); } } } KoShapeManager::ShapeInterface::ShapeInterface(KoShapeManager *_q) : q(_q) { } void KoShapeManager::ShapeInterface::notifyShapeDestructed(KoShape *shape) { QMutexLocker l1(&q->d->shapesMutex); QMutexLocker l2(&q->d->treeMutex); q->d->selection->deselect(shape); q->d->aggregate4update.remove(shape); q->d->compressedUpdatedShapes.remove(shape); // we cannot access RTTI of the semi-destructed shape, so just // unlink it lazily if (q->d->tree.contains(shape)) { q->d->tree.remove(shape); } q->d->shapes.removeAll(shape); } KoShapeManager::ShapeInterface *KoShapeManager::shapeInterface() { return &d->shapeInterface; } void KoShapeManager::paint(QPainter &painter, const KoViewConverter &converter, bool forPrint) { d->updateTree(); QMutexLocker l1(&d->shapesMutex); painter.setPen(Qt::NoPen); // painters by default have a black stroke, lets turn that off. painter.setBrush(Qt::NoBrush); QList unsortedShapes; if (painter.hasClipping()) { QMutexLocker l(&d->treeMutex); QRectF rect = converter.viewToDocument(KisPaintingTweaks::safeClipBoundingRect(painter)); unsortedShapes = d->tree.intersects(rect); } else { unsortedShapes = d->shapes; warnFlake << "KoShapeManager::paint Painting with a painter that has no clipping will lead to too much being painted!"; } // filter all hidden shapes from the list // also filter shapes with a parent which has filter effects applied QList sortedShapes; foreach (KoShape *shape, unsortedShapes) { if (!shape->isVisible()) continue; bool addShapeToList = true; // check if one of the shapes ancestors have filter effects KoShapeContainer *parent = shape->parent(); while (parent) { // parent must be part of the shape manager to be taken into account if (!d->shapes.contains(parent)) break; if (parent->filterEffectStack() && !parent->filterEffectStack()->isEmpty()) { addShapeToList = false; break; } parent = parent->parent(); } if (addShapeToList) { sortedShapes.append(shape); } else if (parent) { sortedShapes.append(parent); } } std::sort(sortedShapes.begin(), sortedShapes.end(), KoShape::compareShapeZIndex); KoShapePaintingContext paintContext(d->canvas, forPrint); //FIXME foreach (KoShape *shape, sortedShapes) { renderSingleShape(shape, painter, converter, paintContext); } #ifdef CALLIGRA_RTREE_DEBUG // paint tree qreal zx = 0; qreal zy = 0; converter.zoom(&zx, &zy); painter.save(); painter.scale(zx, zy); d->tree.paint(painter); painter.restore(); #endif if (! forPrint) { KoShapePaintingContext paintContext(d->canvas, forPrint); //FIXME d->selection->paint(painter, converter, paintContext); } } void KoShapeManager::renderSingleShape(KoShape *shape, QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintContext) { KisQPainterStateSaver saver(&painter); // apply shape clipping KoClipPath::applyClipping(shape, painter, converter); // apply transformation painter.setTransform(shape->absoluteTransformation(&converter) * painter.transform()); // paint the shape paintShape(shape, painter, converter, paintContext); } void KoShapeManager::paintShape(KoShape *shape, QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintContext) { qreal transparency = shape->transparency(true); if (transparency > 0.0) { painter.setOpacity(1.0-transparency); } if (shape->shadow()) { painter.save(); shape->shadow()->paint(shape, painter, converter); painter.restore(); } if (!shape->filterEffectStack() || shape->filterEffectStack()->isEmpty()) { QScopedPointer clipMaskPainter; QPainter *shapePainter = &painter; KoClipMask *clipMask = shape->clipMask(); if (clipMask) { clipMaskPainter.reset(new KoClipMaskPainter(&painter, shape->boundingRect())); shapePainter = clipMaskPainter->shapePainter(); } /** * We expect the shape to save/restore the painter's state itself. Such design was not * not always here, so we need a period of sanity checks to ensure all the shapes are * ported correctly. */ const QTransform sanityCheckTransformSaved = shapePainter->transform(); shape->paint(*shapePainter, converter, paintContext); shape->paintStroke(*shapePainter, converter, paintContext); KIS_SAFE_ASSERT_RECOVER(shapePainter->transform() == sanityCheckTransformSaved) { shapePainter->setTransform(sanityCheckTransformSaved); } if (clipMask) { shape->clipMask()->drawMask(clipMaskPainter->maskPainter(), shape); clipMaskPainter->renderOnGlobalPainter(); } } else { // TODO: clipping mask is not implemented for this case! // There are filter effects, then we need to prerender the shape on an image, to filter it QRectF shapeBound(QPointF(), shape->size()); // First step, compute the rectangle used for the image QRectF clipRegion = shape->filterEffectStack()->clipRectForBoundingRect(shapeBound); // convert clip region to view coordinates QRectF zoomedClipRegion = converter.documentToView(clipRegion); // determine the offset of the clipping rect from the shapes origin QPointF clippingOffset = zoomedClipRegion.topLeft(); // Initialize the buffer image QImage sourceGraphic(zoomedClipRegion.size().toSize(), QImage::Format_ARGB32_Premultiplied); sourceGraphic.fill(qRgba(0,0,0,0)); QHash imageBuffers; QSet requiredStdInputs = shape->filterEffectStack()->requiredStandarsInputs(); if (requiredStdInputs.contains("SourceGraphic") || requiredStdInputs.contains("SourceAlpha")) { // Init the buffer painter QPainter imagePainter(&sourceGraphic); imagePainter.translate(-1.0f*clippingOffset); imagePainter.setPen(Qt::NoPen); imagePainter.setBrush(Qt::NoBrush); imagePainter.setRenderHint(QPainter::Antialiasing, painter.testRenderHint(QPainter::Antialiasing)); // Paint the shape on the image KoShapeGroup *group = dynamic_cast(shape); if (group) { // the childrens matrix contains the groups matrix as well // so we have to compensate for that before painting the children imagePainter.setTransform(group->absoluteTransformation(&converter).inverted(), true); Private::paintGroup(group, imagePainter, converter, paintContext); } else { imagePainter.save(); shape->paint(imagePainter, converter, paintContext); shape->paintStroke(imagePainter, converter, paintContext); imagePainter.restore(); imagePainter.end(); } } if (requiredStdInputs.contains("SourceAlpha")) { QImage sourceAlpha = sourceGraphic; sourceAlpha.fill(qRgba(0,0,0,255)); sourceAlpha.setAlphaChannel(sourceGraphic.alphaChannel()); imageBuffers.insert("SourceAlpha", sourceAlpha); } if (requiredStdInputs.contains("FillPaint")) { QImage fillPaint = sourceGraphic; if (shape->background()) { QPainter fillPainter(&fillPaint); QPainterPath fillPath; fillPath.addRect(fillPaint.rect().adjusted(-1,-1,1,1)); shape->background()->paint(fillPainter, converter, paintContext, fillPath); } else { fillPaint.fill(qRgba(0,0,0,0)); } imageBuffers.insert("FillPaint", fillPaint); } imageBuffers.insert("SourceGraphic", sourceGraphic); imageBuffers.insert(QString(), sourceGraphic); KoFilterEffectRenderContext renderContext(converter); renderContext.setShapeBoundingBox(shapeBound); QImage result; QList filterEffects = shape->filterEffectStack()->filterEffects(); // Filter foreach (KoFilterEffect *filterEffect, filterEffects) { QRectF filterRegion = filterEffect->filterRectForBoundingRect(shapeBound); filterRegion = converter.documentToView(filterRegion); QRect subRegion = filterRegion.translated(-clippingOffset).toRect(); // set current filter region renderContext.setFilterRegion(subRegion & sourceGraphic.rect()); if (filterEffect->maximalInputCount() <= 1) { QList inputs = filterEffect->inputs(); QString input = inputs.count() ? inputs.first() : QString(); // get input image from image buffers and apply the filter effect QImage image = imageBuffers.value(input); if (!image.isNull()) { result = filterEffect->processImage(imageBuffers.value(input), renderContext); } } else { QList inputImages; Q_FOREACH (const QString &input, filterEffect->inputs()) { QImage image = imageBuffers.value(input); if (!image.isNull()) inputImages.append(imageBuffers.value(input)); } // apply the filter effect if (filterEffect->inputs().count() == inputImages.count()) result = filterEffect->processImages(inputImages, renderContext); } // store result of effect imageBuffers.insert(filterEffect->output(), result); } KoFilterEffect *lastEffect = filterEffects.last(); // Paint the result painter.save(); painter.drawImage(clippingOffset, imageBuffers.value(lastEffect->output())); painter.restore(); } } KoShape *KoShapeManager::shapeAt(const QPointF &position, KoFlake::ShapeSelection selection, bool omitHiddenShapes) { d->updateTree(); QMutexLocker l(&d->shapesMutex); QList sortedShapes; { QMutexLocker l(&d->treeMutex); sortedShapes = d->tree.contains(position); } std::sort(sortedShapes.begin(), sortedShapes.end(), KoShape::compareShapeZIndex); KoShape *firstUnselectedShape = 0; for (int count = sortedShapes.count() - 1; count >= 0; count--) { KoShape *shape = sortedShapes.at(count); if (omitHiddenShapes && ! shape->isVisible()) continue; if (! shape->hitTest(position)) continue; switch (selection) { case KoFlake::ShapeOnTop: if (shape->isSelectable()) return shape; break; case KoFlake::Selected: if (d->selection->isSelected(shape)) return shape; break; case KoFlake::Unselected: if (! d->selection->isSelected(shape)) return shape; break; case KoFlake::NextUnselected: // we want an unselected shape if (d->selection->isSelected(shape)) continue; // memorize the first unselected shape if (! firstUnselectedShape) firstUnselectedShape = shape; // check if the shape above is selected if (count + 1 < sortedShapes.count() && d->selection->isSelected(sortedShapes.at(count + 1))) return shape; break; } } // if we want the next unselected below a selected but there was none selected, // return the first found unselected shape if (selection == KoFlake::NextUnselected && firstUnselectedShape) return firstUnselectedShape; if (d->selection->hitTest(position)) return d->selection; return 0; // missed everything } QList KoShapeManager::shapesAt(const QRectF &rect, bool omitHiddenShapes, bool containedMode) { QMutexLocker l(&d->shapesMutex); d->updateTree(); QList shapes; { QMutexLocker l(&d->treeMutex); shapes = containedMode ? d->tree.contained(rect) : d->tree.intersects(rect); } for (int count = shapes.count() - 1; count >= 0; count--) { KoShape *shape = shapes.at(count); if (omitHiddenShapes && !shape->isVisible()) { shapes.removeAt(count); } else { const QPainterPath outline = shape->absoluteTransformation(0).map(shape->outline()); if (!containedMode && !outline.intersects(rect) && !outline.contains(rect)) { shapes.removeAt(count); } else if (containedMode) { QPainterPath containingPath; containingPath.addRect(rect); if (!containingPath.contains(outline)) { shapes.removeAt(count); } } } } return shapes; } void KoShapeManager::update(const QRectF &rect, const KoShape *shape, bool selectionHandles) { if (d->updatesBlocked) return; { QMutexLocker l(&d->shapesMutex); d->compressedUpdate |= rect; if (selectionHandles) { d->compressedUpdatedShapes.insert(shape); } } d->updateCompressor.start(); } void KoShapeManager::setUpdatesBlocked(bool value) { d->updatesBlocked = value; } bool KoShapeManager::updatesBlocked() const { return d->updatesBlocked; } void KoShapeManager::notifyShapeChanged(KoShape *shape) { { QMutexLocker l(&d->treeMutex); Q_ASSERT(shape); if (d->aggregate4update.contains(shape)) { return; } d->aggregate4update.insert(shape); d->shapeIndexesBeforeUpdate.insert(shape, shape->zIndex()); } KoShapeContainer *container = dynamic_cast(shape); if (container) { Q_FOREACH (KoShape *child, container->shapes()) notifyShapeChanged(child); } } QList KoShapeManager::shapes() const { QMutexLocker l(&d->shapesMutex); return d->shapes; } QList KoShapeManager::topLevelShapes() const { QMutexLocker l(&d->shapesMutex); QList shapes; // get all toplevel shapes Q_FOREACH (KoShape *shape, d->shapes) { if (!shape->parent() || dynamic_cast(shape->parent())) { shapes.append(shape); } } return shapes; } KoSelection *KoShapeManager::selection() const { return d->selection; } KoCanvasBase *KoShapeManager::canvas() { return d->canvas; } //have to include this because of Q_PRIVATE_SLOT #include "moc_KoShapeManager.cpp" diff --git a/libs/flake/KoShapeShadow.cpp b/libs/flake/KoShapeShadow.cpp index 299759097a..df346d7e98 100644 --- a/libs/flake/KoShapeShadow.cpp +++ b/libs/flake/KoShapeShadow.cpp @@ -1,357 +1,358 @@ /* This file is part of the KDE project * Copyright (C) 2008-2009 Jan Hambrecht * Copyright (C) 2010 Thomas Zander * Copyright (C) 2010 Ariya Hidayat * Copyright (C) 2010-2011 Yue Liu * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "KoShapeShadow.h" #include "KoShapeGroup.h" #include "KoSelection.h" #include "KoShapeSavingContext.h" #include "KoShapeStrokeModel.h" #include "KoShape.h" #include "KoInsets.h" #include "KoPathShape.h" #include #include #include #include +#include #include #include #include class Q_DECL_HIDDEN KoShapeShadow::Private { public: Private() : offset(2, 2), color(Qt::black), blur(8), visible(true), refCount(0) { } QPointF offset; QColor color; qreal blur; bool visible; QAtomicInt refCount; /** * Paints the shadow of the shape group to the buffer image. * @param group the shape group to paint around * @param painter the painter to paint on the image * @param converter to convert between internal and view coordinates. */ void paintGroupShadow(KoShapeGroup *group, QPainter &painter, const KoViewConverter &converter); /** * Paints the shadow of the shape to the buffer image. * @param shape the shape to paint around * @param painter the painter to paint on the image * @param converter to convert between internal and view coordinates. */ void paintShadow(KoShape *shape, QPainter &painter, const KoViewConverter &converter); void blurShadow(QImage &image, int radius, const QColor& shadowColor); }; void KoShapeShadow::Private::paintGroupShadow(KoShapeGroup *group, QPainter &painter, const KoViewConverter &converter) { QList shapes = group->shapes(); Q_FOREACH (KoShape *child, shapes) { // we paint recursively here, so we do not have to check recursively for visibility if (!child->isVisible(false)) continue; painter.save(); //apply group child's transformation painter.setTransform(child->absoluteTransformation(&converter), true); paintShadow(child, painter, converter); painter.restore(); } } void KoShapeShadow::Private::paintShadow(KoShape *shape, QPainter &painter, const KoViewConverter &converter) { QPainterPath path(shape->shadowOutline()); if (!path.isEmpty()) { painter.save(); KoShape::applyConversion(painter, converter); painter.setBrush(QBrush(color)); // Make sure the shadow has the same fill rule as the shape. KoPathShape * pathShape = dynamic_cast(shape); if (pathShape) path.setFillRule(pathShape->fillRule()); painter.drawPath(path); painter.restore(); } if (shape->stroke()) { shape->stroke()->paint(shape, painter, converter); } } /* You can also find a BSD version to this method from * http://gitorious.org/ofi-labs/x2/blobs/master/graphics/shadowblur/ */ void KoShapeShadow::Private::blurShadow(QImage &image, int radius, const QColor& shadowColor) { static const int BlurSumShift = 15; // Check http://www.w3.org/TR/SVG/filters.html# // As noted in the SVG filter specification, ru // approximates a real gaussian blur nicely. // See comments in http://webkit.org/b/40793, it seems sensible // to follow Skia's limit of 128 pixels for the blur radius. if (radius > 128) radius = 128; int channels[4] = { 3, 0, 1, 3 }; int dmax = radius >> 1; int dmin = dmax - 1 + (radius & 1); if (dmin < 0) dmin = 0; // Two stages: horizontal and vertical for (int k = 0; k < 2; ++k) { unsigned char* pixels = image.bits(); int stride = (k == 0) ? 4 : image.bytesPerLine(); int delta = (k == 0) ? image.bytesPerLine() : 4; int jfinal = (k == 0) ? image.height() : image.width(); int dim = (k == 0) ? image.width() : image.height(); for (int j = 0; j < jfinal; ++j, pixels += delta) { // For each step, we blur the alpha in a channel and store the result // in another channel for the subsequent step. // We use sliding window algorithm to accumulate the alpha values. // This is much more efficient than computing the sum of each pixels // covered by the box kernel size for each x. for (int step = 0; step < 3; ++step) { int side1 = (step == 0) ? dmin : dmax; int side2 = (step == 1) ? dmin : dmax; int pixelCount = side1 + 1 + side2; int invCount = ((1 << BlurSumShift) + pixelCount - 1) / pixelCount; int ofs = 1 + side2; int alpha1 = pixels[channels[step]]; int alpha2 = pixels[(dim - 1) * stride + channels[step]]; unsigned char* ptr = pixels + channels[step + 1]; unsigned char* prev = pixels + stride + channels[step]; unsigned char* next = pixels + ofs * stride + channels[step]; int i; int sum = side1 * alpha1 + alpha1; int limit = (dim < side2 + 1) ? dim : side2 + 1; for (i = 1; i < limit; ++i, prev += stride) sum += *prev; if (limit <= side2) sum += (side2 - limit + 1) * alpha2; limit = (side1 < dim) ? side1 : dim; for (i = 0; i < limit; ptr += stride, next += stride, ++i, ++ofs) { *ptr = (sum * invCount) >> BlurSumShift; sum += ((ofs < dim) ? *next : alpha2) - alpha1; } prev = pixels + channels[step]; for (; ofs < dim; ptr += stride, prev += stride, next += stride, ++i, ++ofs) { *ptr = (sum * invCount) >> BlurSumShift; sum += (*next) - (*prev); } for (; i < dim; ptr += stride, prev += stride, ++i) { *ptr = (sum * invCount) >> BlurSumShift; sum += alpha2 - (*prev); } } } } // "Colorize" with the right shadow color. QPainter p(&image); p.setCompositionMode(QPainter::CompositionMode_SourceIn); p.fillRect(image.rect(), shadowColor); p.end(); } // ---------------------------------------------------------------- // KoShapeShadow KoShapeShadow::KoShapeShadow() : d(new Private()) { } KoShapeShadow::~KoShapeShadow() { delete d; } KoShapeShadow::KoShapeShadow(const KoShapeShadow &rhs) : d(new Private(*rhs.d)) { d->refCount = 0; } KoShapeShadow& KoShapeShadow::operator=(const KoShapeShadow &rhs) { *d = *rhs.d; d->refCount = 0; return *this; } void KoShapeShadow::fillStyle(KoGenStyle &style, KoShapeSavingContext &context) { Q_UNUSED(context); style.addProperty("draw:shadow", d->visible ? "visible" : "hidden", KoGenStyle::GraphicType); style.addProperty("draw:shadow-color", d->color.name(), KoGenStyle::GraphicType); if (d->color.alphaF() != 1.0) style.addProperty("draw:shadow-opacity", QString("%1%").arg(d->color.alphaF() * 100.0), KoGenStyle::GraphicType); style.addProperty("draw:shadow-offset-x", QString("%1pt").arg(d->offset.x()), KoGenStyle::GraphicType); style.addProperty("draw:shadow-offset-y", QString("%1pt").arg(d->offset.y()), KoGenStyle::GraphicType); if (d->blur != 0) style.addProperty("calligra:shadow-blur-radius", QString("%1pt").arg(d->blur), KoGenStyle::GraphicType); } void KoShapeShadow::paint(KoShape *shape, QPainter &painter, const KoViewConverter &converter) { if (! d->visible) return; // So the approach we are taking here is to draw into a buffer image the size of boundingRect // We offset by the shadow offset at the time we draw into the buffer // Then we filter the image and draw it at the position of the bounding rect on canvas //the boundingRect of the shape or the KoSelection boundingRect of the group QRectF shadowRect = shape->boundingRect(); QRectF zoomedClipRegion = converter.documentToView(shadowRect); // Init the buffer image QImage sourceGraphic(zoomedClipRegion.size().toSize(), QImage::Format_ARGB32_Premultiplied); sourceGraphic.fill(qRgba(0,0,0,0)); // Init the buffer painter QPainter imagePainter(&sourceGraphic); imagePainter.setPen(Qt::NoPen); imagePainter.setBrush(Qt::NoBrush); imagePainter.setRenderHint(QPainter::Antialiasing, painter.testRenderHint(QPainter::Antialiasing)); // Since our imagebuffer and the canvas don't align we need to offset our drawings imagePainter.translate(-1.0f*converter.documentToView(shadowRect.topLeft())); // Handle the shadow offset imagePainter.translate(converter.documentToView(offset())); KoShapeGroup *group = dynamic_cast(shape); if (group) { d->paintGroupShadow(group, imagePainter, converter); } else { //apply shape's transformation imagePainter.setTransform(shape->absoluteTransformation(&converter), true); d->paintShadow(shape, imagePainter, converter); } imagePainter.end(); // Blur the shadow (well the entire buffer) d->blurShadow(sourceGraphic, converter.documentToViewX(d->blur), d->color); // Paint the result painter.save(); // The painter is initialized for us with canvas transform 'plus' shape transform // we are only interested in the canvas transform so 'subtract' the shape transform part painter.setTransform(shape->absoluteTransformation(&converter).inverted() * painter.transform()); painter.drawImage(zoomedClipRegion.topLeft(), sourceGraphic); painter.restore(); } void KoShapeShadow::setOffset(const QPointF & offset) { d->offset = offset; } QPointF KoShapeShadow::offset() const { return d->offset; } void KoShapeShadow::setColor(const QColor &color) { d->color = color; } QColor KoShapeShadow::color() const { return d->color; } void KoShapeShadow::setBlur(qreal blur) { // force positive blur radius d->blur = qAbs(blur); } qreal KoShapeShadow::blur() const { return d->blur; } void KoShapeShadow::setVisible(bool visible) { d->visible = visible; } bool KoShapeShadow::isVisible() const { return d->visible; } void KoShapeShadow::insets(KoInsets &insets) const { if (!d->visible) { insets.top = 0; insets.bottom = 0; insets.left = 0; insets.right = 0; return; } qreal expand = d->blur; insets.left = (d->offset.x() < 0.0) ? qAbs(d->offset.x()) : 0.0; insets.top = (d->offset.y() < 0.0) ? qAbs(d->offset.y()) : 0.0; insets.right = (d->offset.x() > 0.0) ? d->offset.x() : 0.0; insets.bottom = (d->offset.y() > 0.0) ? d->offset.y() : 0.0; insets.left += expand; insets.top += expand; insets.right += expand; insets.bottom += expand; } bool KoShapeShadow::ref() { return d->refCount.ref(); } bool KoShapeShadow::deref() { return d->refCount.deref(); } int KoShapeShadow::useCount() const { return d->refCount; } diff --git a/libs/flake/KoSnapGuide.cpp b/libs/flake/KoSnapGuide.cpp index da7f7709a5..0d8e5a9f73 100644 --- a/libs/flake/KoSnapGuide.cpp +++ b/libs/flake/KoSnapGuide.cpp @@ -1,281 +1,282 @@ /* This file is part of the KDE project * Copyright (C) 2008-2009 Jan Hambrecht * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "KoSnapGuide.h" #include "KoSnapProxy.h" #include "KoSnapStrategy.h" #include #include #include #include #include +#include #include template inline QSharedPointer toQShared(T* ptr) { return QSharedPointer(ptr); } class Q_DECL_HIDDEN KoSnapGuide::Private { public: Private(KoCanvasBase *parentCanvas) : canvas(parentCanvas), additionalEditedShape(0), currentStrategy(0), active(true), snapDistance(10) { } ~Private() { strategies.clear(); } KoCanvasBase *canvas; KoShape *additionalEditedShape; typedef QSharedPointer KoSnapStrategySP; typedef QList StrategiesList; StrategiesList strategies; KoSnapStrategySP currentStrategy; KoSnapGuide::Strategies usedStrategies; bool active; int snapDistance; QList ignoredPoints; QList ignoredShapes; }; KoSnapGuide::KoSnapGuide(KoCanvasBase *canvas) : d(new Private(canvas)) { d->strategies.append(toQShared(new GridSnapStrategy())); d->strategies.append(toQShared(new NodeSnapStrategy())); d->strategies.append(toQShared(new OrthogonalSnapStrategy())); d->strategies.append(toQShared(new ExtensionSnapStrategy())); d->strategies.append(toQShared(new IntersectionSnapStrategy())); d->strategies.append(toQShared(new BoundingBoxSnapStrategy())); } KoSnapGuide::~KoSnapGuide() { } void KoSnapGuide::setAdditionalEditedShape(KoShape *shape) { d->additionalEditedShape = shape; } KoShape *KoSnapGuide::additionalEditedShape() const { return d->additionalEditedShape; } void KoSnapGuide::enableSnapStrategy(Strategy type, bool value) { if (value) { d->usedStrategies |= type; } else { d->usedStrategies &= ~type; } } bool KoSnapGuide::isStrategyEnabled(Strategy type) const { return d->usedStrategies & type; } void KoSnapGuide::enableSnapStrategies(Strategies strategies) { d->usedStrategies = strategies; } KoSnapGuide::Strategies KoSnapGuide::enabledSnapStrategies() const { return d->usedStrategies; } bool KoSnapGuide::addCustomSnapStrategy(KoSnapStrategy *customStrategy) { if (!customStrategy || customStrategy->type() != CustomSnapping) return false; d->strategies.append(toQShared(customStrategy)); return true; } void KoSnapGuide::overrideSnapStrategy(Strategy type, KoSnapStrategy *strategy) { for (auto it = d->strategies.begin(); it != d->strategies.end(); /*noop*/) { if ((*it)->type() == type) { if (strategy) { *it = toQShared(strategy); } else { it = d->strategies.erase(it); } return; } else { ++it; } } if (strategy) { d->strategies.append(toQShared(strategy)); } } void KoSnapGuide::enableSnapping(bool on) { d->active = on; } bool KoSnapGuide::isSnapping() const { return d->active; } void KoSnapGuide::setSnapDistance(int distance) { d->snapDistance = qAbs(distance); } int KoSnapGuide::snapDistance() const { return d->snapDistance; } QPointF KoSnapGuide::snap(const QPointF &mousePosition, const QPointF &dragOffset, Qt::KeyboardModifiers modifiers) { QPointF pos = mousePosition + dragOffset; pos = snap(pos, modifiers); return pos - dragOffset; } QPointF KoSnapGuide::snap(const QPointF &mousePosition, Qt::KeyboardModifiers modifiers) { d->currentStrategy.clear(); if (! d->active || (modifiers & Qt::ShiftModifier)) return mousePosition; KoSnapProxy proxy(this); qreal minDistance = HUGE_VAL; qreal maxSnapDistance = d->canvas->viewConverter()->viewToDocument(QSizeF(d->snapDistance, d->snapDistance)).width(); foreach (Private::KoSnapStrategySP strategy, d->strategies) { if (d->usedStrategies & strategy->type() || strategy->type() == GridSnapping || strategy->type() == CustomSnapping) { if (! strategy->snap(mousePosition, &proxy, maxSnapDistance)) continue; QPointF snapCandidate = strategy->snappedPosition(); qreal distance = KoSnapStrategy::squareDistance(snapCandidate, mousePosition); if (distance < minDistance) { d->currentStrategy = strategy; minDistance = distance; } } } if (! d->currentStrategy) return mousePosition; return d->currentStrategy->snappedPosition(); } QRectF KoSnapGuide::boundingRect() { QRectF rect; if (d->currentStrategy) { rect = d->currentStrategy->decoration(*d->canvas->viewConverter()).boundingRect(); return rect.adjusted(-2, -2, 2, 2); } else { return rect; } } void KoSnapGuide::paint(QPainter &painter, const KoViewConverter &converter) { if (! d->currentStrategy || ! d->active) return; QPainterPath decoration = d->currentStrategy->decoration(converter); painter.setBrush(Qt::NoBrush); QPen whitePen(Qt::white, 0); whitePen.setStyle(Qt::SolidLine); painter.setPen(whitePen); painter.drawPath(decoration); QPen redPen(Qt::red, 0); redPen.setStyle(Qt::DotLine); painter.setPen(redPen); painter.drawPath(decoration); } KoCanvasBase *KoSnapGuide::canvas() const { return d->canvas; } void KoSnapGuide::setIgnoredPathPoints(const QList &ignoredPoints) { d->ignoredPoints = ignoredPoints; } QList KoSnapGuide::ignoredPathPoints() const { return d->ignoredPoints; } void KoSnapGuide::setIgnoredShapes(const QList &ignoredShapes) { d->ignoredShapes = ignoredShapes; } QList KoSnapGuide::ignoredShapes() const { return d->ignoredShapes; } void KoSnapGuide::reset() { d->currentStrategy.clear(); d->additionalEditedShape = 0; d->ignoredPoints.clear(); d->ignoredShapes.clear(); // remove all custom strategies int strategyCount = d->strategies.count(); for (int i = strategyCount-1; i >= 0; --i) { if (d->strategies[i]->type() == CustomSnapping) { d->strategies.removeAt(i); } } } diff --git a/libs/flake/KoSnapStrategy.cpp b/libs/flake/KoSnapStrategy.cpp index cab1d09b57..8b09b1070d 100644 --- a/libs/flake/KoSnapStrategy.cpp +++ b/libs/flake/KoSnapStrategy.cpp @@ -1,641 +1,642 @@ /* This file is part of the KDE project * Copyright (C) 2008 Jan Hambrecht * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "KoSnapStrategy.h" #include "KoSnapProxy.h" #include "KoSnapGuide.h" #include #include #include #include #include #include +#include #include #if defined(_MSC_VER) && (_MSC_VER < 1800) #define isfinite(x) (double)(x) #endif KoSnapStrategy::KoSnapStrategy(KoSnapGuide::Strategy type) : m_snapType(type) { } QPointF KoSnapStrategy::snappedPosition() const { return m_snappedPosition; } void KoSnapStrategy::setSnappedPosition(const QPointF &position) { m_snappedPosition = position; } KoSnapGuide::Strategy KoSnapStrategy::type() const { return m_snapType; } qreal KoSnapStrategy::squareDistance(const QPointF &p1, const QPointF &p2) { const qreal dx = p1.x() - p2.x(); const qreal dy = p1.y() - p2.y(); return dx*dx + dy*dy; } qreal KoSnapStrategy::scalarProduct(const QPointF &p1, const QPointF &p2) { return p1.x() * p2.x() + p1.y() * p2.y(); } OrthogonalSnapStrategy::OrthogonalSnapStrategy() : KoSnapStrategy(KoSnapGuide::OrthogonalSnapping) { } bool OrthogonalSnapStrategy::snap(const QPointF &mousePosition, KoSnapProxy * proxy, qreal maxSnapDistance) { Q_ASSERT(std::isfinite(maxSnapDistance)); QPointF horzSnap, vertSnap; qreal minVertDist = HUGE_VAL; qreal minHorzDist = HUGE_VAL; QList shapes = proxy->shapes(true); Q_FOREACH (KoShape * shape, shapes) { QList points = proxy->pointsFromShape(shape); foreach (const QPointF &point, points) { qreal dx = fabs(point.x() - mousePosition.x()); if (dx < minHorzDist && dx < maxSnapDistance) { minHorzDist = dx; horzSnap = point; } qreal dy = fabs(point.y() - mousePosition.y()); if (dy < minVertDist && dy < maxSnapDistance) { minVertDist = dy; vertSnap = point; } } } QPointF snappedPoint = mousePosition; if (minHorzDist < HUGE_VAL) snappedPoint.setX(horzSnap.x()); if (minVertDist < HUGE_VAL) snappedPoint.setY(vertSnap.y()); if (minHorzDist < HUGE_VAL) m_hLine = QLineF(horzSnap, snappedPoint); else m_hLine = QLineF(); if (minVertDist < HUGE_VAL) m_vLine = QLineF(vertSnap, snappedPoint); else m_vLine = QLineF(); setSnappedPosition(snappedPoint); return (minHorzDist < HUGE_VAL || minVertDist < HUGE_VAL); } QPainterPath OrthogonalSnapStrategy::decoration(const KoViewConverter &/*converter*/) const { QPainterPath decoration; if (! m_hLine.isNull()) { decoration.moveTo(m_hLine.p1()); decoration.lineTo(m_hLine.p2()); } if (! m_vLine.isNull()) { decoration.moveTo(m_vLine.p1()); decoration.lineTo(m_vLine.p2()); } return decoration; } NodeSnapStrategy::NodeSnapStrategy() : KoSnapStrategy(KoSnapGuide::NodeSnapping) { } bool NodeSnapStrategy::snap(const QPointF &mousePosition, KoSnapProxy * proxy, qreal maxSnapDistance) { Q_ASSERT(std::isfinite(maxSnapDistance)); const qreal maxDistance = maxSnapDistance * maxSnapDistance; qreal minDistance = HUGE_VAL; QRectF rect(-maxSnapDistance, -maxSnapDistance, maxSnapDistance, maxSnapDistance); rect.moveCenter(mousePosition); QList points = proxy->pointsInRect(rect, false); QPointF snappedPoint = mousePosition; foreach (const QPointF &point, points) { qreal distance = squareDistance(mousePosition, point); if (distance < maxDistance && distance < minDistance) { snappedPoint = point; minDistance = distance; } } setSnappedPosition(snappedPoint); return (minDistance < HUGE_VAL); } QPainterPath NodeSnapStrategy::decoration(const KoViewConverter &converter) const { QRectF unzoomedRect = converter.viewToDocument(QRectF(0, 0, 11, 11)); unzoomedRect.moveCenter(snappedPosition()); QPainterPath decoration; decoration.addEllipse(unzoomedRect); return decoration; } ExtensionSnapStrategy::ExtensionSnapStrategy() : KoSnapStrategy(KoSnapGuide::ExtensionSnapping) { } bool ExtensionSnapStrategy::snap(const QPointF &mousePosition, KoSnapProxy * proxy, qreal maxSnapDistance) { Q_ASSERT(std::isfinite(maxSnapDistance)); const qreal maxDistance = maxSnapDistance * maxSnapDistance; qreal minDistances[2] = { HUGE_VAL, HUGE_VAL }; QPointF snappedPoints[2] = { mousePosition, mousePosition }; QPointF startPoints[2]; QList shapes = proxy->shapes(true); Q_FOREACH (KoShape * shape, shapes) { KoPathShape * path = dynamic_cast(shape); if (! path) { continue; } QTransform matrix = path->absoluteTransformation(0); const int subpathCount = path->subpathCount(); for (int subpathIndex = 0; subpathIndex < subpathCount; ++subpathIndex) { if (path->isClosedSubpath(subpathIndex)) continue; int pointCount = path->subpathPointCount(subpathIndex); // check the extension from the start point KoPathPoint * first = path->pointByIndex(KoPathPointIndex(subpathIndex, 0)); QPointF firstSnapPosition = mousePosition; if (snapToExtension(firstSnapPosition, first, matrix)) { qreal distance = squareDistance(firstSnapPosition, mousePosition); if (distance < maxDistance) { if (distance < minDistances[0]) { minDistances[1] = minDistances[0]; snappedPoints[1] = snappedPoints[0]; startPoints[1] = startPoints[0]; minDistances[0] = distance; snappedPoints[0] = firstSnapPosition; startPoints[0] = matrix.map(first->point()); } else if (distance < minDistances[1]) { minDistances[1] = distance; snappedPoints[1] = firstSnapPosition; startPoints[1] = matrix.map(first->point()); } } } // now check the extension from the last point KoPathPoint * last = path->pointByIndex(KoPathPointIndex(subpathIndex, pointCount - 1)); QPointF lastSnapPosition = mousePosition; if (snapToExtension(lastSnapPosition, last, matrix)) { qreal distance = squareDistance(lastSnapPosition, mousePosition); if (distance < maxDistance) { if (distance < minDistances[0]) { minDistances[1] = minDistances[0]; snappedPoints[1] = snappedPoints[0]; startPoints[1] = startPoints[0]; minDistances[0] = distance; snappedPoints[0] = lastSnapPosition; startPoints[0] = matrix.map(last->point()); } else if (distance < minDistances[1]) { minDistances[1] = distance; snappedPoints[1] = lastSnapPosition; startPoints[1] = matrix.map(last->point()); } } } } } m_lines.clear(); // if we have to extension near our mouse position, they might have an intersection // near our mouse position which we want to use as the snapped position if (minDistances[0] < HUGE_VAL && minDistances[1] < HUGE_VAL) { // check if intersection of extension lines is near mouse position KoPathSegment s1(startPoints[0], snappedPoints[0] + snappedPoints[0]-startPoints[0]); KoPathSegment s2(startPoints[1], snappedPoints[1] + snappedPoints[1]-startPoints[1]); QList isects = s1.intersections(s2); if (isects.count() == 1 && squareDistance(isects[0], mousePosition) < maxDistance) { // add both extension lines m_lines.append(QLineF(startPoints[0], isects[0])); m_lines.append(QLineF(startPoints[1], isects[0])); setSnappedPosition(isects[0]); } else { // only add nearest extension line of both uint index = minDistances[0] < minDistances[1] ? 0 : 1; m_lines.append(QLineF(startPoints[index], snappedPoints[index])); setSnappedPosition(snappedPoints[index]); } } else if (minDistances[0] < HUGE_VAL) { m_lines.append(QLineF(startPoints[0], snappedPoints[0])); setSnappedPosition(snappedPoints[0]); } else if (minDistances[1] < HUGE_VAL) { m_lines.append(QLineF(startPoints[1], snappedPoints[1])); setSnappedPosition(snappedPoints[1]); } else { // none of the extension lines is near our mouse position return false; } return true; } QPainterPath ExtensionSnapStrategy::decoration(const KoViewConverter &/*converter*/) const { QPainterPath decoration; foreach (const QLineF &line, m_lines) { decoration.moveTo(line.p1()); decoration.lineTo(line.p2()); } return decoration; } bool ExtensionSnapStrategy::snapToExtension(QPointF &position, KoPathPoint * point, const QTransform &matrix) { Q_ASSERT(point); QPointF direction = extensionDirection(point, matrix); if (direction.isNull()) return false; QPointF extensionStart = matrix.map(point->point()); QPointF extensionStop = matrix.map(point->point()) + direction; float posOnExtension = project(extensionStart, extensionStop, position); if (posOnExtension < 0.0) return false; position = extensionStart + posOnExtension * direction; return true; } qreal ExtensionSnapStrategy::project(const QPointF &lineStart, const QPointF &lineEnd, const QPointF &point) { // This is how the returned value should be used to get the // projectionPoint: ProjectionPoint = lineStart(1-resultingReal) + resultingReal*lineEnd; QPointF diff = lineEnd - lineStart; QPointF relPoint = point - lineStart; qreal diffLength = sqrt(diff.x() * diff.x() + diff.y() * diff.y()); if (diffLength == 0.0) return 0.0; diff /= diffLength; // project mouse position relative to stop position on extension line qreal scalar = relPoint.x() * diff.x() + relPoint.y() * diff.y(); return scalar /= diffLength; } QPointF ExtensionSnapStrategy::extensionDirection(KoPathPoint * point, const QTransform &matrix) { Q_ASSERT(point); KoPathShape * path = point->parent(); KoPathPointIndex index = path->pathPointIndex(point); // check if it is a start point if (point->properties() & KoPathPoint::StartSubpath) { if (point->activeControlPoint2()) { return matrix.map(point->point()) - matrix.map(point->controlPoint2()); } else { KoPathPoint * next = path->pointByIndex(KoPathPointIndex(index.first, index.second + 1)); if (! next){ return QPointF(); } else if (next->activeControlPoint1()) { return matrix.map(point->point()) - matrix.map(next->controlPoint1()); } else { return matrix.map(point->point()) - matrix.map(next->point()); } } } else { if (point->activeControlPoint1()) { return matrix.map(point->point()) - matrix.map(point->controlPoint1()); } else { KoPathPoint * prev = path->pointByIndex(KoPathPointIndex(index.first, index.second - 1)); if (! prev){ return QPointF(); } else if (prev->activeControlPoint2()) { return matrix.map(point->point()) - matrix.map(prev->controlPoint2()); } else { return matrix.map(point->point()) - matrix.map(prev->point()); } } } } IntersectionSnapStrategy::IntersectionSnapStrategy() : KoSnapStrategy(KoSnapGuide::IntersectionSnapping) { } bool IntersectionSnapStrategy::snap(const QPointF &mousePosition, KoSnapProxy *proxy, qreal maxSnapDistance) { Q_ASSERT(std::isfinite(maxSnapDistance)); const qreal maxDistance = maxSnapDistance * maxSnapDistance; qreal minDistance = HUGE_VAL; QRectF rect(-maxSnapDistance, -maxSnapDistance, maxSnapDistance, maxSnapDistance); rect.moveCenter(mousePosition); QPointF snappedPoint = mousePosition; QList segments = proxy->segmentsInRect(rect, false); int segmentCount = segments.count(); for (int i = 0; i < segmentCount; ++i) { const KoPathSegment &s1 = segments[i]; for (int j = i + 1; j < segmentCount; ++j) { QList isects = s1.intersections(segments[j]); Q_FOREACH (const QPointF &point, isects) { if (! rect.contains(point)) continue; qreal distance = squareDistance(mousePosition, point); if (distance < maxDistance && distance < minDistance) { snappedPoint = point; minDistance = distance; } } } } setSnappedPosition(snappedPoint); return (minDistance < HUGE_VAL); } QPainterPath IntersectionSnapStrategy::decoration(const KoViewConverter &converter) const { QRectF unzoomedRect = converter.viewToDocument(QRectF(0, 0, 11, 11)); unzoomedRect.moveCenter(snappedPosition()); QPainterPath decoration; decoration.addRect(unzoomedRect); return decoration; } GridSnapStrategy::GridSnapStrategy() : KoSnapStrategy(KoSnapGuide::GridSnapping) { } bool GridSnapStrategy::snap(const QPointF &mousePosition, KoSnapProxy *proxy, qreal maxSnapDistance) { Q_ASSERT(std::isfinite(maxSnapDistance)); if (! proxy->canvas()->snapToGrid()) return false; // The 1e-10 here is a workaround for some weird division problem. // 360.00062366 / 2.83465058 gives 127 'exactly' when shown as a qreal, // but when casting into an int, we get 126. In fact it's 127 - 5.64e-15 ! QPointF offset; QSizeF spacing; proxy->canvas()->gridSize(&offset, &spacing); // we want to snap to the nearest grid point, so calculate // the grid rows/columns before and after the points position int col = static_cast((mousePosition.x() - offset.x()) / spacing.width() + 1e-10); int nextCol = col + 1; int row = static_cast((mousePosition.y() - offset.y()) / spacing.height() + 1e-10); int nextRow = row + 1; // now check which grid line has less distance to the point qreal distToCol = qAbs(offset.x() + col * spacing.width() - mousePosition.x()); qreal distToNextCol = qAbs(offset.x() + nextCol * spacing.width() - mousePosition.x()); if (distToCol > distToNextCol) { col = nextCol; distToCol = distToNextCol; } qreal distToRow = qAbs(offset.y() + row * spacing.height() - mousePosition.y()); qreal distToNextRow = qAbs(offset.y() + nextRow * spacing.height() - mousePosition.y()); if (distToRow > distToNextRow) { row = nextRow; distToRow = distToNextRow; } QPointF snappedPoint = mousePosition; bool pointIsSnapped = false; const qreal sqDistance = distToCol * distToCol + distToRow * distToRow; const qreal maxSqDistance = maxSnapDistance * maxSnapDistance; // now check if we are inside the snap distance if (sqDistance < maxSqDistance) { snappedPoint = QPointF(offset.x() + col * spacing.width(), offset.y() + row * spacing.height()); pointIsSnapped = true; } else if (distToRow < maxSnapDistance) { snappedPoint.ry() = offset.y() + row * spacing.height(); pointIsSnapped = true; } else if (distToCol < maxSnapDistance) { snappedPoint.rx() = offset.x() + col * spacing.width(); pointIsSnapped = true; } setSnappedPosition(snappedPoint); return pointIsSnapped; } QPainterPath GridSnapStrategy::decoration(const KoViewConverter &converter) const { QSizeF unzoomedSize = converter.viewToDocument(QSizeF(5, 5)); QPainterPath decoration; decoration.moveTo(snappedPosition() - QPointF(unzoomedSize.width(), 0)); decoration.lineTo(snappedPosition() + QPointF(unzoomedSize.width(), 0)); decoration.moveTo(snappedPosition() - QPointF(0, unzoomedSize.height())); decoration.lineTo(snappedPosition() + QPointF(0, unzoomedSize.height())); return decoration; } BoundingBoxSnapStrategy::BoundingBoxSnapStrategy() : KoSnapStrategy(KoSnapGuide::BoundingBoxSnapping) { } bool BoundingBoxSnapStrategy::snap(const QPointF &mousePosition, KoSnapProxy *proxy, qreal maxSnapDistance) { Q_ASSERT(std::isfinite(maxSnapDistance)); const qreal maxDistance = maxSnapDistance * maxSnapDistance; qreal minDistance = HUGE_VAL; QRectF rect(-maxSnapDistance, -maxSnapDistance, maxSnapDistance, maxSnapDistance); rect.moveCenter(mousePosition); QPointF snappedPoint = mousePosition; KoFlake::AnchorPosition pointId[5] = { KoFlake::TopLeft, KoFlake::TopRight, KoFlake::BottomRight, KoFlake::BottomLeft, KoFlake::Center }; QList shapes = proxy->shapesInRect(rect, true); Q_FOREACH (KoShape * shape, shapes) { qreal shapeMinDistance = HUGE_VAL; // first check the corner and center points for (int i = 0; i < 5; ++i) { m_boxPoints[i] = shape->absolutePosition(pointId[i]); qreal d = squareDistance(mousePosition, m_boxPoints[i]); if (d < minDistance && d < maxDistance) { shapeMinDistance = d; minDistance = d; snappedPoint = m_boxPoints[i]; } } // prioritize points over edges if (shapeMinDistance < maxDistance) continue; // now check distances to edges of bounding box for (int i = 0; i < 4; ++i) { QPointF pointOnLine; qreal d = squareDistanceToLine(m_boxPoints[i], m_boxPoints[(i+1)%4], mousePosition, pointOnLine); if (d < minDistance && d < maxDistance) { minDistance = d; snappedPoint = pointOnLine; } } } setSnappedPosition(snappedPoint); return (minDistance < maxDistance); } qreal BoundingBoxSnapStrategy::squareDistanceToLine(const QPointF &lineA, const QPointF &lineB, const QPointF &point, QPointF &pointOnLine) { QPointF diff = lineB - lineA; if(lineA == lineB) return HUGE_VAL; const qreal diffLength = sqrt(diff.x() * diff.x() + diff.y() * diff.y()); // project mouse position relative to start position on line const qreal scalar = KoSnapStrategy::scalarProduct(point - lineA, diff / diffLength); if (scalar < 0.0 || scalar > diffLength) return HUGE_VAL; // calculate vector between relative mouse position and projected mouse position pointOnLine = lineA + scalar / diffLength * diff; QPointF distVec = pointOnLine - point; return distVec.x()*distVec.x() + distVec.y()*distVec.y(); } QPainterPath BoundingBoxSnapStrategy::decoration(const KoViewConverter &converter) const { QSizeF unzoomedSize = converter.viewToDocument(QSizeF(5, 5)); QPainterPath decoration; decoration.moveTo(snappedPosition() - QPointF(unzoomedSize.width(), unzoomedSize.height())); decoration.lineTo(snappedPosition() + QPointF(unzoomedSize.width(), unzoomedSize.height())); decoration.moveTo(snappedPosition() - QPointF(unzoomedSize.width(), -unzoomedSize.height())); decoration.lineTo(snappedPosition() + QPointF(unzoomedSize.width(), -unzoomedSize.height())); return decoration; } // KoGuidesData has been moved into Krita. Please port this class! // LineGuideSnapStrategy::LineGuideSnapStrategy() // : KoSnapStrategy(KoSnapGuide::GuideLineSnapping) // { // } // bool LineGuideSnapStrategy::snap(const QPointF &mousePosition, KoSnapProxy * proxy, qreal maxSnapDistance) // { // Q_ASSERT(std::isfinite(maxSnapDistance)); // KoGuidesData * guidesData = proxy->canvas()->guidesData(); // if (!guidesData || !guidesData->showGuideLines()) // return false; // QPointF snappedPoint = mousePosition; // m_orientation = 0; // qreal minHorzDistance = maxSnapDistance; // Q_FOREACH (qreal guidePos, guidesData->horizontalGuideLines()) { // qreal distance = qAbs(guidePos - mousePosition.y()); // if (distance < minHorzDistance) { // snappedPoint.setY(guidePos); // minHorzDistance = distance; // m_orientation |= Qt::Horizontal; // } // } // qreal minVertSnapDistance = maxSnapDistance; // Q_FOREACH (qreal guidePos, guidesData->verticalGuideLines()) { // qreal distance = qAbs(guidePos - mousePosition.x()); // if (distance < minVertSnapDistance) { // snappedPoint.setX(guidePos); // minVertSnapDistance = distance; // m_orientation |= Qt::Vertical; // } // } // setSnappedPosition(snappedPoint); // return (minHorzDistance < maxSnapDistance || minVertSnapDistance < maxSnapDistance); // } // QPainterPath LineGuideSnapStrategy::decoration(const KoViewConverter &converter) const // { // QSizeF unzoomedSize = converter.viewToDocument(QSizeF(5, 5)); // Q_ASSERT(unzoomedSize.isValid()); // QPainterPath decoration; // if (m_orientation & Qt::Horizontal) { // decoration.moveTo(snappedPosition() - QPointF(unzoomedSize.width(), 0)); // decoration.lineTo(snappedPosition() + QPointF(unzoomedSize.width(), 0)); // } // if (m_orientation & Qt::Vertical) { // decoration.moveTo(snappedPosition() - QPointF(0, unzoomedSize.height())); // decoration.lineTo(snappedPosition() + QPointF(0, unzoomedSize.height())); // } // return decoration; // } diff --git a/libs/flake/svg/SvgParser.cpp b/libs/flake/svg/SvgParser.cpp index 4f29f6f072..44a90aafd5 100644 --- a/libs/flake/svg/SvgParser.cpp +++ b/libs/flake/svg/SvgParser.cpp @@ -1,1932 +1,1933 @@ /* This file is part of the KDE project * Copyright (C) 2002-2005,2007 Rob Buis * Copyright (C) 2002-2004 Nicolas Goutte * Copyright (C) 2005-2006 Tim Beaulen * Copyright (C) 2005-2009 Jan Hambrecht * Copyright (C) 2005,2007 Thomas Zander * Copyright (C) 2006-2007 Inge Wallin * Copyright (C) 2007-2008,2010 Thorsten Zachmann * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "SvgParser.h" #include #include #include #include +#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "KoFilterEffectStack.h" #include "KoFilterEffectLoadingContext.h" #include #include #include #include #include "SvgUtil.h" #include "SvgShape.h" #include "SvgGraphicContext.h" #include "SvgFilterHelper.h" #include "SvgGradientHelper.h" #include "SvgClipPathHelper.h" #include "parsers/SvgTransformParser.h" #include "kis_pointer_utils.h" #include #include #include #include #include "kis_dom_utils.h" #include "kis_algebra_2d.h" #include "kis_debug.h" #include "kis_global.h" #include struct SvgParser::DeferredUseStore { struct El { El(const KoXmlElement* ue, const QString& key) : m_useElement(ue), m_key(key) { } const KoXmlElement* m_useElement; QString m_key; }; DeferredUseStore(SvgParser* p) : m_parse(p) { } void add(const KoXmlElement* useE, const QString& key) { m_uses.push_back(El(useE, key)); } bool empty() const { return m_uses.empty(); } void checkPendingUse(const KoXmlElement &b, QList& shapes) { KoShape* shape = 0; const QString id = b.attribute("id"); if (id.isEmpty()) return; // debugFlake << "Checking id: " << id; auto i = std::partition(m_uses.begin(), m_uses.end(), [&](const El& e) -> bool {return e.m_key != id;}); while (i != m_uses.end()) { const El& el = m_uses.back(); if (m_parse->m_context.hasDefinition(el.m_key)) { // debugFlake << "Found pending use for id: " << el.m_key; shape = m_parse->resolveUse(*(el.m_useElement), el.m_key); if (shape) { shapes.append(shape); } } m_uses.pop_back(); } } ~DeferredUseStore() { while (!m_uses.empty()) { const El& el = m_uses.back(); debugFlake << "WARNING: could not find path in m_uses; }; SvgParser::SvgParser(KoDocumentResourceManager *documentResourceManager) : m_context(documentResourceManager) , m_documentResourceManager(documentResourceManager) { } SvgParser::~SvgParser() { } KoXmlDocument SvgParser::createDocumentFromSvg(QIODevice *device, QString *errorMsg, int *errorLine, int *errorColumn) { QXmlInputSource source(device); return createDocumentFromSvg(&source, errorMsg, errorLine, errorColumn); } KoXmlDocument SvgParser::createDocumentFromSvg(const QByteArray &data, QString *errorMsg, int *errorLine, int *errorColumn) { QXmlInputSource source; source.setData(data); return createDocumentFromSvg(&source, errorMsg, errorLine, errorColumn); } KoXmlDocument SvgParser::createDocumentFromSvg(const QString &data, QString *errorMsg, int *errorLine, int *errorColumn) { QXmlInputSource source; source.setData(data); return createDocumentFromSvg(&source, errorMsg, errorLine, errorColumn); } KoXmlDocument SvgParser::createDocumentFromSvg(QXmlInputSource *source, QString *errorMsg, int *errorLine, int *errorColumn) { // we should read all spaces to parse text node correctly QXmlSimpleReader reader; reader.setFeature("http://qt-project.org/xml/features/report-whitespace-only-CharData", true); reader.setFeature("http://xml.org/sax/features/namespaces", false); reader.setFeature("http://xml.org/sax/features/namespace-prefixes", true); QDomDocument doc; if (!doc.setContent(source, &reader, errorMsg, errorLine, errorColumn)) { return QDomDocument(); } return doc; } void SvgParser::setXmlBaseDir(const QString &baseDir) { m_context.setInitialXmlBaseDir(baseDir); setFileFetcher( [this](const QString &name) { const QString fileName = m_context.xmlBaseDir() + QDir::separator() + name; QFile file(fileName); if (!file.exists()) { return QByteArray(); } file.open(QIODevice::ReadOnly); return file.readAll(); }); } void SvgParser::setResolution(const QRectF boundsInPixels, qreal pixelsPerInch) { KIS_ASSERT(!m_context.currentGC()); m_context.pushGraphicsContext(); m_context.currentGC()->isResolutionFrame = true; m_context.currentGC()->pixelsPerInch = pixelsPerInch; const qreal scale = 72.0 / pixelsPerInch; const QTransform t = QTransform::fromScale(scale, scale); m_context.currentGC()->currentBoundingBox = boundsInPixels; m_context.currentGC()->matrix = t; } void SvgParser::setForcedFontSizeResolution(qreal value) { if (qFuzzyCompare(value, 0.0)) return; m_context.currentGC()->forcedFontSizeCoeff = 72.0 / value; } QList SvgParser::shapes() const { return m_shapes; } QVector SvgParser::takeSymbols() { QVector symbols = m_symbols.values().toVector(); m_symbols.clear(); return symbols; } // Helper functions // --------------------------------------------------------------------------------------- SvgGradientHelper* SvgParser::findGradient(const QString &id) { SvgGradientHelper *result = 0; // check if gradient was already parsed, and return it if (m_gradients.contains(id)) { result = &m_gradients[ id ]; } // check if gradient was stored for later parsing if (!result && m_context.hasDefinition(id)) { const KoXmlElement &e = m_context.definition(id); if (e.tagName().contains("Gradient")) { result = parseGradient(m_context.definition(id)); } } return result; } QSharedPointer SvgParser::findPattern(const QString &id, const KoShape *shape) { QSharedPointer result; // check if gradient was stored for later parsing if (m_context.hasDefinition(id)) { const KoXmlElement &e = m_context.definition(id); if (e.tagName() == "pattern") { result = parsePattern(m_context.definition(id), shape); } } return result; } SvgFilterHelper* SvgParser::findFilter(const QString &id, const QString &href) { // check if filter was already parsed, and return it if (m_filters.contains(id)) return &m_filters[ id ]; // check if filter was stored for later parsing if (!m_context.hasDefinition(id)) return 0; const KoXmlElement &e = m_context.definition(id); if (KoXml::childNodesCount(e) == 0) { QString mhref = e.attribute("xlink:href").mid(1); if (m_context.hasDefinition(mhref)) return findFilter(mhref, id); else return 0; } else { // ok parse filter now if (! parseFilter(m_context.definition(id), m_context.definition(href))) return 0; } // return successfully parsed filter or 0 QString n; if (href.isEmpty()) n = id; else n = href; if (m_filters.contains(n)) return &m_filters[ n ]; else return 0; } SvgClipPathHelper* SvgParser::findClipPath(const QString &id) { return m_clipPaths.contains(id) ? &m_clipPaths[id] : 0; } // Parsing functions // --------------------------------------------------------------------------------------- qreal SvgParser::parseUnit(const QString &unit, bool horiz, bool vert, const QRectF &bbox) { return SvgUtil::parseUnit(m_context.currentGC(), unit, horiz, vert, bbox); } qreal SvgParser::parseUnitX(const QString &unit) { return SvgUtil::parseUnitX(m_context.currentGC(), unit); } qreal SvgParser::parseUnitY(const QString &unit) { return SvgUtil::parseUnitY(m_context.currentGC(), unit); } qreal SvgParser::parseUnitXY(const QString &unit) { return SvgUtil::parseUnitXY(m_context.currentGC(), unit); } qreal SvgParser::parseAngular(const QString &unit) { return SvgUtil::parseUnitAngular(m_context.currentGC(), unit); } SvgGradientHelper* SvgParser::parseGradient(const KoXmlElement &e) { // IMPROVEMENTS: // - Store the parsed colorstops in some sort of a cache so they don't need to be parsed again. // - A gradient inherits attributes it does not have from the referencing gradient. // - Gradients with no color stops have no fill or stroke. // - Gradients with one color stop have a solid color. SvgGraphicsContext *gc = m_context.currentGC(); if (!gc) return 0; SvgGradientHelper gradHelper; QString gradientId = e.attribute("id"); if (gradientId.isEmpty()) return 0; // check if we have this gradient already parsed // copy existing gradient if it exists if (m_gradients.contains(gradientId)) { return &m_gradients[gradientId]; } if (e.hasAttribute("xlink:href")) { // strip the '#' symbol QString href = e.attribute("xlink:href").mid(1); if (!href.isEmpty()) { // copy the referenced gradient if found SvgGradientHelper *pGrad = findGradient(href); if (pGrad) { gradHelper = *pGrad; } } } const QGradientStops defaultStops = gradHelper.gradient()->stops(); if (e.attribute("gradientUnits") == "userSpaceOnUse") { gradHelper.setGradientUnits(KoFlake::UserSpaceOnUse); } m_context.pushGraphicsContext(e); uploadStyleToContext(e); if (e.tagName() == "linearGradient") { QLinearGradient *g = new QLinearGradient(); if (gradHelper.gradientUnits() == KoFlake::ObjectBoundingBox) { g->setCoordinateMode(QGradient::ObjectBoundingMode); g->setStart(QPointF(SvgUtil::fromPercentage(e.attribute("x1", "0%")), SvgUtil::fromPercentage(e.attribute("y1", "0%")))); g->setFinalStop(QPointF(SvgUtil::fromPercentage(e.attribute("x2", "100%")), SvgUtil::fromPercentage(e.attribute("y2", "0%")))); } else { g->setStart(QPointF(parseUnitX(e.attribute("x1")), parseUnitY(e.attribute("y1")))); g->setFinalStop(QPointF(parseUnitX(e.attribute("x2")), parseUnitY(e.attribute("y2")))); } gradHelper.setGradient(g); } else if (e.tagName() == "radialGradient") { QRadialGradient *g = new QRadialGradient(); if (gradHelper.gradientUnits() == KoFlake::ObjectBoundingBox) { g->setCoordinateMode(QGradient::ObjectBoundingMode); g->setCenter(QPointF(SvgUtil::fromPercentage(e.attribute("cx", "50%")), SvgUtil::fromPercentage(e.attribute("cy", "50%")))); g->setRadius(SvgUtil::fromPercentage(e.attribute("r", "50%"))); g->setFocalPoint(QPointF(SvgUtil::fromPercentage(e.attribute("fx", "50%")), SvgUtil::fromPercentage(e.attribute("fy", "50%")))); } else { g->setCenter(QPointF(parseUnitX(e.attribute("cx")), parseUnitY(e.attribute("cy")))); g->setFocalPoint(QPointF(parseUnitX(e.attribute("fx")), parseUnitY(e.attribute("fy")))); g->setRadius(parseUnitXY(e.attribute("r"))); } gradHelper.setGradient(g); } else { debugFlake << "WARNING: Failed to parse gradient with tag" << e.tagName(); } // handle spread method QGradient::Spread spreadMethod = QGradient::PadSpread; QString spreadMethodStr = e.attribute("spreadMethod"); if (!spreadMethodStr.isEmpty()) { if (spreadMethodStr == "reflect") { spreadMethod = QGradient::ReflectSpread; } else if (spreadMethodStr == "repeat") { spreadMethod = QGradient::RepeatSpread; } } gradHelper.setSpreadMode(spreadMethod); // Parse the color stops. m_context.styleParser().parseColorStops(gradHelper.gradient(), e, gc, defaultStops); if (e.hasAttribute("gradientTransform")) { SvgTransformParser p(e.attribute("gradientTransform")); if (p.isValid()) { gradHelper.setTransform(p.transform()); } } m_context.popGraphicsContext(); m_gradients.insert(gradientId, gradHelper); return &m_gradients[gradientId]; } inline QPointF bakeShapeOffset(const QTransform &patternTransform, const QPointF &shapeOffset) { QTransform result = patternTransform * QTransform::fromTranslate(-shapeOffset.x(), -shapeOffset.y()) * patternTransform.inverted(); KIS_ASSERT_RECOVER_NOOP(result.type() <= QTransform::TxTranslate); return QPointF(result.dx(), result.dy()); } QSharedPointer SvgParser::parsePattern(const KoXmlElement &e, const KoShape *shape) { /** * Unlike the gradient parsing function, this method is called every time we * *reference* the pattern, not when we define it. Therefore we can already * use the coordinate system of the destination. */ QSharedPointer pattHelper; SvgGraphicsContext *gc = m_context.currentGC(); if (!gc) return pattHelper; const QString patternId = e.attribute("id"); if (patternId.isEmpty()) return pattHelper; pattHelper = toQShared(new KoVectorPatternBackground); if (e.hasAttribute("xlink:href")) { // strip the '#' symbol QString href = e.attribute("xlink:href").mid(1); if (!href.isEmpty() &&href != patternId) { // copy the referenced pattern if found QSharedPointer pPatt = findPattern(href, shape); if (pPatt) { pattHelper = pPatt; } } } pattHelper->setReferenceCoordinates( KoFlake::coordinatesFromString(e.attribute("patternUnits"), pattHelper->referenceCoordinates())); pattHelper->setContentCoordinates( KoFlake::coordinatesFromString(e.attribute("patternContentUnits"), pattHelper->contentCoordinates())); if (e.hasAttribute("patternTransform")) { SvgTransformParser p(e.attribute("patternTransform")); if (p.isValid()) { pattHelper->setPatternTransform(p.transform()); } } if (pattHelper->referenceCoordinates() == KoFlake::ObjectBoundingBox) { QRectF referenceRect( SvgUtil::fromPercentage(e.attribute("x", "0%")), SvgUtil::fromPercentage(e.attribute("y", "0%")), SvgUtil::fromPercentage(e.attribute("width", "0%")), // 0% is according to SVG 1.1, don't ask me why! SvgUtil::fromPercentage(e.attribute("height", "0%"))); // 0% is according to SVG 1.1, don't ask me why! pattHelper->setReferenceRect(referenceRect); } else { QRectF referenceRect( parseUnitX(e.attribute("x", "0")), parseUnitY(e.attribute("y", "0")), parseUnitX(e.attribute("width", "0")), // 0 is according to SVG 1.1, don't ask me why! parseUnitY(e.attribute("height", "0"))); // 0 is according to SVG 1.1, don't ask me why! pattHelper->setReferenceRect(referenceRect); } /** * In Krita shapes X,Y coordinates are baked into the shape global transform, but * the pattern should be painted in "user" coordinates. Therefore, we should handle * this offfset separately. * * TODO: Please also note that this offset is different from extraShapeOffset(), * because A.inverted() * B != A * B.inverted(). I'm not sure which variant is * correct (DK) */ const QTransform dstShapeTransform = shape->absoluteTransformation(0); const QTransform shapeOffsetTransform = dstShapeTransform * gc->matrix.inverted(); KIS_SAFE_ASSERT_RECOVER_NOOP(shapeOffsetTransform.type() <= QTransform::TxTranslate); const QPointF extraShapeOffset(shapeOffsetTransform.dx(), shapeOffsetTransform.dy()); m_context.pushGraphicsContext(e); gc = m_context.currentGC(); gc->workaroundClearInheritedFillProperties(); // HACK! // start building shape tree from scratch gc->matrix = QTransform(); const QRectF boundingRect = shape->outline().boundingRect()/*.translated(extraShapeOffset)*/; const QTransform relativeToShape(boundingRect.width(), 0, 0, boundingRect.height(), boundingRect.x(), boundingRect.y()); // WARNING1: OBB and ViewBox transformations are *baked* into the pattern shapes! // although we expect the pattern be reusable, but it is not so! // WARNING2: the pattern shapes are stored in *User* coordinate system, although // the "official" content system might be either OBB or User. It means that // this baked transform should be stripped before writing the shapes back // into SVG if (e.hasAttribute("viewBox")) { gc->currentBoundingBox = pattHelper->referenceCoordinates() == KoFlake::ObjectBoundingBox ? relativeToShape.mapRect(pattHelper->referenceRect()) : pattHelper->referenceRect(); applyViewBoxTransform(e); pattHelper->setContentCoordinates(pattHelper->referenceCoordinates()); } else if (pattHelper->contentCoordinates() == KoFlake::ObjectBoundingBox) { gc->matrix = relativeToShape * gc->matrix; } // We do *not* apply patternTransform here! Here we only bake the untransformed // version of the shape. The transformed one will be done in the very end while rendering. QList patternShapes = parseContainer(e); if (pattHelper->contentCoordinates() == KoFlake::UserSpaceOnUse) { // In Krita we normalize the shapes, bake this transform into the pattern shapes const QPointF offset = bakeShapeOffset(pattHelper->patternTransform(), extraShapeOffset); Q_FOREACH (KoShape *shape, patternShapes) { shape->applyAbsoluteTransformation(QTransform::fromTranslate(offset.x(), offset.y())); } } if (pattHelper->referenceCoordinates() == KoFlake::UserSpaceOnUse) { // In Krita we normalize the shapes, bake this transform into reference rect // NOTE: this is possible *only* when pattern transform is not perspective // (which is always true for SVG) const QPointF offset = bakeShapeOffset(pattHelper->patternTransform(), extraShapeOffset); QRectF ref = pattHelper->referenceRect(); ref.translate(offset); pattHelper->setReferenceRect(ref); } m_context.popGraphicsContext(); gc = m_context.currentGC(); if (!patternShapes.isEmpty()) { pattHelper->setShapes(patternShapes); } return pattHelper; } bool SvgParser::parseFilter(const KoXmlElement &e, const KoXmlElement &referencedBy) { SvgFilterHelper filter; // Use the filter that is referencing, or if there isn't one, the original filter KoXmlElement b; if (!referencedBy.isNull()) b = referencedBy; else b = e; // check if we are referencing another filter if (e.hasAttribute("xlink:href")) { QString href = e.attribute("xlink:href").mid(1); if (! href.isEmpty()) { // copy the referenced filter if found SvgFilterHelper *refFilter = findFilter(href); if (refFilter) filter = *refFilter; } } else { filter.setContent(b); } if (b.attribute("filterUnits") == "userSpaceOnUse") filter.setFilterUnits(KoFlake::UserSpaceOnUse); if (b.attribute("primitiveUnits") == "objectBoundingBox") filter.setPrimitiveUnits(KoFlake::ObjectBoundingBox); // parse filter region rectangle if (filter.filterUnits() == KoFlake::UserSpaceOnUse) { filter.setPosition(QPointF(parseUnitX(b.attribute("x")), parseUnitY(b.attribute("y")))); filter.setSize(QSizeF(parseUnitX(b.attribute("width")), parseUnitY(b.attribute("height")))); } else { // x, y, width, height are in percentages of the object referencing the filter // so we just parse the percentages filter.setPosition(QPointF(SvgUtil::fromPercentage(b.attribute("x", "-0.1")), SvgUtil::fromPercentage(b.attribute("y", "-0.1")))); filter.setSize(QSizeF(SvgUtil::fromPercentage(b.attribute("width", "1.2")), SvgUtil::fromPercentage(b.attribute("height", "1.2")))); } m_filters.insert(b.attribute("id"), filter); return true; } bool SvgParser::parseMarker(const KoXmlElement &e) { const QString id = e.attribute("id"); if (id.isEmpty()) return false; QScopedPointer marker(new KoMarker()); marker->setCoordinateSystem( KoMarker::coordinateSystemFromString(e.attribute("markerUnits", "strokeWidth"))); marker->setReferencePoint(QPointF(parseUnitX(e.attribute("refX")), parseUnitY(e.attribute("refY")))); marker->setReferenceSize(QSizeF(parseUnitX(e.attribute("markerWidth", "3")), parseUnitY(e.attribute("markerHeight", "3")))); const QString orientation = e.attribute("orient", "0"); if (orientation == "auto") { marker->setAutoOrientation(true); } else { marker->setExplicitOrientation(parseAngular(orientation)); } // ensure that the clip path is loaded in local coordinates system m_context.pushGraphicsContext(e, false); m_context.currentGC()->matrix = QTransform(); m_context.currentGC()->currentBoundingBox = QRectF(QPointF(0, 0), marker->referenceSize()); KoShape *markerShape = parseGroup(e); m_context.popGraphicsContext(); if (!markerShape) return false; marker->setShapes({markerShape}); m_markers.insert(id, QExplicitlySharedDataPointer(marker.take())); return true; } bool SvgParser::parseSymbol(const KoXmlElement &e) { const QString id = e.attribute("id"); if (id.isEmpty()) return false; QScopedPointer svgSymbol(new KoSvgSymbol()); // ensure that the clip path is loaded in local coordinates system m_context.pushGraphicsContext(e, false); m_context.currentGC()->matrix = QTransform(); m_context.currentGC()->currentBoundingBox = QRectF(0.0, 0.0, 1.0, 1.0); QString title = e.firstChildElement("title").toElement().text(); QScopedPointer symbolShape(parseGroup(e)); m_context.popGraphicsContext(); if (!symbolShape) return false; svgSymbol->shape = symbolShape.take(); svgSymbol->title = title; svgSymbol->id = id; if (title.isEmpty()) svgSymbol->title = id; if (svgSymbol->shape->boundingRect() == QRectF(0.0, 0.0, 0.0, 0.0)) { debugFlake << "Symbol" << id << "seems to be empty, discarding"; return false; } m_symbols.insert(id, svgSymbol.take()); return true; } bool SvgParser::parseClipPath(const KoXmlElement &e) { SvgClipPathHelper clipPath; const QString id = e.attribute("id"); if (id.isEmpty()) return false; clipPath.setClipPathUnits( KoFlake::coordinatesFromString(e.attribute("clipPathUnits"), KoFlake::UserSpaceOnUse)); // ensure that the clip path is loaded in local coordinates system m_context.pushGraphicsContext(e); m_context.currentGC()->matrix = QTransform(); m_context.currentGC()->workaroundClearInheritedFillProperties(); // HACK! KoShape *clipShape = parseGroup(e); m_context.popGraphicsContext(); if (!clipShape) return false; clipPath.setShapes({clipShape}); m_clipPaths.insert(id, clipPath); return true; } bool SvgParser::parseClipMask(const KoXmlElement &e) { QSharedPointer clipMask(new KoClipMask); const QString id = e.attribute("id"); if (id.isEmpty()) return false; clipMask->setCoordinates(KoFlake::coordinatesFromString(e.attribute("maskUnits"), KoFlake::ObjectBoundingBox)); clipMask->setContentCoordinates(KoFlake::coordinatesFromString(e.attribute("maskContentUnits"), KoFlake::UserSpaceOnUse)); QRectF maskRect; if (clipMask->coordinates() == KoFlake::ObjectBoundingBox) { maskRect.setRect( SvgUtil::fromPercentage(e.attribute("x", "-10%")), SvgUtil::fromPercentage(e.attribute("y", "-10%")), SvgUtil::fromPercentage(e.attribute("width", "120%")), SvgUtil::fromPercentage(e.attribute("height", "120%"))); } else { maskRect.setRect( parseUnitX(e.attribute("x", "-10%")), // yes, percents are insane in this case, parseUnitY(e.attribute("y", "-10%")), // but this is what SVG 1.1 tells us... parseUnitX(e.attribute("width", "120%")), parseUnitY(e.attribute("height", "120%"))); } clipMask->setMaskRect(maskRect); // ensure that the clip mask is loaded in local coordinates system m_context.pushGraphicsContext(e); m_context.currentGC()->matrix = QTransform(); m_context.currentGC()->workaroundClearInheritedFillProperties(); // HACK! KoShape *clipShape = parseGroup(e); m_context.popGraphicsContext(); if (!clipShape) return false; clipMask->setShapes({clipShape}); m_clipMasks.insert(id, clipMask); return true; } void SvgParser::uploadStyleToContext(const KoXmlElement &e) { SvgStyles styles = m_context.styleParser().collectStyles(e); m_context.styleParser().parseFont(styles); m_context.styleParser().parseStyle(styles); } void SvgParser::applyCurrentStyle(KoShape *shape, const QPointF &shapeToOriginalUserCoordinates) { if (!shape) return; applyCurrentBasicStyle(shape); if (KoPathShape *pathShape = dynamic_cast(shape)) { applyMarkers(pathShape); } applyFilter(shape); applyClipping(shape, shapeToOriginalUserCoordinates); applyMaskClipping(shape, shapeToOriginalUserCoordinates); } void SvgParser::applyCurrentBasicStyle(KoShape *shape) { if (!shape) return; SvgGraphicsContext *gc = m_context.currentGC(); KIS_ASSERT(gc); if (!dynamic_cast(shape)) { applyFillStyle(shape); applyStrokeStyle(shape); } if (!gc->display || !gc->visible) { /** * WARNING: here is a small inconsistency with the standard: * in the standard, 'display' is not inherited, but in * flake it is! * * NOTE: though the standard says: "A value of 'display:none' indicates * that the given element and ***its children*** shall not be * rendered directly". Therefore, using setVisible(false) is fully * legitimate here (DK 29.11.16). */ shape->setVisible(false); } shape->setTransparency(1.0 - gc->opacity); } void SvgParser::applyStyle(KoShape *obj, const KoXmlElement &e, const QPointF &shapeToOriginalUserCoordinates) { applyStyle(obj, m_context.styleParser().collectStyles(e), shapeToOriginalUserCoordinates); } void SvgParser::applyStyle(KoShape *obj, const SvgStyles &styles, const QPointF &shapeToOriginalUserCoordinates) { SvgGraphicsContext *gc = m_context.currentGC(); if (!gc) return; m_context.styleParser().parseStyle(styles); if (!obj) return; if (!dynamic_cast(obj)) { applyFillStyle(obj); applyStrokeStyle(obj); } if (KoPathShape *pathShape = dynamic_cast(obj)) { applyMarkers(pathShape); } applyFilter(obj); applyClipping(obj, shapeToOriginalUserCoordinates); applyMaskClipping(obj, shapeToOriginalUserCoordinates); if (!gc->display || !gc->visible) { obj->setVisible(false); } obj->setTransparency(1.0 - gc->opacity); } QGradient* prepareGradientForShape(const SvgGradientHelper *gradient, const KoShape *shape, const SvgGraphicsContext *gc, QTransform *transform) { QGradient *resultGradient = 0; KIS_ASSERT(transform); if (gradient->gradientUnits() == KoFlake::ObjectBoundingBox) { resultGradient = KoFlake::cloneGradient(gradient->gradient()); *transform = gradient->transform(); } else { if (gradient->gradient()->type() == QGradient::LinearGradient) { /** * Create a converted gradient that looks the same, but linked to the * bounding rect of the shape, so it would be transformed with the shape */ const QRectF boundingRect = shape->outline().boundingRect(); const QTransform relativeToShape(boundingRect.width(), 0, 0, boundingRect.height(), boundingRect.x(), boundingRect.y()); const QTransform relativeToUser = relativeToShape * shape->transformation() * gc->matrix.inverted(); const QTransform userToRelative = relativeToUser.inverted(); const QLinearGradient *o = static_cast(gradient->gradient()); QLinearGradient *g = new QLinearGradient(); g->setStart(userToRelative.map(o->start())); g->setFinalStop(userToRelative.map(o->finalStop())); g->setCoordinateMode(QGradient::ObjectBoundingMode); g->setStops(o->stops()); g->setSpread(o->spread()); resultGradient = g; *transform = relativeToUser * gradient->transform() * userToRelative; } else if (gradient->gradient()->type() == QGradient::RadialGradient) { // For radial and conical gradients such conversion is not possible resultGradient = KoFlake::cloneGradient(gradient->gradient()); *transform = gradient->transform() * gc->matrix * shape->transformation().inverted(); const QRectF outlineRect = shape->outlineRect(); if (outlineRect.isEmpty()) return resultGradient; /** * If shape outline rect is valid, convert the gradient into OBB mode by * doing some magic conversions: we compensate non-uniform size of the shape * by applying an additional pre-transform */ QRadialGradient *rgradient = static_cast(resultGradient); const qreal maxDimension = KisAlgebra2D::maxDimension(outlineRect); const QRectF uniformSize(outlineRect.topLeft(), QSizeF(maxDimension, maxDimension)); const QTransform uniformizeTransform = QTransform::fromTranslate(-outlineRect.x(), -outlineRect.y()) * QTransform::fromScale(maxDimension / shape->outlineRect().width(), maxDimension / shape->outlineRect().height()) * QTransform::fromTranslate(outlineRect.x(), outlineRect.y()); const QPointF centerLocal = transform->map(rgradient->center()); const QPointF focalLocal = transform->map(rgradient->focalPoint()); const QPointF centerOBB = KisAlgebra2D::absoluteToRelative(centerLocal, uniformSize); const QPointF focalOBB = KisAlgebra2D::absoluteToRelative(focalLocal, uniformSize); rgradient->setCenter(centerOBB); rgradient->setFocalPoint(focalOBB); const qreal centerRadiusOBB = KisAlgebra2D::absoluteToRelative(rgradient->centerRadius(), uniformSize); const qreal focalRadiusOBB = KisAlgebra2D::absoluteToRelative(rgradient->focalRadius(), uniformSize); rgradient->setCenterRadius(centerRadiusOBB); rgradient->setFocalRadius(focalRadiusOBB); rgradient->setCoordinateMode(QGradient::ObjectBoundingMode); // Warning: should it really be pre-multiplication? *transform = uniformizeTransform * gradient->transform(); } } return resultGradient; } void SvgParser::applyFillStyle(KoShape *shape) { SvgGraphicsContext *gc = m_context.currentGC(); if (! gc) return; if (gc->fillType == SvgGraphicsContext::None) { shape->setBackground(QSharedPointer(0)); } else if (gc->fillType == SvgGraphicsContext::Solid) { shape->setBackground(QSharedPointer(new KoColorBackground(gc->fillColor))); } else if (gc->fillType == SvgGraphicsContext::Complex) { // try to find referenced gradient SvgGradientHelper *gradient = findGradient(gc->fillId); if (gradient) { QTransform transform; QGradient *result = prepareGradientForShape(gradient, shape, gc, &transform); if (result) { QSharedPointer bg; bg = toQShared(new KoGradientBackground(result)); bg->setTransform(transform); shape->setBackground(bg); } } else { QSharedPointer pattern = findPattern(gc->fillId, shape); if (pattern) { shape->setBackground(pattern); } else { // no referenced fill found, use fallback color shape->setBackground(QSharedPointer(new KoColorBackground(gc->fillColor))); } } } KoPathShape *path = dynamic_cast(shape); if (path) path->setFillRule(gc->fillRule); } void applyDashes(const KoShapeStrokeSP srcStroke, KoShapeStrokeSP dstStroke) { const double lineWidth = srcStroke->lineWidth(); QVector dashes = srcStroke->lineDashes(); // apply line width to dashes and dash offset if (dashes.count() && lineWidth > 0.0) { const double dashOffset = srcStroke->dashOffset(); QVector dashes = srcStroke->lineDashes(); for (int i = 0; i < dashes.count(); ++i) { dashes[i] /= lineWidth; } dstStroke->setLineStyle(Qt::CustomDashLine, dashes); dstStroke->setDashOffset(dashOffset / lineWidth); } else { dstStroke->setLineStyle(Qt::SolidLine, QVector()); } } void SvgParser::applyStrokeStyle(KoShape *shape) { SvgGraphicsContext *gc = m_context.currentGC(); if (! gc) return; if (gc->strokeType == SvgGraphicsContext::None) { shape->setStroke(KoShapeStrokeModelSP()); } else if (gc->strokeType == SvgGraphicsContext::Solid) { KoShapeStrokeSP stroke(new KoShapeStroke(*gc->stroke)); applyDashes(gc->stroke, stroke); shape->setStroke(stroke); } else if (gc->strokeType == SvgGraphicsContext::Complex) { // try to find referenced gradient SvgGradientHelper *gradient = findGradient(gc->strokeId); if (gradient) { QTransform transform; QGradient *result = prepareGradientForShape(gradient, shape, gc, &transform); if (result) { QBrush brush = *result; delete result; brush.setTransform(transform); KoShapeStrokeSP stroke(new KoShapeStroke(*gc->stroke)); stroke->setLineBrush(brush); applyDashes(gc->stroke, stroke); shape->setStroke(stroke); } } else { // no referenced stroke found, use fallback color KoShapeStrokeSP stroke(new KoShapeStroke(*gc->stroke)); applyDashes(gc->stroke, stroke); shape->setStroke(stroke); } } } void SvgParser::applyFilter(KoShape *shape) { SvgGraphicsContext *gc = m_context.currentGC(); if (! gc) return; if (gc->filterId.isEmpty()) return; SvgFilterHelper *filter = findFilter(gc->filterId); if (! filter) return; KoXmlElement content = filter->content(); // parse filter region QRectF bound(shape->position(), shape->size()); // work on bounding box without viewbox transformation applied // so user space coordinates of bounding box and filter region match up bound = gc->viewboxTransform.inverted().mapRect(bound); QRectF filterRegion(filter->position(bound), filter->size(bound)); // convert filter region to boundingbox units QRectF objectFilterRegion; objectFilterRegion.setTopLeft(SvgUtil::userSpaceToObject(filterRegion.topLeft(), bound)); objectFilterRegion.setSize(SvgUtil::userSpaceToObject(filterRegion.size(), bound)); KoFilterEffectLoadingContext context(m_context.xmlBaseDir()); context.setShapeBoundingBox(bound); // enable units conversion context.enableFilterUnitsConversion(filter->filterUnits() == KoFlake::UserSpaceOnUse); context.enableFilterPrimitiveUnitsConversion(filter->primitiveUnits() == KoFlake::UserSpaceOnUse); KoFilterEffectRegistry *registry = KoFilterEffectRegistry::instance(); KoFilterEffectStack *filterStack = 0; QSet stdInputs; stdInputs << "SourceGraphic" << "SourceAlpha"; stdInputs << "BackgroundImage" << "BackgroundAlpha"; stdInputs << "FillPaint" << "StrokePaint"; QMap inputs; // create the filter effects and add them to the shape for (KoXmlNode n = content.firstChild(); !n.isNull(); n = n.nextSibling()) { KoXmlElement primitive = n.toElement(); KoFilterEffect *filterEffect = registry->createFilterEffectFromXml(primitive, context); if (!filterEffect) { debugFlake << "filter effect" << primitive.tagName() << "is not implemented yet"; continue; } const QString input = primitive.attribute("in"); if (!input.isEmpty()) { filterEffect->setInput(0, input); } const QString output = primitive.attribute("result"); if (!output.isEmpty()) { filterEffect->setOutput(output); } QRectF subRegion; // parse subregion if (filter->primitiveUnits() == KoFlake::UserSpaceOnUse) { const QString xa = primitive.attribute("x"); const QString ya = primitive.attribute("y"); const QString wa = primitive.attribute("width"); const QString ha = primitive.attribute("height"); if (xa.isEmpty() || ya.isEmpty() || wa.isEmpty() || ha.isEmpty()) { bool hasStdInput = false; bool isFirstEffect = filterStack == 0; // check if one of the inputs is a standard input Q_FOREACH (const QString &input, filterEffect->inputs()) { if ((isFirstEffect && input.isEmpty()) || stdInputs.contains(input)) { hasStdInput = true; break; } } if (hasStdInput || primitive.tagName() == "feImage") { // default to 0%, 0%, 100%, 100% subRegion.setTopLeft(QPointF(0, 0)); subRegion.setSize(QSizeF(1, 1)); } else { // defaults to bounding rect of all referenced nodes Q_FOREACH (const QString &input, filterEffect->inputs()) { if (!inputs.contains(input)) continue; KoFilterEffect *inputFilter = inputs[input]; if (inputFilter) subRegion |= inputFilter->filterRect(); } } } else { const qreal x = parseUnitX(xa); const qreal y = parseUnitY(ya); const qreal w = parseUnitX(wa); const qreal h = parseUnitY(ha); subRegion.setTopLeft(SvgUtil::userSpaceToObject(QPointF(x, y), bound)); subRegion.setSize(SvgUtil::userSpaceToObject(QSizeF(w, h), bound)); } } else { // x, y, width, height are in percentages of the object referencing the filter // so we just parse the percentages const qreal x = SvgUtil::fromPercentage(primitive.attribute("x", "0")); const qreal y = SvgUtil::fromPercentage(primitive.attribute("y", "0")); const qreal w = SvgUtil::fromPercentage(primitive.attribute("width", "1")); const qreal h = SvgUtil::fromPercentage(primitive.attribute("height", "1")); subRegion = QRectF(QPointF(x, y), QSizeF(w, h)); } filterEffect->setFilterRect(subRegion); if (!filterStack) filterStack = new KoFilterEffectStack(); filterStack->appendFilterEffect(filterEffect); inputs[filterEffect->output()] = filterEffect; } if (filterStack) { filterStack->setClipRect(objectFilterRegion); shape->setFilterEffectStack(filterStack); } } void SvgParser::applyMarkers(KoPathShape *shape) { SvgGraphicsContext *gc = m_context.currentGC(); if (!gc) return; if (!gc->markerStartId.isEmpty() && m_markers.contains(gc->markerStartId)) { shape->setMarker(m_markers[gc->markerStartId].data(), KoFlake::StartMarker); } if (!gc->markerMidId.isEmpty() && m_markers.contains(gc->markerMidId)) { shape->setMarker(m_markers[gc->markerMidId].data(), KoFlake::MidMarker); } if (!gc->markerEndId.isEmpty() && m_markers.contains(gc->markerEndId)) { shape->setMarker(m_markers[gc->markerEndId].data(), KoFlake::EndMarker); } shape->setAutoFillMarkers(gc->autoFillMarkers); } void SvgParser::applyClipping(KoShape *shape, const QPointF &shapeToOriginalUserCoordinates) { SvgGraphicsContext *gc = m_context.currentGC(); if (! gc) return; if (gc->clipPathId.isEmpty()) return; SvgClipPathHelper *clipPath = findClipPath(gc->clipPathId); if (!clipPath || clipPath->isEmpty()) return; QList shapes; Q_FOREACH (KoShape *item, clipPath->shapes()) { KoShape *clonedShape = item->cloneShape(); KIS_ASSERT_RECOVER(clonedShape) { continue; } shapes.append(clonedShape); } if (!shapeToOriginalUserCoordinates.isNull()) { const QTransform t = QTransform::fromTranslate(shapeToOriginalUserCoordinates.x(), shapeToOriginalUserCoordinates.y()); Q_FOREACH(KoShape *s, shapes) { s->applyAbsoluteTransformation(t); } } KoClipPath *clipPathObject = new KoClipPath(shapes, clipPath->clipPathUnits() == KoFlake::ObjectBoundingBox ? KoFlake::ObjectBoundingBox : KoFlake::UserSpaceOnUse); shape->setClipPath(clipPathObject); } void SvgParser::applyMaskClipping(KoShape *shape, const QPointF &shapeToOriginalUserCoordinates) { SvgGraphicsContext *gc = m_context.currentGC(); if (!gc) return; if (gc->clipMaskId.isEmpty()) return; QSharedPointer originalClipMask = m_clipMasks.value(gc->clipMaskId); if (!originalClipMask || originalClipMask->isEmpty()) return; KoClipMask *clipMask = originalClipMask->clone(); clipMask->setExtraShapeOffset(shapeToOriginalUserCoordinates); shape->setClipMask(clipMask); } KoShape* SvgParser::parseUse(const KoXmlElement &e, DeferredUseStore* deferredUseStore) { QString href = e.attribute("xlink:href"); if (href.isEmpty()) return 0; QString key = href.mid(1); const bool gotDef = m_context.hasDefinition(key); if (gotDef) { return resolveUse(e, key); } else if (deferredUseStore) { deferredUseStore->add(&e, key); return 0; } debugFlake << "WARNING: Did not find reference for svg 'use' element. Skipping. Id: " << key; return 0; } KoShape* SvgParser::resolveUse(const KoXmlElement &e, const QString& key) { KoShape *result = 0; SvgGraphicsContext *gc = m_context.pushGraphicsContext(e); // TODO: parse 'width' and 'height' as well gc->matrix.translate(parseUnitX(e.attribute("x", "0")), parseUnitY(e.attribute("y", "0"))); const KoXmlElement &referencedElement = m_context.definition(key); result = parseGroup(e, referencedElement); m_context.popGraphicsContext(); return result; } void SvgParser::addToGroup(QList shapes, KoShapeContainer *group) { m_shapes += shapes; if (!group || shapes.isEmpty()) return; // not normalized KoShapeGroupCommand cmd(group, shapes, false); cmd.redo(); } QList SvgParser::parseSvg(const KoXmlElement &e, QSizeF *fragmentSize) { // check if we are the root svg element const bool isRootSvg = m_context.isRootContext(); // parse 'transform' field if preset SvgGraphicsContext *gc = m_context.pushGraphicsContext(e); applyStyle(0, e, QPointF()); const QString w = e.attribute("width"); const QString h = e.attribute("height"); qreal width = w.isEmpty() ? 666.0 : parseUnitX(w); qreal height = h.isEmpty() ? 555.0 : parseUnitY(h); if (w.isEmpty() || h.isEmpty()) { QRectF viewRect; QTransform viewTransform_unused; QRectF fakeBoundingRect(0.0, 0.0, 1.0, 1.0); if (SvgUtil::parseViewBox(e, fakeBoundingRect, &viewRect, &viewTransform_unused)) { QSizeF estimatedSize = viewRect.size(); if (estimatedSize.isValid()) { if (!w.isEmpty()) { estimatedSize = QSizeF(width, width * estimatedSize.height() / estimatedSize.width()); } else if (!h.isEmpty()) { estimatedSize = QSizeF(height * estimatedSize.width() / estimatedSize.height(), height); } width = estimatedSize.width(); height = estimatedSize.height(); } } } QSizeF svgFragmentSize(QSizeF(width, height)); if (fragmentSize) { *fragmentSize = svgFragmentSize; } gc->currentBoundingBox = QRectF(QPointF(0, 0), svgFragmentSize); if (!isRootSvg) { // x and y attribute has no meaning for outermost svg elements const qreal x = parseUnit(e.attribute("x", "0")); const qreal y = parseUnit(e.attribute("y", "0")); QTransform move = QTransform::fromTranslate(x, y); gc->matrix = move * gc->matrix; } applyViewBoxTransform(e); QList shapes; // First find the metadata for (KoXmlNode n = e.firstChild(); !n.isNull(); n = n.nextSibling()) { KoXmlElement b = n.toElement(); if (b.isNull()) continue; if (b.tagName() == "title") { m_documentTitle = b.text().trimmed(); } else if (b.tagName() == "desc") { m_documentDescription = b.text().trimmed(); } else if (b.tagName() == "metadata") { // TODO: parse the metadata } } // SVG 1.1: skip the rendering of the element if it has null viewBox; however an inverted viewbox is just peachy // and as mother makes them -- if mother is inkscape. if (gc->currentBoundingBox.normalized().isValid()) { shapes = parseContainer(e); } m_context.popGraphicsContext(); return shapes; } void SvgParser::applyViewBoxTransform(const KoXmlElement &element) { SvgGraphicsContext *gc = m_context.currentGC(); QRectF viewRect = gc->currentBoundingBox; QTransform viewTransform; if (SvgUtil::parseViewBox(element, gc->currentBoundingBox, &viewRect, &viewTransform)) { gc->matrix = viewTransform * gc->matrix; gc->currentBoundingBox = viewRect; } } QList > SvgParser::knownMarkers() const { return m_markers.values(); } QString SvgParser::documentTitle() const { return m_documentTitle; } QString SvgParser::documentDescription() const { return m_documentDescription; } void SvgParser::setFileFetcher(SvgParser::FileFetcherFunc func) { m_context.setFileFetcher(func); } inline QPointF extraShapeOffset(const KoShape *shape, const QTransform coordinateSystemOnLoading) { const QTransform shapeToOriginalUserCoordinates = shape->absoluteTransformation(0).inverted() * coordinateSystemOnLoading; KIS_SAFE_ASSERT_RECOVER_NOOP(shapeToOriginalUserCoordinates.type() <= QTransform::TxTranslate); return QPointF(shapeToOriginalUserCoordinates.dx(), shapeToOriginalUserCoordinates.dy()); } KoShape* SvgParser::parseGroup(const KoXmlElement &b, const KoXmlElement &overrideChildrenFrom) { m_context.pushGraphicsContext(b); KoShapeGroup *group = new KoShapeGroup(); group->setZIndex(m_context.nextZIndex()); // groups should also have their own coordinate system! group->applyAbsoluteTransformation(m_context.currentGC()->matrix); const QPointF extraOffset = extraShapeOffset(group, m_context.currentGC()->matrix); uploadStyleToContext(b); QList childShapes; if (!overrideChildrenFrom.isNull()) { // we upload styles from both: and uploadStyleToContext(overrideChildrenFrom); childShapes = parseSingleElement(overrideChildrenFrom, 0); } else { childShapes = parseContainer(b); } // handle id applyId(b.attribute("id"), group); addToGroup(childShapes, group); applyCurrentStyle(group, extraOffset); // apply style to this group after size is set m_context.popGraphicsContext(); return group; } KoShape* SvgParser::parseTextNode(const KoXmlText &e) { QScopedPointer textChunk(new KoSvgTextChunkShape()); textChunk->setZIndex(m_context.nextZIndex()); if (!textChunk->loadSvgTextNode(e, m_context)) { return 0; } textChunk->applyAbsoluteTransformation(m_context.currentGC()->matrix); applyCurrentBasicStyle(textChunk.data()); // apply style to this group after size is set return textChunk.take(); } KoXmlText getTheOnlyTextChild(const KoXmlElement &e) { KoXmlNode firstChild = e.firstChild(); return !firstChild.isNull() && firstChild == e.lastChild() && firstChild.isText() ? firstChild.toText() : KoXmlText(); } KoShape *SvgParser::parseTextElement(const KoXmlElement &e, KoSvgTextShape *mergeIntoShape) { KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(e.tagName() == "text" || e.tagName() == "tspan", 0); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(m_isInsideTextSubtree || e.tagName() == "text", 0); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(e.tagName() == "text" || !mergeIntoShape, 0); KoSvgTextShape *rootTextShape = 0; if (e.tagName() == "text") { // XXX: Shapes need to be created by their factories if (mergeIntoShape) { rootTextShape = mergeIntoShape; } else { rootTextShape = new KoSvgTextShape(); const QString useRichText = e.attribute("krita:useRichText", "true"); rootTextShape->setRichTextPreferred(useRichText != "false"); } } if (rootTextShape) { m_isInsideTextSubtree = true; } m_context.pushGraphicsContext(e); uploadStyleToContext(e); KoSvgTextChunkShape *textChunk = rootTextShape ? rootTextShape : new KoSvgTextChunkShape(); if (!mergeIntoShape) { textChunk->setZIndex(m_context.nextZIndex()); } textChunk->loadSvg(e, m_context); // 1) apply transformation only in case we are not overriding the shape! // 2) the transformation should be applied *before* the shape is added to the group! if (!mergeIntoShape) { // groups should also have their own coordinate system! textChunk->applyAbsoluteTransformation(m_context.currentGC()->matrix); const QPointF extraOffset = extraShapeOffset(textChunk, m_context.currentGC()->matrix); // handle id applyId(e.attribute("id"), textChunk); applyCurrentStyle(textChunk, extraOffset); // apply style to this group after size is set } else { m_context.currentGC()->matrix = mergeIntoShape->absoluteTransformation(0); applyCurrentBasicStyle(textChunk); } KoXmlText onlyTextChild = getTheOnlyTextChild(e); if (!onlyTextChild.isNull()) { textChunk->loadSvgTextNode(onlyTextChild, m_context); } else { QList childShapes = parseContainer(e, true); addToGroup(childShapes, textChunk); } m_context.popGraphicsContext(); textChunk->normalizeCharTransformations(); if (rootTextShape) { textChunk->simplifyFillStrokeInheritance(); m_isInsideTextSubtree = false; rootTextShape->relayout(); } return textChunk; } QList SvgParser::parseContainer(const KoXmlElement &e, bool parseTextNodes) { QList shapes; // are we parsing a switch container bool isSwitch = e.tagName() == "switch"; DeferredUseStore deferredUseStore(this); for (KoXmlNode n = e.firstChild(); !n.isNull(); n = n.nextSibling()) { KoXmlElement b = n.toElement(); if (b.isNull()) { if (parseTextNodes && n.isText()) { KoShape *shape = parseTextNode(n.toText()); if (shape) { shapes += shape; } } continue; } if (isSwitch) { // if we are parsing a switch check the requiredFeatures, requiredExtensions // and systemLanguage attributes // TODO: evaluate feature list if (b.hasAttribute("requiredFeatures")) { continue; } if (b.hasAttribute("requiredExtensions")) { // we do not support any extensions continue; } if (b.hasAttribute("systemLanguage")) { // not implemented yet } } QList currentShapes = parseSingleElement(b, &deferredUseStore); shapes.append(currentShapes); // if we are parsing a switch, stop after the first supported element if (isSwitch && !currentShapes.isEmpty()) break; } return shapes; } void SvgParser::parseDefsElement(const KoXmlElement &e) { KIS_SAFE_ASSERT_RECOVER_RETURN(e.tagName() == "defs"); parseSingleElement(e); } QList SvgParser::parseSingleElement(const KoXmlElement &b, DeferredUseStore* deferredUseStore) { QList shapes; // save definition for later instantiation with 'use' m_context.addDefinition(b); if (deferredUseStore) { deferredUseStore->checkPendingUse(b, shapes); } if (b.tagName() == "svg") { shapes += parseSvg(b); } else if (b.tagName() == "g" || b.tagName() == "a") { // treat svg link as group so we don't miss its child elements shapes += parseGroup(b); } else if (b.tagName() == "switch") { m_context.pushGraphicsContext(b); shapes += parseContainer(b); m_context.popGraphicsContext(); } else if (b.tagName() == "defs") { if (KoXml::childNodesCount(b) > 0) { /** * WARNING: 'defs' are basically 'display:none' style, therefore they should not play * any role in shapes outline calculation. But setVisible(false) shapes do! * Should be fixed in the future! */ KoShape *defsShape = parseGroup(b); defsShape->setVisible(false); m_defsShapes << defsShape; // TODO: where to delete the shape!? } } else if (b.tagName() == "linearGradient" || b.tagName() == "radialGradient") { } else if (b.tagName() == "pattern") { } else if (b.tagName() == "filter") { parseFilter(b); } else if (b.tagName() == "clipPath") { parseClipPath(b); } else if (b.tagName() == "mask") { parseClipMask(b); } else if (b.tagName() == "marker") { parseMarker(b); } else if (b.tagName() == "symbol") { parseSymbol(b); } else if (b.tagName() == "style") { m_context.addStyleSheet(b); } else if (b.tagName() == "text" || b.tagName() == "tspan") { shapes += parseTextElement(b); } else if (b.tagName() == "rect" || b.tagName() == "ellipse" || b.tagName() == "circle" || b.tagName() == "line" || b.tagName() == "polyline" || b.tagName() == "polygon" || b.tagName() == "path" || b.tagName() == "image") { KoShape *shape = createObjectDirect(b); if (shape) shapes.append(shape); } else if (b.tagName() == "use") { KoShape* s = parseUse(b, deferredUseStore); if (s) { shapes += s; } } else if (b.tagName() == "color-profile") { m_context.parseProfile(b); } else { // this is an unknown element, so try to load it anyway // there might be a shape that handles that element KoShape *shape = createObject(b); if (shape) { shapes.append(shape); } } return shapes; } // Creating functions // --------------------------------------------------------------------------------------- KoShape * SvgParser::createPath(const KoXmlElement &element) { KoShape *obj = 0; if (element.tagName() == "line") { KoPathShape *path = static_cast(createShape(KoPathShapeId)); if (path) { double x1 = element.attribute("x1").isEmpty() ? 0.0 : parseUnitX(element.attribute("x1")); double y1 = element.attribute("y1").isEmpty() ? 0.0 : parseUnitY(element.attribute("y1")); double x2 = element.attribute("x2").isEmpty() ? 0.0 : parseUnitX(element.attribute("x2")); double y2 = element.attribute("y2").isEmpty() ? 0.0 : parseUnitY(element.attribute("y2")); path->clear(); path->moveTo(QPointF(x1, y1)); path->lineTo(QPointF(x2, y2)); path->normalize(); obj = path; } } else if (element.tagName() == "polyline" || element.tagName() == "polygon") { KoPathShape *path = static_cast(createShape(KoPathShapeId)); if (path) { path->clear(); bool bFirst = true; QStringList pointList = SvgUtil::simplifyList(element.attribute("points")); for (QStringList::Iterator it = pointList.begin(); it != pointList.end(); ++it) { QPointF point; point.setX(SvgUtil::fromUserSpace(KisDomUtils::toDouble(*it))); ++it; if (it == pointList.end()) break; point.setY(SvgUtil::fromUserSpace(KisDomUtils::toDouble(*it))); if (bFirst) { path->moveTo(point); bFirst = false; } else path->lineTo(point); } if (element.tagName() == "polygon") path->close(); path->setPosition(path->normalize()); obj = path; } } else if (element.tagName() == "path") { KoPathShape *path = static_cast(createShape(KoPathShapeId)); if (path) { path->clear(); KoPathShapeLoader loader(path); loader.parseSvg(element.attribute("d"), true); path->setPosition(path->normalize()); QPointF newPosition = QPointF(SvgUtil::fromUserSpace(path->position().x()), SvgUtil::fromUserSpace(path->position().y())); QSizeF newSize = QSizeF(SvgUtil::fromUserSpace(path->size().width()), SvgUtil::fromUserSpace(path->size().height())); path->setSize(newSize); path->setPosition(newPosition); obj = path; } } return obj; } KoShape * SvgParser::createObjectDirect(const KoXmlElement &b) { m_context.pushGraphicsContext(b); uploadStyleToContext(b); KoShape *obj = createShapeFromElement(b, m_context); if (obj) { obj->applyAbsoluteTransformation(m_context.currentGC()->matrix); const QPointF extraOffset = extraShapeOffset(obj, m_context.currentGC()->matrix); applyCurrentStyle(obj, extraOffset); // handle id applyId(b.attribute("id"), obj); obj->setZIndex(m_context.nextZIndex()); } m_context.popGraphicsContext(); return obj; } KoShape * SvgParser::createObject(const KoXmlElement &b, const SvgStyles &style) { m_context.pushGraphicsContext(b); KoShape *obj = createShapeFromElement(b, m_context); if (obj) { obj->applyAbsoluteTransformation(m_context.currentGC()->matrix); const QPointF extraOffset = extraShapeOffset(obj, m_context.currentGC()->matrix); SvgStyles objStyle = style.isEmpty() ? m_context.styleParser().collectStyles(b) : style; m_context.styleParser().parseFont(objStyle); applyStyle(obj, objStyle, extraOffset); // handle id applyId(b.attribute("id"), obj); obj->setZIndex(m_context.nextZIndex()); } m_context.popGraphicsContext(); return obj; } KoShape * SvgParser::createShapeFromElement(const KoXmlElement &element, SvgLoadingContext &context) { KoShape *object = 0; const QString tagName = SvgUtil::mapExtendedShapeTag(element.tagName(), element); QList factories = KoShapeRegistry::instance()->factoriesForElement(KoXmlNS::svg, tagName); foreach (KoShapeFactoryBase *f, factories) { KoShape *shape = f->createDefaultShape(m_documentResourceManager); if (!shape) continue; SvgShape *svgShape = dynamic_cast(shape); if (!svgShape) { delete shape; continue; } // reset transformation that might come from the default shape shape->setTransformation(QTransform()); // reset border KoShapeStrokeModelSP oldStroke = shape->stroke(); shape->setStroke(KoShapeStrokeModelSP()); // reset fill shape->setBackground(QSharedPointer(0)); if (!svgShape->loadSvg(element, context)) { delete shape; continue; } object = shape; break; } if (!object) { object = createPath(element); } return object; } KoShape *SvgParser::createShape(const QString &shapeID) { KoShapeFactoryBase *factory = KoShapeRegistry::instance()->get(shapeID); if (!factory) { debugFlake << "Could not find factory for shape id" << shapeID; return 0; } KoShape *shape = factory->createDefaultShape(m_documentResourceManager); if (!shape) { debugFlake << "Could not create Default shape for shape id" << shapeID; return 0; } if (shape->shapeId().isEmpty()) { shape->setShapeId(factory->id()); } // reset transformation that might come from the default shape shape->setTransformation(QTransform()); // reset border // ??? KoShapeStrokeModelSP oldStroke = shape->stroke(); shape->setStroke(KoShapeStrokeModelSP()); // reset fill shape->setBackground(QSharedPointer(0)); return shape; } void SvgParser::applyId(const QString &id, KoShape *shape) { if (id.isEmpty()) return; shape->setName(id); m_context.registerShape(id, shape); } diff --git a/libs/flake/tests/TestKoMarkerCollection.cpp b/libs/flake/tests/TestKoMarkerCollection.cpp index de05a13edc..ac9f570151 100644 --- a/libs/flake/tests/TestKoMarkerCollection.cpp +++ b/libs/flake/tests/TestKoMarkerCollection.cpp @@ -1,111 +1,112 @@ /* * Copyright (c) 2017 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 "TestKoMarkerCollection.h" #include #include #include +#include #include #include #include #include #include "kis_debug.h" #include "../../sdk/tests/qimage_test_util.h" #include #include void initMarkerCollection(KoMarkerCollection *collection) { QCOMPARE(collection->markers().size(), 1); const QString fileName = TestUtil::fetchDataFileLazy("test_markers.svg"); QVERIFY(QFileInfo(fileName).exists()); collection->loadMarkersFromFile(fileName); QCOMPARE(collection->markers().size(), 10); } void TestKoMarkerCollection::testLoadMarkersFromFile() { KoMarkerCollection collection; initMarkerCollection(&collection); } void TestKoMarkerCollection::testDeduplication() { QPainterPath path1; path1.addRect(QRect(5,5,15,15)); KoPathShape *shape1(KoPathShape::createShapeFromPainterPath(path1)); shape1->setBackground(QSharedPointer(new KoColorBackground(Qt::blue))); KoMarker *marker(new KoMarker()); marker->setAutoOrientation(true); marker->setShapes({shape1}); KoMarkerCollection collection; QCOMPARE(collection.markers().size(), 1); KoMarker *clonedMarker = new KoMarker(*marker); collection.addMarker(marker); QCOMPARE(collection.markers().size(), 2); collection.addMarker(marker); QCOMPARE(collection.markers().size(), 2); collection.addMarker(clonedMarker); QCOMPARE(collection.markers().size(), 2); } void testOneMarkerPosition(KoMarker *marker, KoFlake::MarkerPosition position, const QString &testName) { QImage image(30,30, QImage::Format_ARGB32); image.fill(0); QPainter painter(&image); QPen pen(Qt::black, 2); marker->drawPreview(&painter, image.rect(), pen, position); QVERIFY(TestUtil::checkQImage(image, "marker_collection", "preview", testName)); } void TestKoMarkerCollection::testMarkerBounds() { KoMarkerCollection collection; initMarkerCollection(&collection); QList allMarkers = collection.markers(); KoMarker *marker = allMarkers[3]; QCOMPARE(marker->boundingRect(1, 0).toAlignedRect(), QRect(-7,-3,9,6)); QCOMPARE(marker->boundingRect(1, M_PI).toAlignedRect(), QRect(-2,-3,9,6)); QCOMPARE(marker->outline(1, 0).boundingRect().toAlignedRect(), QRect(-6,-2,7,4)); QCOMPARE(marker->outline(1, M_PI).boundingRect().toAlignedRect(), QRect(-1,-2,7,4)); testOneMarkerPosition(marker, KoFlake::StartMarker, "start_marker"); testOneMarkerPosition(marker, KoFlake::MidMarker, "mid_marker"); testOneMarkerPosition(marker, KoFlake::EndMarker, "end_marker"); } KISTEST_MAIN(TestKoMarkerCollection) diff --git a/libs/flake/tests/TestPointMergeCommand.cpp b/libs/flake/tests/TestPointMergeCommand.cpp index ee6c6477bb..c6b078e5b8 100644 --- a/libs/flake/tests/TestPointMergeCommand.cpp +++ b/libs/flake/tests/TestPointMergeCommand.cpp @@ -1,577 +1,578 @@ /* This file is part of the KDE project * Copyright (C) 2009 Jan Hambrecht * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "TestPointMergeCommand.h" #include "KoPathPointMergeCommand.h" #include "KoPathShape.h" #include "KoPathPoint.h" #include "KoPathPointData.h" #include +#include #include #include void TestPointMergeCommand::closeSingleLinePath() { KoPathShape path1; path1.moveTo(QPointF(40, 0)); path1.lineTo(QPointF(60, 0)); path1.lineTo(QPointF(60, 30)); path1.lineTo(QPointF(0, 30)); path1.lineTo(QPointF(0, 0)); path1.lineTo(QPointF(20, 0)); KoPathPointIndex index1(0,0); KoPathPointIndex index2(0,5); KoPathPointData pd1(&path1, index1); KoPathPointData pd2(&path1, index2); KoPathPoint * p1 = path1.pointByIndex(index1); KoPathPoint * p2 = path1.pointByIndex(index2); QVERIFY(!path1.isClosedSubpath(0)); QCOMPARE(path1.subpathPointCount(0), 6); QCOMPARE(p1->point(), QPointF(40,0)); QCOMPARE(p2->point(), QPointF(20,0)); KoPathPointMergeCommand cmd1(pd1,pd2); cmd1.redo(); QVERIFY(path1.isClosedSubpath(0)); QCOMPARE(path1.subpathPointCount(0), 5); QCOMPARE(p2->point(), QPointF(20,0)); cmd1.undo(); QVERIFY(!path1.isClosedSubpath(0)); QCOMPARE(path1.subpathPointCount(0), 6); QCOMPARE(p1->point(), QPointF(40,0)); QCOMPARE(p2->point(), QPointF(20,0)); KoPathPointMergeCommand cmd2(pd2,pd1); cmd2.redo(); QVERIFY(path1.isClosedSubpath(0)); QCOMPARE(path1.subpathPointCount(0), 5); QCOMPARE(p2->point(), QPointF(20,0)); cmd2.undo(); QVERIFY(!path1.isClosedSubpath(0)); QCOMPARE(path1.subpathPointCount(0), 6); QCOMPARE(p1->point(), QPointF(40,0)); QCOMPARE(p2->point(), QPointF(20,0)); } void TestPointMergeCommand::closeSingleCurvePath() { KoPathShape path1; path1.moveTo(QPointF(40, 0)); path1.curveTo(QPointF(60, 0), QPointF(60,0), QPointF(60,60)); path1.lineTo(QPointF(0, 60)); path1.curveTo(QPointF(0, 0), QPointF(0,0), QPointF(20,0)); KoPathPointIndex index1(0,0); KoPathPointIndex index2(0,3); KoPathPointData pd1(&path1, index1); KoPathPointData pd2(&path1, index2); KoPathPoint * p1 = path1.pointByIndex(index1); KoPathPoint * p2 = path1.pointByIndex(index2); QVERIFY(!path1.isClosedSubpath(0)); QCOMPARE(path1.subpathPointCount(0), 4); QCOMPARE(p1->point(), QPointF(40,0)); QVERIFY(!p1->activeControlPoint1()); QCOMPARE(p2->point(), QPointF(20,0)); QVERIFY(!p2->activeControlPoint2()); KoPathPointMergeCommand cmd1(pd1,pd2); cmd1.redo(); QVERIFY(path1.isClosedSubpath(0)); QCOMPARE(path1.subpathPointCount(0), 3); QCOMPARE(p2->point(), QPointF(20,0)); QVERIFY(p2->activeControlPoint1()); QVERIFY(!p2->activeControlPoint2()); cmd1.undo(); QVERIFY(!path1.isClosedSubpath(0)); QCOMPARE(path1.subpathPointCount(0), 4); QCOMPARE(p1->point(), QPointF(40,0)); QVERIFY(!p1->activeControlPoint1()); QCOMPARE(p2->point(), QPointF(20,0)); QVERIFY(!p2->activeControlPoint2()); KoPathPointMergeCommand cmd2(pd2,pd1); cmd2.redo(); QVERIFY(path1.isClosedSubpath(0)); QCOMPARE(path1.subpathPointCount(0), 3); QCOMPARE(p2->point(), QPointF(20,0)); QVERIFY(p2->activeControlPoint1()); QVERIFY(!p2->activeControlPoint2()); cmd2.undo(); QVERIFY(!path1.isClosedSubpath(0)); QCOMPARE(path1.subpathPointCount(0), 4); QCOMPARE(p1->point(), QPointF(40,0)); QVERIFY(!p1->activeControlPoint1()); QCOMPARE(p2->point(), QPointF(20,0)); QVERIFY(!p2->activeControlPoint2()); } void TestPointMergeCommand::connectLineSubpaths() { KoPathShape path1; path1.moveTo(QPointF(0,0)); path1.lineTo(QPointF(10,0)); path1.moveTo(QPointF(20,0)); path1.lineTo(QPointF(30,0)); KoPathPointIndex index1(0,1); KoPathPointIndex index2(1,0); KoPathPointData pd1(&path1, index1); KoPathPointData pd2(&path1, index2); QCOMPARE(path1.subpathCount(), 2); QCOMPARE(path1.pointByIndex(index1)->point(), QPointF(10,0)); QCOMPARE(path1.pointByIndex(index2)->point(), QPointF(20,0)); KoPathPointMergeCommand cmd1(pd1, pd2); cmd1.redo(); QCOMPARE(path1.subpathCount(), 1); QCOMPARE(path1.pointByIndex(index1)->point(), QPointF(15,0)); cmd1.undo(); QCOMPARE(path1.subpathCount(), 2); QCOMPARE(path1.pointByIndex(index1)->point(), QPointF(10,0)); QCOMPARE(path1.pointByIndex(index2)->point(), QPointF(20,0)); KoPathPointMergeCommand cmd2(pd2, pd1); cmd2.redo(); QCOMPARE(path1.subpathCount(), 1); QCOMPARE(path1.pointByIndex(index1)->point(), QPointF(15,0)); cmd2.undo(); QCOMPARE(path1.subpathCount(), 2); QCOMPARE(path1.pointByIndex(index1)->point(), QPointF(10,0)); QCOMPARE(path1.pointByIndex(index2)->point(), QPointF(20,0)); } void TestPointMergeCommand::connectCurveSubpaths() { KoPathShape path1; path1.moveTo(QPointF(0,0)); path1.curveTo(QPointF(20,0),QPointF(0,20),QPointF(20,20)); path1.moveTo(QPointF(50,0)); path1.curveTo(QPointF(30,0), QPointF(50,20), QPointF(30,20)); KoPathPointIndex index1(0,1); KoPathPointIndex index2(1,1); KoPathPointData pd1(&path1, index1); KoPathPointData pd2(&path1, index2); QCOMPARE(path1.subpathCount(), 2); QCOMPARE(path1.pointByIndex(index1)->point(), QPointF(20,20)); QCOMPARE(path1.pointByIndex(index1)->controlPoint1(), QPointF(0,20)); QCOMPARE(path1.pointByIndex(index2)->point(), QPointF(30,20)); QCOMPARE(path1.pointByIndex(index2)->controlPoint1(), QPointF(50,20)); QVERIFY(path1.pointByIndex(index1)->activeControlPoint1()); QVERIFY(!path1.pointByIndex(index1)->activeControlPoint2()); KoPathPointMergeCommand cmd1(pd1, pd2); cmd1.redo(); QCOMPARE(path1.subpathCount(), 1); QCOMPARE(path1.pointByIndex(index1)->point(), QPointF(25,20)); QCOMPARE(path1.pointByIndex(index1)->controlPoint1(), QPointF(5,20)); QCOMPARE(path1.pointByIndex(index1)->controlPoint2(), QPointF(45,20)); QVERIFY(path1.pointByIndex(index1)->activeControlPoint1()); QVERIFY(path1.pointByIndex(index1)->activeControlPoint2()); cmd1.undo(); QCOMPARE(path1.subpathCount(), 2); QCOMPARE(path1.pointByIndex(index1)->point(), QPointF(20,20)); QCOMPARE(path1.pointByIndex(index1)->controlPoint1(), QPointF(0,20)); QCOMPARE(path1.pointByIndex(index2)->point(), QPointF(30,20)); QCOMPARE(path1.pointByIndex(index2)->controlPoint1(), QPointF(50,20)); QVERIFY(path1.pointByIndex(index1)->activeControlPoint1()); QVERIFY(!path1.pointByIndex(index1)->activeControlPoint2()); KoPathPointMergeCommand cmd2(pd2, pd1); cmd2.redo(); QCOMPARE(path1.subpathCount(), 1); QCOMPARE(path1.pointByIndex(index1)->point(), QPointF(25,20)); QCOMPARE(path1.pointByIndex(index1)->controlPoint1(), QPointF(5,20)); QCOMPARE(path1.pointByIndex(index1)->controlPoint2(), QPointF(45,20)); QVERIFY(path1.pointByIndex(index1)->activeControlPoint1()); QVERIFY(path1.pointByIndex(index1)->activeControlPoint2()); cmd2.undo(); QCOMPARE(path1.subpathCount(), 2); QCOMPARE(path1.pointByIndex(index1)->point(), QPointF(20,20)); QCOMPARE(path1.pointByIndex(index1)->controlPoint1(), QPointF(0,20)); QCOMPARE(path1.pointByIndex(index2)->point(), QPointF(30,20)); QCOMPARE(path1.pointByIndex(index2)->controlPoint1(), QPointF(50,20)); QVERIFY(path1.pointByIndex(index1)->activeControlPoint1()); QVERIFY(!path1.pointByIndex(index1)->activeControlPoint2()); } #include #include #include "kis_debug.h" void TestPointMergeCommand::testCombineShapes() { MockShapeController mockController; MockCanvas canvas(&mockController); QList shapesToCombine; for (int i = 0; i < 3; i++) { const QPointF step(15,15); const QRectF rect = QRectF(5,5,10,10).translated(step * i); QPainterPath p; p.addRect(rect); KoPathShape *shape = KoPathShape::createShapeFromPainterPath(p); QCOMPARE(shape->absoluteOutlineRect(), rect); shapesToCombine << shape; mockController.addShape(shape); } KoPathCombineCommand cmd(&mockController, shapesToCombine); cmd.redo(); QCOMPARE(canvas.shapeManager()->shapes().size(), 1); KoPathShape *combinedShape = dynamic_cast(canvas.shapeManager()->shapes().first()); QCOMPARE(combinedShape, cmd.combinedPath()); QCOMPARE(combinedShape->subpathCount(), 3); QCOMPARE(combinedShape->absoluteOutlineRect(), QRectF(5,5,40,40)); QList tstPoints; QList expPoints; tstPoints << KoPathPointData(shapesToCombine[0], KoPathPointIndex(0,1)); expPoints << KoPathPointData(combinedShape, KoPathPointIndex(0,1)); tstPoints << KoPathPointData(shapesToCombine[1], KoPathPointIndex(0,2)); expPoints << KoPathPointData(combinedShape, KoPathPointIndex(1,2)); tstPoints << KoPathPointData(shapesToCombine[2], KoPathPointIndex(0,3)); expPoints << KoPathPointData(combinedShape, KoPathPointIndex(2,3)); for (int i = 0; i < tstPoints.size(); i++) { KoPathPointData convertedPoint = cmd.originalToCombined(tstPoints[i]); QCOMPARE(convertedPoint, expPoints[i]); } Q_FOREACH (KoShape *shape, canvas.shapeManager()->shapes()) { mockController.removeShape(shape); shape->setParent(0); delete shape; } // 'shapesToCombine' will be deleted by KoPathCombineCommand } #include #include #include #include "kis_algebra_2d.h" inline QPointF fetchPoint(KoPathShape *shape, int subpath, int pointIndex) { return shape->absoluteTransformation(0).map( shape->pointByIndex(KoPathPointIndex(subpath, pointIndex))->point()); } void dumpShape(KoPathShape *shape, const QString &fileName) { QImage tmp(50,50, QImage::Format_ARGB32); tmp.fill(0); QPainter p(&tmp); p.drawPath(shape->absoluteTransformation(0).map(shape->outline())); tmp.save(fileName); } template void testMultipathMergeShapesImpl(const int srcPointIndex1, const int srcPointIndex2, const QList &expectedResultPoints, bool singleShape = false) { MockShapeController mockController; MockCanvas canvas(&mockController); QList shapes; for (int i = 0; i < 3; i++) { const QPointF step(15,15); const QRectF rect = QRectF(5,5,10,10).translated(step * i); QPainterPath p; p.moveTo(rect.topLeft()); p.lineTo(rect.bottomRight()); p.lineTo(rect.topRight()); KoPathShape *shape = KoPathShape::createShapeFromPainterPath(p); QCOMPARE(shape->absoluteOutlineRect(), rect); shapes << shape; mockController.addShape(shape); } { KoPathPointData pd1(shapes[0], KoPathPointIndex(0,srcPointIndex1)); KoPathPointData pd2(shapes[singleShape ? 0 : 1], KoPathPointIndex(0,srcPointIndex2)); MergeCommand cmd(pd1, pd2, &mockController, canvas.shapeManager()->selection()); cmd.redo(); const int expectedShapesCount = singleShape ? 3 : 2; QCOMPARE(canvas.shapeManager()->shapes().size(), expectedShapesCount); KoPathShape *combinedShape = 0; if (!singleShape) { combinedShape = dynamic_cast(canvas.shapeManager()->shapes()[1]); QCOMPARE(combinedShape, cmd.testingCombinedPath()); } else { combinedShape = dynamic_cast(canvas.shapeManager()->shapes()[0]); QCOMPARE(combinedShape, shapes[0]); } QCOMPARE(combinedShape->subpathCount(), 1); QRectF expectedOutlineRect; KisAlgebra2D::accumulateBounds(expectedResultPoints, &expectedOutlineRect); QVERIFY(KisAlgebra2D::fuzzyCompareRects(combinedShape->absoluteOutlineRect(), expectedOutlineRect, 0.01)); if (singleShape) { QCOMPARE(combinedShape->isClosedSubpath(0), true); } QCOMPARE(combinedShape->subpathPointCount(0), expectedResultPoints.size()); for (int i = 0; i < expectedResultPoints.size(); i++) { if (fetchPoint(combinedShape, 0, i) != expectedResultPoints[i]) { qDebug() << ppVar(i); qDebug() << ppVar(fetchPoint(combinedShape, 0, i)); qDebug() << ppVar(expectedResultPoints[i]); QFAIL("Resulting shape points are different!"); } } QList shapes = canvas.shapeManager()->selection()->selectedEditableShapes(); QCOMPARE(shapes.size(), 1); QCOMPARE(shapes.first(), combinedShape); //dumpShape(combinedShape, "tmp_0_seq.png"); cmd.undo(); QCOMPARE(canvas.shapeManager()->shapes().size(), 3); } Q_FOREACH (KoShape *shape, canvas.shapeManager()->shapes()) { mockController.removeShape(shape); shape->setParent(0); delete shape; } // combined shapes will be deleted by the corresponding commands } void TestPointMergeCommand::testMultipathMergeShapesBothSequential() { // both sequential testMultipathMergeShapesImpl(2, 0, { QPointF(5,5), QPointF(15,15), QPointF(17.5,12.5), // merged by melding the points! QPointF(30,30), QPointF(30,20) }); } void TestPointMergeCommand::testMultipathMergeShapesFirstReversed() { // first reversed testMultipathMergeShapesImpl(0, 0, { QPointF(15,5), QPointF(15,15), QPointF(12.5,12.5), // merged by melding the points! QPointF(30,30), QPointF(30,20) }); } void TestPointMergeCommand::testMultipathMergeShapesSecondReversed() { // second reversed testMultipathMergeShapesImpl(2, 2, { QPointF(5,5), QPointF(15,15), QPointF(22.5,12.5), // merged by melding the points! QPointF(30,30), QPointF(20,20) }); } void TestPointMergeCommand::testMultipathMergeShapesBothReversed() { // both reversed testMultipathMergeShapesImpl(0, 2, { QPointF(15,5), QPointF(15,15), QPointF(17.5,12.5), // merged by melding the points! QPointF(30,30), QPointF(20,20) }); } void TestPointMergeCommand::testMultipathMergeShapesSingleShapeEndToStart() { // close end->start testMultipathMergeShapesImpl(2, 0, { QPointF(10,5), QPointF(15,15) }, true); } void TestPointMergeCommand::testMultipathMergeShapesSingleShapeStartToEnd() { // close start->end testMultipathMergeShapesImpl(0, 2, { QPointF(10,5), QPointF(15,15) }, true); } void TestPointMergeCommand::testMultipathJoinShapesBothSequential() { // both sequential testMultipathMergeShapesImpl (2, 0, { QPointF(5,5), QPointF(15,15), QPointF(15,5), QPointF(20,20), QPointF(30,30), QPointF(30,20) }); } void TestPointMergeCommand::testMultipathJoinShapesFirstReversed() { // first reversed testMultipathMergeShapesImpl (0, 0, { QPointF(15,5), QPointF(15,15), QPointF(5,5), QPointF(20,20), QPointF(30,30), QPointF(30,20) }); } void TestPointMergeCommand::testMultipathJoinShapesSecondReversed() { // second reversed testMultipathMergeShapesImpl (2, 2, { QPointF(5,5), QPointF(15,15), QPointF(15,5), QPointF(30,20), QPointF(30,30), QPointF(20,20) }); } void TestPointMergeCommand::testMultipathJoinShapesBothReversed() { // both reversed testMultipathMergeShapesImpl (0, 2, { QPointF(15,5), QPointF(15,15), QPointF(5,5), QPointF(30,20), QPointF(30,30), QPointF(20,20) }); } void TestPointMergeCommand::testMultipathJoinShapesSingleShapeEndToStart() { // close end->start testMultipathMergeShapesImpl (2, 0, { QPointF(5,5), QPointF(15,15), QPointF(15,5) }, true); } void TestPointMergeCommand::testMultipathJoinShapesSingleShapeStartToEnd() { // close start->end testMultipathMergeShapesImpl (0, 2, { QPointF(5,5), QPointF(15,15), QPointF(15,5) }, true); } KISTEST_MAIN(TestPointMergeCommand) diff --git a/libs/flake/tests/TestShapeBackgroundCommand.cpp b/libs/flake/tests/TestShapeBackgroundCommand.cpp index a3a44cd46d..a8a8c164a6 100644 --- a/libs/flake/tests/TestShapeBackgroundCommand.cpp +++ b/libs/flake/tests/TestShapeBackgroundCommand.cpp @@ -1,72 +1,73 @@ /* This file is part of the KDE project * Copyright (C) 2008 Jan Hambrecht * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "TestShapeBackgroundCommand.h" #include #include "KoShapeBackgroundCommand.h" #include "KoColorBackground.h" #include "KoShapePaintingContext.h" #include "KoViewConverter.h" +#include #include void TestShapeBackgroundCommand::refCounting() { MockShape * shape1 = new MockShape(); QSharedPointer whiteFill(new KoColorBackground(QColor(Qt::white))); QSharedPointer blackFill(new KoColorBackground(QColor(Qt::black))); QSharedPointer redFill (new KoColorBackground(QColor(Qt::red))); shape1->setBackground(whiteFill); QVERIFY(shape1->background() == whiteFill); // old fill is white, new fill is black KUndo2Command *cmd1 = new KoShapeBackgroundCommand(shape1, blackFill); cmd1->redo(); QVERIFY(shape1->background() == blackFill); // change fill back to white fill cmd1->undo(); QVERIFY(shape1->background() == whiteFill); // old fill is white, new fill is red KUndo2Command *cmd2 = new KoShapeBackgroundCommand(shape1, redFill); cmd2->redo(); QVERIFY(shape1->background() == redFill); // this command has the white fill as the old fill delete cmd1; // set fill back to white fill cmd2->undo(); QVERIFY(shape1->background() == whiteFill); // if white is deleted when deleting cmd1 this will crash QPainter p; QPainterPath path; path.addRect( QRectF(0,0,100,100) ); KoViewConverter converter; KoShapePaintingContext context; whiteFill->paint( p, converter, context, path ); delete cmd2; delete shape1; } QTEST_MAIN(TestShapeBackgroundCommand) diff --git a/libs/flake/tests/TestSnapStrategy.cpp b/libs/flake/tests/TestSnapStrategy.cpp index 8df1995908..70342dc9bc 100644 --- a/libs/flake/tests/TestSnapStrategy.cpp +++ b/libs/flake/tests/TestSnapStrategy.cpp @@ -1,840 +1,841 @@ /* Copyright (C) 2012 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; either version 2.1 of the License, or (at your option) any later version. 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 library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "TestSnapStrategy.h" +#include #include #include "KoSnapStrategy.h" #include "KoPathShape.h" #include "KoSnapProxy.h" #include "KoShapeControllerBase.h" #include "MockShapes.h" #include "KoPathPoint.h" #include "KoViewConverter.h" #include //#include #include void TestSnapStrategy::testOrthogonalSnap() { //Test case one - expected not to snap OrthogonalSnapStrategy toTest; const QPointF paramMousePosition; MockShapeController fakeShapeControllerBase; MockCanvas fakeKoCanvasBase(&fakeShapeControllerBase); //the shapeManager() function of this will be called KoSnapGuide aKoSnapGuide(&fakeKoCanvasBase); //the call that will be made to the snap guide created is m_snapGuide->canvas()->shapeManager()->shapes(); KoSnapProxy paramProxy(&aKoSnapGuide); //param proxy will have no shapes hence it will not snap qreal paramSnapDistance = 0; bool didSnap = toTest.snap(paramMousePosition, ¶mProxy, paramSnapDistance); QVERIFY(!didSnap); //Second test case - makes sure the there are shapes in the fakeShapeControllerBase thus it should snap OrthogonalSnapStrategy toTestTwo; //paramMousePosition must be within paramSnapDistance of the points in firstSnapPointList const QPointF paramMousePositionTwo(3,3); MockShapeController fakeShapeControllerBaseTwo; //This call will be made on the paramProxy: proxy->pointsFromShape(shape) which in turn //will make this call shape->snapData().snapPoints(); so the shapes have to have snapPoints //In order to have snapPoints we have to use the call //shape->snapData().setSnapPoints() for each fakeShape, where we send in a const //QList &snapPoints in order to have snapPoints to iterate - which is the only //way to change the value of minHorzDist and minVertDist in KoSnapStrategy.cpp so it //differs from HUGE_VAL - i.e. gives us the true value for the snap function. //creating the lists of points //example QList pts; pts.push_back(QPointF(0.2, 0.3)); pts.push_back(QPointF(0.5, 0.7)); MockCanvas fakeKoCanvasBaseTwo(&fakeShapeControllerBaseTwo); //the shapeManager() function of this will be called KoShapeManager *fakeShapeManager = fakeKoCanvasBaseTwo.shapeManager(); MockShape fakeShapeOne; QList firstSnapPointList; firstSnapPointList.push_back(QPointF(1,2)); firstSnapPointList.push_back(QPointF(2,2)); firstSnapPointList.push_back(QPointF(3,2)); firstSnapPointList.push_back(QPointF(4,2)); fakeShapeOne.snapData().setSnapPoints(firstSnapPointList); fakeShapeOne.isVisible(true); fakeShapeManager->addShape(&fakeShapeOne); KoSnapGuide aKoSnapGuideTwo(&fakeKoCanvasBaseTwo); //the call that will be made to the snap guide created is m_snapGuide->canvas()->shapeManager()->shapes(); KoSnapProxy paramProxyTwo(&aKoSnapGuideTwo); //param proxy will have shapes now //Make sure at least one point in firstSnapPointList is within this distance of //paramMousePoint to trigger the branches if (dx < minHorzDist && dx < maxSnapDistance) //and if (dy < minVertDist && dy < maxSnapDistance) WHICH IS WHERE minVertDist and minHorzDist //ARE CHANGED FROM HUGE_VAL qreal paramSnapDistanceTwo = 4; bool didSnapTwo = toTestTwo.snap(paramMousePositionTwo, ¶mProxyTwo, paramSnapDistanceTwo); QVERIFY(didSnapTwo); // don't forget to remove the shape from the shape manager before exiting! fakeShapeManager->remove(&fakeShapeOne); } void TestSnapStrategy::testNodeSnap() { //Test case one - expected to not snap NodeSnapStrategy toTest; const QPointF paramMousePos; MockShapeController fakeShapeControllerBase; MockCanvas fakeKoCanvasBase(&fakeShapeControllerBase); KoSnapGuide aKoSnapGuide(&fakeKoCanvasBase); KoSnapProxy paramProxy(&aKoSnapGuide); qreal paramSnapDistance = 0; bool didSnap = toTest.snap(paramMousePos, ¶mProxy, paramSnapDistance); QVERIFY(!didSnap); //Test case two - exercising the branches by putting a shape and snap points into the ShapeManager NodeSnapStrategy toTestTwo; const QPointF paramMousePosTwo; MockShapeController fakeShapeControllerBaseTwo; MockCanvas fakeKoCanvasBaseTwo(&fakeShapeControllerBaseTwo); KoShapeManager *fakeShapeManager = fakeKoCanvasBaseTwo.shapeManager(); MockShape fakeShapeOne; QList firstSnapPointList; firstSnapPointList.push_back(QPointF(1,2)); firstSnapPointList.push_back(QPointF(2,2)); firstSnapPointList.push_back(QPointF(3,2)); firstSnapPointList.push_back(QPointF(4,2)); qreal paramSnapDistanceTwo = 4; fakeShapeOne.snapData().setSnapPoints(firstSnapPointList); fakeShapeOne.isVisible(true); fakeShapeManager->addShape(&fakeShapeOne); KoSnapGuide aKoSnapGuideTwo(&fakeKoCanvasBaseTwo); KoSnapProxy paramProxyTwo(&aKoSnapGuideTwo); bool didSnapTwo = toTestTwo.snap(paramMousePosTwo, ¶mProxyTwo, paramSnapDistanceTwo); QVERIFY(didSnapTwo); // don't forget to remove the shape from the shape manager before exiting! fakeShapeManager->remove(&fakeShapeOne); } void TestSnapStrategy::testExtensionSnap() { //bool ExtensionSnapStrategy::snap(const QPointF &mousePosition, KoSnapProxy * proxy, qreal maxSnapDistance) ExtensionSnapStrategy toTest; const QPointF paramMousePos; MockShapeController fakeShapeControllerBase; MockCanvas fakeKoCanvasBase(&fakeShapeControllerBase); KoSnapGuide aKoSnapGuide(&fakeKoCanvasBase); KoSnapProxy paramProxy(&aKoSnapGuide); qreal paramSnapDistance = 0; bool didSnap = toTest.snap(paramMousePos, ¶mProxy, paramSnapDistance); QVERIFY(!didSnap); //Second test case - testing the snap by providing ShapeManager with a shape that has snap points and a path //fakeShapeOne needs at least one subpath that is open in order to change the values of minDistances //which in turn opens the path where it is possible to get a true bool value back from the snap function // KoPathPointIndex openSubpath(const KoPathPointIndex &pointIndex); in KoPathShape needs to be called ExtensionSnapStrategy toTestTwo; const QPointF paramMousePosTwo; MockShapeController fakeShapeControllerBaseTwo; MockCanvas fakeKoCanvasBaseTwo(&fakeShapeControllerBaseTwo); KoShapeManager *fakeShapeManager = fakeKoCanvasBaseTwo.shapeManager(); KoPathShape fakeShapeOne; QList firstSnapPointList; firstSnapPointList.push_back(QPointF(1,2)); firstSnapPointList.push_back(QPointF(2,2)); firstSnapPointList.push_back(QPointF(3,2)); firstSnapPointList.push_back(QPointF(4,2)); qreal paramSnapDistanceTwo = 4; fakeShapeOne.snapData().setSnapPoints(firstSnapPointList); fakeShapeOne.isVisible(true); QPointF firstPoint(0,2); QPointF secondPoint(1,2); QPointF thirdPoint(2,3); QPointF fourthPoint(3,4); fakeShapeOne.moveTo(firstPoint); fakeShapeOne.lineTo(secondPoint); fakeShapeOne.lineTo(thirdPoint); fakeShapeOne.lineTo(fourthPoint); fakeShapeManager->addShape(&fakeShapeOne); KoSnapGuide aKoSnapGuideTwo(&fakeKoCanvasBaseTwo); KoSnapProxy paramProxyTwo(&aKoSnapGuideTwo); bool didSnapTwo = toTest.snap(paramMousePosTwo, ¶mProxyTwo, paramSnapDistanceTwo); QVERIFY(didSnapTwo); // don't forget to remove the shape from the shape manager before exiting! fakeShapeManager->remove(&fakeShapeOne); } void TestSnapStrategy::testIntersectionSnap() { //Testing so it does not work without a path IntersectionSnapStrategy toTest; const QPointF paramMousePos; MockShapeController fakeShapeControllerBase; MockCanvas fakeKoCanvasBase(&fakeShapeControllerBase); KoSnapGuide aKoSnapGuide(&fakeKoCanvasBase); KoSnapProxy paramProxy(&aKoSnapGuide); qreal paramSnapDistance = 0; bool didSnap = toTest.snap(paramMousePos, ¶mProxy, paramSnapDistance); QVERIFY(!didSnap); //Exercising the working snap by providing the shape manager with three path shapes //In order for this test to work the shapeManager has to have more than one fakeShape in it //(requirement in QList KoShapeManager::shapesAt(const QRectF &rect, bool omitHiddenShapes) //And both shapes have to be not-visible IntersectionSnapStrategy toTestTwo; const QPointF paramMousePosTwo; MockShapeController fakeShapeControllerBaseTwo; MockCanvas fakeKoCanvasBaseTwo(&fakeShapeControllerBaseTwo); KoShapeManager *ShapeManager = fakeKoCanvasBaseTwo.shapeManager(); qreal paramSnapDistanceTwo = 8; KoPathShape pathShapeOne; QList firstSnapPointList; pathShapeOne.moveTo(QPointF(1,2)); pathShapeOne.lineTo(QPointF(2,2)); pathShapeOne.lineTo(QPointF(3,2)); pathShapeOne.lineTo(QPointF(4,2)); //pathShapeOne.snapData().setSnapPoints(firstSnapPointList); pathShapeOne.isVisible(true); ShapeManager->addShape(&pathShapeOne); KoPathShape pathShapeTwo; QList secondSnapPointList; pathShapeTwo.moveTo(QPointF(1,1)); pathShapeTwo.lineTo(QPointF(2,2)); pathShapeTwo.lineTo(QPointF(3,3)); pathShapeTwo.lineTo(QPointF(4,4)); //pathShapeTwo.snapData().setSnapPoints(secondSnapPointList); pathShapeTwo.isVisible(true); ShapeManager->addShape(&pathShapeTwo); KoPathShape pathShapeThree; QList thirdSnapPointList; pathShapeThree.moveTo(QPointF(5,5)); pathShapeThree.lineTo(QPointF(6,6)); pathShapeThree.lineTo(QPointF(7,7)); pathShapeThree.lineTo(QPointF(8,8)); pathShapeThree.isVisible(true); ShapeManager->addShape(&pathShapeThree); KoSnapGuide aKoSnapGuideTwo(&fakeKoCanvasBaseTwo); KoSnapProxy paramProxyTwo(&aKoSnapGuideTwo); bool didSnapTwo = toTestTwo.snap(paramMousePosTwo, ¶mProxyTwo, paramSnapDistanceTwo); QVERIFY(didSnapTwo); // don't forget to remove the shape from the shape manager before exiting! ShapeManager->remove(&pathShapeOne); ShapeManager->remove(&pathShapeTwo); ShapeManager->remove(&pathShapeThree); } void TestSnapStrategy::testGridSnap() { //This test is the default case - meant to fail since the grid of the SnapGuide is not set GridSnapStrategy toTest; const QPointF paramMousePos; MockShapeController fakeShapeControllerBase; MockCanvas fakeKoCanvasBase(&fakeShapeControllerBase); KoSnapGuide aKoSnapGuide(&fakeKoCanvasBase); KoSnapProxy paramProxy(&aKoSnapGuide); qreal paramSnapDistance = 0; bool didSnap = toTest.snap(paramMousePos, ¶mProxy, paramSnapDistance); QVERIFY(!didSnap); //This test tests the snapping by providing the SnapGuide with a grid to snap against GridSnapStrategy toTestTwo; const QPointF paramMousePosTwo(40,60); MockShapeController fakeShapeControllerBaseTwo; MockCanvas fakeKoCanvasBaseTwo(&fakeShapeControllerBaseTwo); fakeKoCanvasBaseTwo.setHorz(10); fakeKoCanvasBaseTwo.setVert(8); KoSnapGuide aKoSnapGuideTwo(&fakeKoCanvasBaseTwo); KoSnapProxy paramProxyTwo(&aKoSnapGuideTwo); qreal paramSnapDistanceTwo = 8; bool didSnapTwo = toTestTwo.snap(paramMousePosTwo, ¶mProxyTwo, paramSnapDistanceTwo); QVERIFY(didSnapTwo); } void TestSnapStrategy::testBoundingBoxSnap() { //Tests so the snap does not work when there is no shape with a path BoundingBoxSnapStrategy toTest; const QPointF paramMousePos; MockShapeController fakeShapeControllerBase; MockCanvas fakeKoCanvasBase(&fakeShapeControllerBase); KoSnapGuide aKoSnapGuide(&fakeKoCanvasBase); KoSnapProxy paramProxy(&aKoSnapGuide); qreal paramSnapDistance = 0; bool didSnap = toTest.snap(paramMousePos, ¶mProxy, paramSnapDistance); QVERIFY(!didSnap); //tests the snap by providing three path shapes to the shape manager BoundingBoxSnapStrategy toTestTwo; const QPointF paramMousePosTwo; MockShapeController fakeShapeControllerBaseTwo; MockCanvas fakeKoCanvasBaseTwo(&fakeShapeControllerBaseTwo); KoShapeManager *ShapeManager = fakeKoCanvasBaseTwo.shapeManager(); qreal paramSnapDistanceTwo = 8; KoPathShape pathShapeOne; QList firstSnapPointList; pathShapeOne.moveTo(QPointF(1,2)); pathShapeOne.lineTo(QPointF(2,2)); pathShapeOne.lineTo(QPointF(3,2)); pathShapeOne.lineTo(QPointF(4,2)); pathShapeOne.isVisible(true); ShapeManager->addShape(&pathShapeOne); KoPathShape pathShapeTwo; QList secondSnapPointList; pathShapeTwo.moveTo(QPointF(1,1)); pathShapeTwo.lineTo(QPointF(2,2)); pathShapeTwo.lineTo(QPointF(3,3)); pathShapeTwo.lineTo(QPointF(4,4)); pathShapeTwo.isVisible(true); ShapeManager->addShape(&pathShapeTwo); KoPathShape pathShapeThree; QList thirdSnapPointList; pathShapeThree.moveTo(QPointF(5,5)); pathShapeThree.lineTo(QPointF(6,6)); pathShapeThree.lineTo(QPointF(7,7)); pathShapeThree.lineTo(QPointF(8,8)); pathShapeThree.isVisible(true); ShapeManager->addShape(&pathShapeThree); KoSnapGuide aKoSnapGuideTwo(&fakeKoCanvasBaseTwo); KoSnapProxy paramProxyTwo(&aKoSnapGuideTwo); bool didSnapTwo = toTestTwo.snap(paramMousePosTwo, ¶mProxyTwo, paramSnapDistanceTwo); QVERIFY(didSnapTwo); // don't forget to remove the shape from the shape manager before exiting! ShapeManager->remove(&pathShapeOne); ShapeManager->remove(&pathShapeTwo); ShapeManager->remove(&pathShapeThree); } void TestSnapStrategy::testLineGuideSnap() { // KoGuides data has been moved into Krita // // //Testing so the snap does not work without horizontal and vertial lines // LineGuideSnapStrategy toTest; // const QPointF paramMousePos; // MockShapeController fakeShapeControllerBase; // MockCanvas fakeKoCanvasBase(&fakeShapeControllerBase); // KoSnapGuide aKoSnapGuide(&fakeKoCanvasBase); // KoSnapProxy paramProxy(&aKoSnapGuide); // qreal paramSnapDistance = 0; // bool didSnap = toTest.snap(paramMousePos, ¶mProxy, paramSnapDistance); // QVERIFY(!didSnap); // //Test case that covers the path of the snap by providing horizontal and vertical lines for the GuidesData // LineGuideSnapStrategy toTestTwo; // const QPointF paramMousePosTwo; // MockShapeController fakeShapeControllerBaseTwo; // MockCanvas fakeKoCanvasBaseTwo(&fakeShapeControllerBaseTwo); // KoGuidesData guidesData; // QList horzLines; // horzLines.push_back(2); // horzLines.push_back(3); // horzLines.push_back(4); // horzLines.push_back(5); // QList vertLines; // vertLines.push_back(1); // vertLines.push_back(2); // vertLines.push_back(3); // vertLines.push_back(4); // guidesData.setHorizontalGuideLines(horzLines); // guidesData.setVerticalGuideLines(vertLines); // fakeKoCanvasBaseTwo.setGuidesData(&guidesData); // qreal paramSnapDistanceTwo = 8; // KoSnapGuide aKoSnapGuideTwo(&fakeKoCanvasBaseTwo); // KoSnapProxy paramProxyTwo(&aKoSnapGuideTwo); // bool didSnapTwo = toTestTwo.snap(paramMousePosTwo, ¶mProxyTwo, paramSnapDistanceTwo); // QVERIFY(didSnapTwo); } void TestSnapStrategy::testOrhogonalDecoration() { //Making sure the decoration is created but is empty OrthogonalSnapStrategy toTestTwo; const QPointF paramMousePositionTwo(3,3); MockShapeController fakeShapeControllerBaseTwo; MockCanvas fakeKoCanvasBaseTwo(&fakeShapeControllerBaseTwo); KoShapeManager *fakeShapeManager = fakeKoCanvasBaseTwo.shapeManager(); MockShape fakeShapeOne; QList firstSnapPointList; firstSnapPointList.push_back(QPointF(1,2)); firstSnapPointList.push_back(QPointF(2,2)); firstSnapPointList.push_back(QPointF(3,2)); firstSnapPointList.push_back(QPointF(4,2)); fakeShapeOne.snapData().setSnapPoints(firstSnapPointList); fakeShapeOne.isVisible(true); fakeShapeManager->addShape(&fakeShapeOne); KoSnapGuide aKoSnapGuideTwo(&fakeKoCanvasBaseTwo); KoSnapProxy paramProxyTwo(&aKoSnapGuideTwo); //Make sure at least one point in firstSnapPointList is within this distance of //paramMousePoint to trigger the branches if (dx < minHorzDist && dx < maxSnapDistance) //and if (dy < minVertDist && dy < maxSnapDistance) WHICH IS WHERE minVertDist and minHorzDist //ARE CHANGED FROM HUGE_VAL qreal paramSnapDistanceTwo = 4; toTestTwo.snap(paramMousePositionTwo, ¶mProxyTwo, paramSnapDistanceTwo); KoViewConverter irrelevantParameter; QPainterPath resultingDecoration = toTestTwo.decoration(irrelevantParameter); QVERIFY( resultingDecoration.isEmpty() ); // don't forget to remove the shape from the shape manager before exiting! fakeShapeManager->remove(&fakeShapeOne); } void TestSnapStrategy::testNodeDecoration() { //Tests so the decoration returns a rect which is inside the "standard outer rect" NodeSnapStrategy toTest; KoViewConverter irrelevantParameter; QRectF originalRect = QRectF(-5.5, -5.5, 11, 11); QPainterPath resultingDecoration = toTest.decoration(irrelevantParameter); QRectF rectInsidePath = resultingDecoration.boundingRect(); QVERIFY(originalRect==rectInsidePath); } void TestSnapStrategy::testExtensionDecoration() { //Tests the decoration is exercised by providing it with path //fakeShapeOne needs at least one subpath that is open in order to change the values of minDistances //which in turn opens the path where it is possible to get a true bool value back from the snap function // KoPathPointIndex openSubpath(const KoPathPointIndex &pointIndex); in KoPathShape needs to be called ExtensionSnapStrategy toTestTwo; const QPointF paramMousePosTwo; MockShapeController fakeShapeControllerBaseTwo; MockCanvas fakeKoCanvasBaseTwo(&fakeShapeControllerBaseTwo); KoShapeManager *fakeShapeManager = fakeKoCanvasBaseTwo.shapeManager(); KoPathShape fakeShapeOne; QList firstSnapPointList; firstSnapPointList.push_back(QPointF(1,2)); firstSnapPointList.push_back(QPointF(2,2)); firstSnapPointList.push_back(QPointF(3,2)); firstSnapPointList.push_back(QPointF(4,2)); qreal paramSnapDistanceTwo = 4; fakeShapeOne.snapData().setSnapPoints(firstSnapPointList); fakeShapeOne.isVisible(true); QPointF firstPoint(0,2); QPointF secondPoint(1,2); QPointF thirdPoint(2,3); QPointF fourthPoint(3,4); fakeShapeOne.moveTo(firstPoint); fakeShapeOne.lineTo(secondPoint); fakeShapeOne.lineTo(thirdPoint); fakeShapeOne.lineTo(fourthPoint); fakeShapeManager->addShape(&fakeShapeOne); KoSnapGuide aKoSnapGuideTwo(&fakeKoCanvasBaseTwo); KoSnapProxy paramProxyTwo(&aKoSnapGuideTwo); toTestTwo.snap(paramMousePosTwo, ¶mProxyTwo, paramSnapDistanceTwo); const KoViewConverter aConverter; QPainterPath resultingDecoration = toTestTwo.decoration(aConverter); QPointF resultDecorationLastPoint = resultingDecoration.currentPosition(); QVERIFY( resultDecorationLastPoint == QPointF(0,2) ); // don't forget to remove the shape from the shape manager before exiting! fakeShapeManager->remove(&fakeShapeOne); } void TestSnapStrategy::testIntersectionDecoration() { //Tests the decoration by making sure that the returned rect is within the "standard outer rect" IntersectionSnapStrategy toTest; KoViewConverter irrelevantParameter; QRectF originalRect = QRectF(-5.5,-5.5,11,11); //std outer rect QPainterPath resultingDecoration = toTest.decoration(irrelevantParameter); QRectF rectInsidePath = resultingDecoration.boundingRect(); QVERIFY(originalRect==rectInsidePath); } void TestSnapStrategy::testGridDecoration() { //Tests the decoration by making sure the path returned has the calculated endpoint GridSnapStrategy toTest; const QPointF paramMousePosTwo(40,60); MockShapeController fakeShapeControllerBaseTwo; MockCanvas fakeKoCanvasBaseTwo(&fakeShapeControllerBaseTwo); fakeKoCanvasBaseTwo.setHorz(10); fakeKoCanvasBaseTwo.setVert(8); KoSnapGuide aKoSnapGuideTwo(&fakeKoCanvasBaseTwo); KoSnapProxy paramProxyTwo(&aKoSnapGuideTwo); qreal paramSnapDistanceTwo = 8; toTest.snap(paramMousePosTwo, ¶mProxyTwo, paramSnapDistanceTwo); KoViewConverter viewConverter; QSizeF unzoomedSize = viewConverter.viewToDocument(QSizeF(5, 5)); QPointF snappedPos(40, 56); //the snapped position is 40, 56 because horz 10 - so 40 is right on the gridline, and 56 because 7*8 = 56 which is within 8 of 60 QPointF originalEndPoint(snappedPos + QPointF(0, unzoomedSize.height())); QPainterPath resultingDecoration = toTest.decoration(viewConverter); QVERIFY( resultingDecoration.currentPosition() == originalEndPoint ); } void TestSnapStrategy::testBoundingBoxDecoration() { //tests the decoration by making sure the returned path has the pre-calculated end point BoundingBoxSnapStrategy toTest; KoViewConverter viewConverter; QSizeF unzoomedSize = viewConverter.viewToDocument(QSizeF(5, 5)); QPointF snappedPos(0,0); QPointF originalEndPoint(snappedPos + QPointF(unzoomedSize.width(), -unzoomedSize.height())); QPainterPath resultingDecoration = toTest.decoration(viewConverter); QVERIFY( resultingDecoration.currentPosition() == originalEndPoint ); } void TestSnapStrategy::testLineGuideDecoration() { // KoGuides data has been moved into Krita // // //tests the decoration by making sure there are horizontal and vertical lines in the guidesData // LineGuideSnapStrategy toTest; // const QPointF paramMousePosTwo; // MockShapeController fakeShapeControllerBaseTwo; // MockCanvas fakeKoCanvasBaseTwo(&fakeShapeControllerBaseTwo); // KoGuidesData guidesData; // //firstSnapPointList.push_back( // QList horzLines; // horzLines.push_back(2); // horzLines.push_back(3); // horzLines.push_back(4); // horzLines.push_back(5); // QList vertLines; // vertLines.push_back(1); // vertLines.push_back(2); // vertLines.push_back(3); // vertLines.push_back(4); // guidesData.setHorizontalGuideLines(horzLines); // guidesData.setVerticalGuideLines(vertLines); // fakeKoCanvasBaseTwo.setGuidesData(&guidesData); // qreal paramSnapDistanceTwo = 8; // KoSnapGuide aKoSnapGuideTwo(&fakeKoCanvasBaseTwo); // KoSnapProxy paramProxyTwo(&aKoSnapGuideTwo); // toTest.snap(paramMousePosTwo, ¶mProxyTwo, paramSnapDistanceTwo); // KoViewConverter parameterConverter; // QSizeF unzoomedSize = parameterConverter.viewToDocument(QSizeF(5, 5)); // QPointF snappedPos(1,2); // QPointF originalEndPointOne(snappedPos + QPointF(unzoomedSize.width(), 0)); // QPointF originalEndPointTwo(snappedPos + QPointF(0, unzoomedSize.height())); // QPainterPath resultingDecoration = toTest.decoration(parameterConverter); // QVERIFY( (resultingDecoration.currentPosition() == originalEndPointOne) || (resultingDecoration.currentPosition() == originalEndPointTwo ) ); } void TestSnapStrategy::testSquareDistance() { //tests that it does not work without setting the points OrthogonalSnapStrategy toTest; QPointF p1; QPointF p2; qreal resultingRealOne = toTest.squareDistance(p1, p2); QVERIFY(resultingRealOne == 0); //tests that the returned value is as expected for positive values OrthogonalSnapStrategy toTestTwo; QPointF p1_2(2,2); QPointF p2_2(1,1); qreal resultingRealTwo = toTestTwo.squareDistance(p1_2, p2_2); QVERIFY(resultingRealTwo == 2); //tests that the returned value is as expected for positive and negative values OrthogonalSnapStrategy toTestThree; QPointF p1_3(2,2); QPointF p2_3(-2,-2); qreal resultingRealThree = toTestThree.squareDistance(p1_3, p2_3); QVERIFY(resultingRealThree == 32); //tests that the returned value is 0 when the points are the same OrthogonalSnapStrategy toTestFour; QPointF p1_4(2,2); QPointF p2_4(2,2); qreal resultingRealFour = toTestFour.squareDistance(p1_4, p2_4); QVERIFY(resultingRealFour == 0); } void TestSnapStrategy::testScalarProduct() { //Tests so the scalarProduct cannot be calculated unless the points are set OrthogonalSnapStrategy toTest; QPointF p1_5; QPointF p2_5; qreal resultingRealOne = toTest.squareDistance(p1_5, p2_5); QVERIFY(resultingRealOne == 0 ); //tests that the product is correctly calculated for positive point values OrthogonalSnapStrategy toTestTwo; QPointF p1_6(2,2); QPointF p2_6(3,3); qreal resultingRealTwo = toTestTwo.squareDistance(p1_6, p2_6); QVERIFY(resultingRealTwo == 2 ); //tests that the product is correctly calculated for positive and negative point values OrthogonalSnapStrategy toTestThree; QPointF p1_7(2,2); QPointF p2_7(-2,-2); qreal resultingRealThree = toTestThree.squareDistance(p1_7, p2_7); QVERIFY(resultingRealThree == 32); //tests so the product is 0 when the points are the same OrthogonalSnapStrategy toTestFour; QPointF p1_8(1,1); QPointF p2_8(1,1); qreal resultingRealFour = toTestFour.squareDistance(p1_8, p2_8); QVERIFY(resultingRealFour == 0); //tests so there is nothing fishy when using origo OrthogonalSnapStrategy toTestFive; QPointF p1_9(1,1); QPointF p2_9(0,0); qreal resultingRealFive = toTestFive.squareDistance(p1_9, p2_9); QVERIFY(resultingRealFive == 2); } //------------------------------------------------------------------ void TestSnapStrategy::testSnapToExtension() { /* toTest.snapToExtension(paramPosition, ¶mPoint, paramMatrix); qDebug() << direction << " is the returned direction for this point in TestSnapStrategy::testSnapToExtension()"; QCOMPARE(direction, ); */ } void TestSnapStrategy::testProject() { //tests for positive point values but backwards leaning line ExtensionSnapStrategy toTestOne; qreal toCompWithOne = -1; QPointF lineStart(4,4); QPointF lineEnd(2,2); QPointF comparisonPoint(6,6); qreal resultingRealOne = toTestOne.project(lineStart, lineEnd, comparisonPoint); QCOMPARE(resultingRealOne, toCompWithOne); //testing for for negative point values ExtensionSnapStrategy toTestTwo; qreal toCompWithTwo = -4; QPointF lineStart_2(-2,-2); QPointF lineEnd_2(-4,-4); QPointF comparisonPoint_2(6,6); qreal resultingRealTwo = toTestTwo.project(lineStart_2, lineEnd_2, comparisonPoint_2); QCOMPARE(resultingRealTwo, toCompWithTwo); //testing for negative and positive point values ExtensionSnapStrategy toTestThree; qreal toCompWithThree = (10*(6/sqrt(72.0)) + 10*(6/sqrt(72.0))) / sqrt(72.0); //diffLength = sqrt(72), scalar = (10*(6/sqrt(72)) + 10*(6/sqrt(72))) QPointF lineStart_3(-2,-2); QPointF lineEnd_3(4, 4); QPointF comparisonPoint_3(8,8); qreal resultingRealThree = toTestThree.project(lineStart_3, lineEnd_3, comparisonPoint_3); QCOMPARE(resultingRealThree, toCompWithThree); //Below we test the formula itself for the dot-product by using values we know return t=0.5 //Formula for how to use the t value is: //ProjectionPoint = lineStart*(1-resultingReal) + resultingReal*lineEnd; (this is the formula used in BoundingBoxSnapStrategy::squareDistanceToLine()) //Note: The angle of the line from projection point to comparison point is always 90 degrees ExtensionSnapStrategy toTestFour; qreal toCompWithFour = 0.5; QPointF lineStart_4(2,1); QPointF lineEnd_4(6,3); QPointF comparisonPoint_4(3,4); qreal resultingRealFour = toTestFour.project(lineStart_4, lineEnd_4, comparisonPoint_4); QCOMPARE(resultingRealFour, toCompWithFour); } void TestSnapStrategy::testExtensionDirection() { /* TEST CASE 0 Supposed to return null */ ExtensionSnapStrategy toTestOne; KoPathShape uninitiatedPathShape; KoPathPoint::PointProperties normal = KoPathPoint::Normal; const QPointF initiatedPoint0(0,0); KoPathPoint initiatedPoint(&uninitiatedPathShape, initiatedPoint0, normal); QMatrix initiatedMatrixParam(1,1,1,1,1,1); const QTransform initiatedMatrix(initiatedMatrixParam); QPointF direction2 = toTestOne.extensionDirection( &initiatedPoint, initiatedMatrix); QVERIFY(direction2.isNull()); /* TEST CASE 1 tests a point that: - is the first in a subpath, - does not have the firstSubpath property set, - it has no activeControlPoint1, - is has no previous point = expected returning an empty QPointF */ ExtensionSnapStrategy toTestTwo; QPointF expectedPointTwo(0,0); KoPathShape shapeOne; QPointF firstPoint(0,1); QPointF secondPoint(1,2); QPointF thirdPoint(2,3); QPointF fourthPoint(3,4); shapeOne.moveTo(firstPoint); shapeOne.lineTo(secondPoint); shapeOne.lineTo(thirdPoint); shapeOne.lineTo(fourthPoint); QPointF paramPositionTwo(0,1); KoPathPoint paramPointTwo; paramPointTwo.setPoint(paramPositionTwo); paramPointTwo.setParent(&shapeOne); const QTransform paramTransMatrix(1,2,3,4,5,6); QPointF directionTwo = toTestTwo.extensionDirection( ¶mPointTwo, paramTransMatrix); QCOMPARE(directionTwo, expectedPointTwo); /* TEST CASE 2 tests a point that: - is the second in a subpath, - does not have the firstSubpath property set, - it has no activeControlPoint1, - is has a previous point = expected returning an */ ExtensionSnapStrategy toTestThree; QPointF expectedPointThree(0,0); QPointF paramPositionThree(1,1); KoPathPoint paramPointThree; paramPointThree.setPoint(paramPositionThree); paramPointThree.setParent(&shapeOne); QPointF directionThree = toTestThree.extensionDirection( ¶mPointThree, paramTransMatrix); QCOMPARE(directionThree, expectedPointThree); } void TestSnapStrategy::testSquareDistanceToLine() { BoundingBoxSnapStrategy toTestOne; const QPointF lineA(4,1); const QPointF lineB(6,3); const QPointF point(5,8); QPointF pointOnLine(0,0); qreal result = toTestOne.squareDistanceToLine(lineA, lineB, point, pointOnLine); //Should be HUGE_VAL because scalar > diffLength QVERIFY(result == HUGE_VAL); BoundingBoxSnapStrategy toTestTwo; QPointF lineA2(4,4); QPointF lineB2(4,4); QPointF point2(5,8); QPointF pointOnLine2(0,0); qreal result2 = toTestTwo.squareDistanceToLine(lineA2, lineB2, point2, pointOnLine2); //Should be HUGE_VAL because lineA2 == lineB2 QVERIFY(result2 == HUGE_VAL); BoundingBoxSnapStrategy toTestThree; QPointF lineA3(6,4); QPointF lineB3(8,6); QPointF point3(2,2); QPointF pointOnLine3(0,0); qreal result3 = toTestThree.squareDistanceToLine(lineA3, lineB3, point3, pointOnLine3); //Should be HUGE_VAL because scalar < 0.0 QVERIFY(result3 == HUGE_VAL); BoundingBoxSnapStrategy toTestFour; QPointF lineA4(2,2); QPointF lineB4(8,6); QPointF point4(3,4); QPointF pointOnLine4(0,0); QPointF diff(6,4); //diff = lineB3 - point3 = 6,4 //diffLength = sqrt(52) //scalar = (1*(6/sqrt(52)) + 2*(4/sqrt(52))); //pointOnLine = lineA + scalar / diffLength * diff; lineA + ((1*(6/sqrt(52)) + 2*(4/sqrt(52))) / sqrt(52)) * 6,4; QPointF distToPointOnLine = (lineA4 + ((1*(6/sqrt(52.0)) + 2*(4/sqrt(52.0))) / sqrt(52.0)) * diff)-point4; qreal toCompWithFour = distToPointOnLine.x()*distToPointOnLine.x()+distToPointOnLine.y()*distToPointOnLine.y(); qreal result4 = toTestFour.squareDistanceToLine(lineA4, lineB4, point4, pointOnLine4); //Normal case with example data QVERIFY(qFuzzyCompare(result4, toCompWithFour)); } KISTEST_MAIN(TestSnapStrategy) diff --git a/libs/flake/tests/TestSvgParser.cpp b/libs/flake/tests/TestSvgParser.cpp index b03ac84435..f8e748b6b2 100644 --- a/libs/flake/tests/TestSvgParser.cpp +++ b/libs/flake/tests/TestSvgParser.cpp @@ -1,3226 +1,3227 @@ /* * 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. */ #include "TestSvgParser.h" +#include #include #include #include #include "SvgParserTestingUtils.h" #include "../../sdk/tests/qimage_test_util.h" #ifdef USE_ROUND_TRIP #include "SvgWriter.h" #include #include #endif void TestSvgParser::testUnitPx() { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 72 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->boundingRect(), kisGrowRect(QRectF(0,0,10,20), 0.5)); QCOMPARE(shape->absoluteTransformation(0), QTransform()); QCOMPARE(shape->outlineRect(), QRectF(0,0,10,20)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(0,0)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(10,20)); } void TestSvgParser::testUnitPxResolution() { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 144 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->boundingRect(), kisGrowRect(QRectF(0,0,5,10), 0.25)); QCOMPARE(shape->absoluteTransformation(0), QTransform::fromScale(0.5, 0.5)); QCOMPARE(shape->outlineRect(), QRectF(0,0,10,20)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(0,0)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(5,10)); } void TestSvgParser::testUnitPt() { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 666 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->boundingRect(), kisGrowRect(QRectF(0,0,10,20), 0.5)); QCOMPARE(shape->absoluteTransformation(0), QTransform()); QCOMPARE(shape->outlineRect(), QRectF(0,0,10,20)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(0,0)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(10,20)); } void TestSvgParser::testUnitIn() { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 666 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->boundingRect(), kisGrowRect(QRectF(0,0,720,1440), 36)); QCOMPARE(shape->absoluteTransformation(0), QTransform::fromScale(72, 72)); QCOMPARE(shape->outlineRect(), QRectF(0,0,10,20)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(0,0)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(720,1440)); } void TestSvgParser::testUnitPercentInitial() { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 80, 80) /* px */, 144 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->boundingRect(), kisGrowRect(QRectF(0,0,5,10), 0.25)); QCOMPARE(shape->absoluteTransformation(0), QTransform::fromScale(0.5, 0.5)); QCOMPARE(shape->outlineRect(), QRectF(0,0,10,20)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(0,0)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(5,10)); } void TestSvgParser::testScalingViewport() { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 72 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->absoluteTransformation(0), QTransform::fromTranslate(4, 4) * QTransform::fromScale(0.5, 0.5)); QCOMPARE(shape->outlineRect(), QRectF(0,0,12,32)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(2,2)); QCOMPARE(shape->absolutePosition(KoFlake::TopRight), QPointF(8,2)); QCOMPARE(shape->absolutePosition(KoFlake::BottomLeft), QPointF(2,18)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(8,18)); } void TestSvgParser::testScalingViewportKeepMeet1() { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 72 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->absoluteTransformation(0), QTransform::fromTranslate(4, 4) * QTransform::fromScale(0.5, 0.5)); QCOMPARE(shape->outlineRect(), QRectF(0,0,12,32)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(2,2)); QCOMPARE(shape->absolutePosition(KoFlake::TopRight), QPointF(8,2)); QCOMPARE(shape->absolutePosition(KoFlake::BottomLeft), QPointF(2,18)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(8,18)); } void TestSvgParser::testScalingViewportKeepMeet2() { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 72 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->absoluteTransformation(0), QTransform::fromTranslate(4, 4) * QTransform::fromScale(0.5, 0.5)); QCOMPARE(shape->outlineRect(), QRectF(0,0,12,32)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(2,2)); QCOMPARE(shape->absolutePosition(KoFlake::TopRight), QPointF(8,2)); QCOMPARE(shape->absolutePosition(KoFlake::BottomLeft), QPointF(2,18)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(8,18)); } void TestSvgParser::testScalingViewportKeepMeetAlign() { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 72 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->absoluteTransformation(0), QTransform::fromTranslate(4, 24) * QTransform::fromScale(0.5, 0.5)); QCOMPARE(shape->outlineRect(), QRectF(0,0,12,32)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(2,12)); QCOMPARE(shape->absolutePosition(KoFlake::TopRight), QPointF(8,12)); QCOMPARE(shape->absolutePosition(KoFlake::BottomLeft), QPointF(2,28)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(8,28)); } void TestSvgParser::testScalingViewportKeepSlice1() { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 72 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->absoluteTransformation(0), QTransform::fromTranslate(4, 4) * QTransform::fromScale(0.5, 0.5)); QCOMPARE(shape->outlineRect(), QRectF(0,0,12,32)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(2,2)); QCOMPARE(shape->absolutePosition(KoFlake::TopRight), QPointF(8,2)); QCOMPARE(shape->absolutePosition(KoFlake::BottomLeft), QPointF(2,18)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(8,18)); } void TestSvgParser::testScalingViewportKeepSlice2() { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 72 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->absoluteTransformation(0), QTransform::fromTranslate(4, 4) * QTransform::fromScale(0.5, 0.5)); QCOMPARE(shape->outlineRect(), QRectF(0,0,12,32)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(2,2)); QCOMPARE(shape->absolutePosition(KoFlake::TopRight), QPointF(8,2)); QCOMPARE(shape->absolutePosition(KoFlake::BottomLeft), QPointF(2,18)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(8,18)); } void TestSvgParser::testScalingViewportResolution() { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 144 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->absoluteTransformation(0), QTransform::fromTranslate(4, 4) * QTransform::fromScale(0.25, 0.25)); QCOMPARE(shape->outlineRect(), QRectF(0,0,12,32)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(1,1)); QCOMPARE(shape->absolutePosition(KoFlake::TopRight), QPointF(4,1)); QCOMPARE(shape->absolutePosition(KoFlake::BottomLeft), QPointF(1,9)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(4,9)); } void TestSvgParser::testScalingViewportPercentInternal() { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 72 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->absoluteTransformation(0), QTransform::fromTranslate(4, 4) * QTransform::fromScale(0.5, 0.5)); QCOMPARE(shape->outlineRect(), QRectF(0,0,12,32)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(2,2)); QCOMPARE(shape->absolutePosition(KoFlake::TopRight), QPointF(8,2)); QCOMPARE(shape->absolutePosition(KoFlake::BottomLeft), QPointF(2,18)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(8,18)); } void TestSvgParser::testParsePreserveAspectRatio() { { SvgUtil::PreserveAspectRatioParser p(" defer xMinYMax meet"); QCOMPARE(p.defer, true); QCOMPARE(p.mode, Qt::KeepAspectRatio); QCOMPARE(p.xAlignment, SvgUtil::PreserveAspectRatioParser::Min); QCOMPARE(p.yAlignment, SvgUtil::PreserveAspectRatioParser::Max); } { SvgUtil::PreserveAspectRatioParser p(" xMinYMid slice"); QCOMPARE(p.defer, false); QCOMPARE(p.mode, Qt::KeepAspectRatioByExpanding); QCOMPARE(p.xAlignment, SvgUtil::PreserveAspectRatioParser::Min); QCOMPARE(p.yAlignment, SvgUtil::PreserveAspectRatioParser::Middle); } { SvgUtil::PreserveAspectRatioParser p(" xmidYMid "); QCOMPARE(p.defer, false); QCOMPARE(p.mode, Qt::KeepAspectRatio); QCOMPARE(p.xAlignment, SvgUtil::PreserveAspectRatioParser::Middle); QCOMPARE(p.yAlignment, SvgUtil::PreserveAspectRatioParser::Middle); } { SvgUtil::PreserveAspectRatioParser p(" NoNe "); QCOMPARE(p.defer, false); QCOMPARE(p.mode, Qt::IgnoreAspectRatio); QCOMPARE(p.xAlignment, SvgUtil::PreserveAspectRatioParser::Min); QCOMPARE(p.yAlignment, SvgUtil::PreserveAspectRatioParser::Min); } { SvgUtil::PreserveAspectRatioParser p("defer NoNe "); QCOMPARE(p.defer, true); QCOMPARE(p.mode, Qt::IgnoreAspectRatio); QCOMPARE(p.xAlignment, SvgUtil::PreserveAspectRatioParser::Min); QCOMPARE(p.yAlignment, SvgUtil::PreserveAspectRatioParser::Min); } { SvgUtil::PreserveAspectRatioParser p("sweet brown fox jumps over a nice svg file"); QCOMPARE(p.defer, false); QCOMPARE(p.mode, Qt::IgnoreAspectRatio); QCOMPARE(p.xAlignment, SvgUtil::PreserveAspectRatioParser::Min); QCOMPARE(p.yAlignment, SvgUtil::PreserveAspectRatioParser::Min); } } #include "parsers/SvgTransformParser.h" void TestSvgParser::testParseTransform() { { QString str("translate(-111.0, 33) translate(-111.0, 33) matrix (1 1 0 0 1, 3), translate(1)" "scale(0.5) rotate(10) rotate(10, 3 3) skewX(1) skewY(2)"); SvgTransformParser p(str); QCOMPARE(p.isValid(), true); } { // forget about one brace QString str("translate(-111.0, 33) translate(-111.0, 33 matrix (1 1 0 0 1, 3), translate(1)" "scale(0.5) rotate(10) rotate(10, 3 3) skewX(1) skewY(2)"); SvgTransformParser p(str); QCOMPARE(p.isValid(), false); } { SvgTransformParser p("translate(100, 50)"); QCOMPARE(p.isValid(), true); QCOMPARE(p.transform(), QTransform::fromTranslate(100, 50)); } { SvgTransformParser p("translate(100 50)"); QCOMPARE(p.isValid(), true); QCOMPARE(p.transform(), QTransform::fromTranslate(100, 50)); } { SvgTransformParser p("translate(100)"); QCOMPARE(p.isValid(), true); QCOMPARE(p.transform(), QTransform::fromTranslate(100, 0)); } { SvgTransformParser p("scale(100, 50)"); QCOMPARE(p.isValid(), true); QCOMPARE(p.transform(), QTransform::fromScale(100, 50)); } { SvgTransformParser p("scale(100)"); QCOMPARE(p.isValid(), true); QCOMPARE(p.transform(), QTransform::fromScale(100, 100)); } { SvgTransformParser p("rotate(90 70 74.0)"); QCOMPARE(p.isValid(), true); QTransform t; t.rotate(90); t = QTransform::fromTranslate(-70, -74) * t * QTransform::fromTranslate(70, 74); qDebug() << ppVar(p.transform()); QCOMPARE(p.transform(), t); } } void TestSvgParser::testScalingViewportTransform() { /** * Note: 'transform' affects all the attributes of the *current* * element, while 'viewBox' affects only the descendants! */ const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 72 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->absoluteTransformation(0), QTransform::fromTranslate(10, 4) * QTransform::fromScale(0.5, 0.5)); QCOMPARE(shape->outlineRect(), QRectF(0,0,12,32)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(5,2)); QCOMPARE(shape->absolutePosition(KoFlake::TopRight), QPointF(11,2)); QCOMPARE(shape->absolutePosition(KoFlake::BottomLeft), QPointF(5,18)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(11,18)); } void TestSvgParser::testTransformNesting() { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 72 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->boundingRect(), QRectF(10 - 1,10 - 0.5, 20 + 2, 20 + 1)); QCOMPARE(shape->outlineRect(), QRectF(0,0,10,20)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(10,10)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(30,30)); } void TestSvgParser::testTransformNestingGroups() { const QString data = "" "" " " "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 72 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->boundingRect(), QRectF(10 - 1,10 - 0.5, 20 + 2, 20 + 1)); QCOMPARE(shape->outlineRect(), QRectF(0,0,10,20)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(10,10)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(30,30)); } void TestSvgParser::testTransformRotation1() { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 72 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->boundingRect(), kisGrowRect(QRectF(-20,0,20,10), 0.5)); QCOMPARE(shape->outlineRect(), QRectF(0,0,10,20)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(0,0)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(-20,10)); } void TestSvgParser::testTransformRotation2() { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 72 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->boundingRect(), kisGrowRect(QRectF(5,5,20,10), 0.5)); QCOMPARE(shape->outlineRect(), QRectF(0,0,10,20)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(5,15)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(25,5)); } void TestSvgParser::testRenderStrokeNone() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_none"); } void TestSvgParser::testRenderStrokeColorName() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_blue"); } void TestSvgParser::testRenderStrokeColorHex3() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_blue"); } void TestSvgParser::testRenderStrokeColorHex6() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_blue"); } void TestSvgParser::testRenderStrokeColorRgbValues() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_blue"); } void TestSvgParser::testRenderStrokeColorRgbPercent() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_blue"); } void TestSvgParser::testRenderStrokeColorCurrent() { const QString data = "" "" " " "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_blue"); } void TestSvgParser::testRenderStrokeColorNonexistentIri() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_blue"); } void TestSvgParser::testRenderStrokeWidth() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_blue_width_2"); } void TestSvgParser::testRenderStrokeZeroWidth() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_none"); } void TestSvgParser::testRenderStrokeOpacity() { const QString data = "" "" ""; SvgRenderTester t (data); t.setFuzzyThreshold(1); t.test_standard_30px_72ppi("stroke_blue_0_3_opacity"); } void TestSvgParser::testRenderStrokeJointRound() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_blue_join_round"); } void TestSvgParser::testRenderStrokeLinecap() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_blue_linecap_round"); } void TestSvgParser::testRenderStrokeMiterLimit() { // TODO:seems like doesn't work!! qWarning() << "WARNING: Miter limit test is skipped!!!"; return; const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_blue_miter_limit"); } void TestSvgParser::testRenderStrokeDashArrayEven() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_blue_dasharray_even"); } void TestSvgParser::testRenderStrokeDashArrayEvenOffset() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_blue_dasharray_even_offset"); } void TestSvgParser::testRenderStrokeDashArrayOdd() { // SVG 1.1: if the dasharray is odd, repeat it const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_blue_dasharray_odd"); } void TestSvgParser::testRenderStrokeDashArrayRelative() { // SVG 1.1: relative to view box // (40 x 50) * sqrt(2) => dash length = 5 px const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_blue_dasharray_relative"); } void TestSvgParser::testRenderFillDefault() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_black"); } void TestSvgParser::testRenderFillRuleNonZero() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_non_zero"); } void TestSvgParser::testRenderFillRuleEvenOdd() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_even_odd"); } void TestSvgParser::testRenderFillOpacity() { const QString data = "" "" ""; SvgRenderTester t (data); t.setFuzzyThreshold(1); t.test_standard_30px_72ppi("fill_opacity_0_3"); } void TestSvgParser::testRenderDisplayAttribute() { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 144 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->isVisible(), false); } void TestSvgParser::testRenderVisibilityAttribute() { { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 144 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->isVisible(), true); } { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 144 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->isVisible(), false); } { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 144 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->isVisible(), false); } } void TestSvgParser::testRenderVisibilityInheritance() { const QString data = "" "" " " "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 144 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->isVisible(false), true); QCOMPARE(shape->isVisible(true), false); } void TestSvgParser::testRenderDisplayInheritance() { const QString data = "" "" " " "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 144 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->isVisible(false), true); QEXPECT_FAIL("", "TODO: Fix 'display' attribute not to be inherited in shapes hierarchy!", Continue); QCOMPARE(shape->isVisible(true), true); } void TestSvgParser::testRenderStrokeWithInlineStyle() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_blue_width_2"); } void TestSvgParser::testIccColor() { const QString data = "" "" " " " " " " "" ""; SvgRenderTester t (data); int numFetches = 0; t.parser.setFileFetcher( [&numFetches](const QString &name) { numFetches++; const QString fileName = TestUtil::fetchDataFileLazy(name); QFile file(fileName); KIS_ASSERT(file.exists()); file.open(QIODevice::ReadOnly); return file.readAll(); }); t.test_standard_30px_72ppi("stroke_blue_width_2"); QCOMPARE(numFetches, 1); } void TestSvgParser::testRenderFillLinearGradientRelativePercent() { const QString data = "" "" " " " " " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_gradient"); } void TestSvgParser::testRenderFillLinearGradientRelativePortion() { const QString data = "" "" " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_gradient"); } void TestSvgParser::testRenderFillLinearGradientUserCoord() { const QString data = "" "" " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_gradient"); } void TestSvgParser::testRenderFillLinearGradientStopPortion() { const QString data = "" "" " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_gradient"); } void TestSvgParser::testRenderFillLinearGradientTransform() { const QString data = "" "" " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_gradient_vertical"); } void TestSvgParser::testRenderFillLinearGradientTransformUserCoord() { const QString data = "" "" " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_gradient_vertical_in_user"); } void TestSvgParser::testRenderFillLinearGradientRotatedShape() { // DK: I'm not sure I fully understand if it is a correct transformation, // but inkscape opens the file in the same way... const QString data = "" "" " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_gradient_shape_rotated", false); } void TestSvgParser::testRenderFillLinearGradientRotatedShapeUserCoord() { // DK: I'm not sure I fully understand if it is a correct transformation, // but inkscape opens the file in the same way... const QString data = "" "" " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_gradient_shape_rotated_in_user", false); } void TestSvgParser::testRenderFillRadialGradient() { const QString data = "" "" " " " " "" "" ""; SvgRenderTester t (data); t.setFuzzyThreshold(1); t.test_standard_30px_72ppi("fill_gradient_radial"); } void TestSvgParser::testRenderFillRadialGradientUserCoord() { const QString data = "" "" " " " " "" "" ""; SvgRenderTester t (data); t.setFuzzyThreshold(1); t.test_standard_30px_72ppi("fill_gradient_radial_in_user"); } void TestSvgParser::testRenderFillLinearGradientUserCoordPercent() { const QString data = "" "" " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_gradient"); } void TestSvgParser::testRenderStrokeLinearGradient() { const QString data = "" "" " " " " " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_gradient_dashed"); } QTransform rotateTransform(qreal degree, const QPointF ¢er) { QTransform rotate; rotate.rotate(degree); return QTransform::fromTranslate(-center.x(), -center.y()) * rotate * QTransform::fromTranslate(center.x(), center.y()); } QTransform viewTransform(const QRectF &src, const QRectF &dst) { return QTransform::fromTranslate(-src.x(), -src.y()) * QTransform::fromScale(dst.width() / src.width(), dst.height() / src.height()) * QTransform::fromTranslate(dst.x(), dst.y()); } QPainterPath bakeShape(const QPainterPath &path, const QTransform &bakeTransform, bool contentIsObb = false, const QRectF &shapeBoundingRect = QRectF(), bool contentIsViewBox = false, const QRectF &viewBoxRect= QRectF(), const QRectF &refRect = QRectF()) { const QTransform relativeToShape(shapeBoundingRect.width(), 0, 0, shapeBoundingRect.height(), shapeBoundingRect.x(), shapeBoundingRect.y()); QTransform newTransform = bakeTransform; if (contentIsObb) { newTransform = relativeToShape * newTransform; } if (contentIsViewBox) { newTransform = viewTransform(viewBoxRect, refRect) * newTransform; } return newTransform.map(path); } #include void renderBakedPath(QPainter &painter, const QPainterPath &bakedFillPath, const QTransform &bakedTransform, const QRect &shapeOutline, const QTransform &shapeTransform, const QRectF &referenceRect, bool contentIsObb, const QRectF &bakedShapeBoundingRect, bool referenceIsObb, const QTransform &patternTransform, QImage *stampResult) { painter.setTransform(QTransform()); painter.setPen(Qt::NoPen); QPainterPath shapeOutlinePath; shapeOutlinePath.addRect(shapeOutline); KoBakedShapeRenderer renderer( shapeOutlinePath, shapeTransform, bakedTransform, referenceRect, contentIsObb, bakedShapeBoundingRect, referenceIsObb, patternTransform); QPainter *patchPainter = renderer.bakeShapePainter(); patchPainter->fillPath(bakedFillPath, Qt::blue); patchPainter->end(); renderer.renderShape(painter); if (stampResult) { *stampResult = renderer.patchImage(); } } void TestSvgParser::testManualRenderPattern_ContentUser_RefObb() { const QRectF referenceRect(0, 0, 1.0, 0.5); QPainterPath fillPath; fillPath.addRect(QRect(2, 2, 6, 6)); fillPath.addRect(QRect(8, 4, 3, 2)); QTransform bakedTransform = QTransform::fromTranslate(10, 10) * QTransform::fromScale(2, 2); QPainterPath bakedFillPath = bakeShape(fillPath, bakedTransform); QRect shape1OutlineRect(0,0,10,20); QImage stampResult; QImage fillResult(QSize(60,60), QImage::Format_ARGB32); QPainter gc(&fillResult); fillResult.fill(0); renderBakedPath(gc, bakedFillPath, bakedTransform, shape1OutlineRect, bakedTransform, referenceRect, false, QRectF(), true, QTransform(), &stampResult); QVERIFY(TestUtil::checkQImage(stampResult, "svg_render", "render", "pattern_c_user_r_obb_patch1")); QVERIFY(TestUtil::checkQImage(fillResult, "svg_render", "render", "pattern_c_user_r_obb_fill1")); QRect shape2OutlineRect(5,5,20,10); QTransform shape2Transform = QTransform::fromScale(2, 2) * QTransform::fromTranslate(5, 5); fillResult.fill(0); renderBakedPath(gc, bakedFillPath, bakedTransform, shape2OutlineRect, shape2Transform, referenceRect, false, QRectF(), true, QTransform(), &stampResult); QVERIFY(TestUtil::checkQImage(stampResult, "svg_render", "render", "pattern_c_user_r_obb_patch2")); QVERIFY(TestUtil::checkQImage(fillResult, "svg_render", "render", "pattern_c_user_r_obb_fill2")); } void TestSvgParser::testManualRenderPattern_ContentObb_RefObb() { const QRectF referenceRect(0.3, 0.3, 0.4, 0.4); QPainterPath fillPath; fillPath.addRect(QRectF(0.4, 0.4, 0.2, 0.2)); fillPath.addRect(QRectF(0.6, 0.5, 0.1, 0.1)); fillPath.addRect(QRectF(0.3, 0.4, 0.1, 0.1)); const QRect bakedShapeRect(2,2,10,10); QTransform bakedTransform = QTransform::fromTranslate(10, 10) * QTransform::fromScale(2, 2); QPainterPath bakedFillPath = bakeShape(fillPath, bakedTransform, true, bakedShapeRect); QImage stampResult; QImage fillResult(QSize(60,60), QImage::Format_ARGB32); QPainter gc(&fillResult); // Round trip to the same shape fillResult.fill(0); renderBakedPath(gc, bakedFillPath, bakedTransform, bakedShapeRect, bakedTransform, referenceRect, true, bakedShapeRect, true, QTransform(), &stampResult); QVERIFY(TestUtil::checkQImage(stampResult, "svg_render", "render", "pattern_c_obb_r_obb_patch1")); QVERIFY(TestUtil::checkQImage(fillResult, "svg_render", "render", "pattern_c_obb_r_obb_fill1")); // Move to a different shape QRect shape2OutlineRect(5,5,20,10); QTransform shape2Transform = QTransform::fromScale(2, 2) * QTransform::fromTranslate(5, 5); fillResult.fill(0); renderBakedPath(gc, bakedFillPath, bakedTransform, shape2OutlineRect, shape2Transform, referenceRect, true, bakedShapeRect, true, QTransform(), &stampResult); QVERIFY(TestUtil::checkQImage(stampResult, "svg_render", "render", "pattern_c_obb_r_obb_patch2")); QVERIFY(TestUtil::checkQImage(fillResult, "svg_render", "render", "pattern_c_obb_r_obb_fill2")); } void TestSvgParser::testManualRenderPattern_ContentUser_RefUser() { const QRectF referenceRect(5, 2, 8, 8); QPainterPath fillPath; fillPath.addRect(QRect(2, 2, 6, 6)); fillPath.addRect(QRect(8, 4, 3, 2)); QTransform bakedTransform = QTransform::fromTranslate(10, 10) * QTransform::fromScale(2, 2); QPainterPath bakedFillPath = bakeShape(fillPath, bakedTransform); QRect shape1OutlineRect(0,0,10,20); QImage stampResult; QImage fillResult(QSize(60,60), QImage::Format_ARGB32); QPainter gc(&fillResult); fillResult.fill(0); renderBakedPath(gc, bakedFillPath, bakedTransform, shape1OutlineRect, bakedTransform, referenceRect, false, QRectF(), false, QTransform(), &stampResult); QVERIFY(TestUtil::checkQImage(stampResult, "svg_render", "render", "pattern_c_user_r_user_patch1")); QVERIFY(TestUtil::checkQImage(fillResult, "svg_render", "render", "pattern_c_user_r_user_fill1")); QRect shape2OutlineRect(5,5,20,10); QTransform shape2Transform = QTransform::fromScale(2, 2) * QTransform::fromTranslate(5, 5); fillResult.fill(0); renderBakedPath(gc, bakedFillPath, bakedTransform, shape2OutlineRect, shape2Transform, referenceRect, false, QRectF(), false, QTransform(), &stampResult); QVERIFY(TestUtil::checkQImage(stampResult, "svg_render", "render", "pattern_c_user_r_user_patch2")); QVERIFY(TestUtil::checkQImage(fillResult, "svg_render", "render", "pattern_c_user_r_user_fill2")); } void TestSvgParser::testManualRenderPattern_ContentObb_RefObb_Transform_Rotate() { const QRectF referenceRect(0.0, 0.0, 0.4, 0.2); QPainterPath fillPath; fillPath.addRect(QRectF(0.0, 0.0, 0.5, 0.1)); fillPath.addRect(QRectF(0.0, 0.1, 0.1, 0.1)); const QRect bakedShapeRect(2,1,10,10); QTransform bakedTransform = QTransform::fromScale(2, 2) * QTransform::fromTranslate(10,10); QPainterPath bakedFillPath = bakeShape(fillPath, bakedTransform, true, bakedShapeRect); QImage stampResult; QImage fillResult(QSize(60,60), QImage::Format_ARGB32); QPainter gc(&fillResult); QTransform patternTransform; patternTransform.rotate(90); patternTransform = patternTransform * QTransform::fromTranslate(0.5, 0.0); // Round trip to the same shape fillResult.fill(0); renderBakedPath(gc, bakedFillPath, bakedTransform, bakedShapeRect, bakedTransform, referenceRect, true, bakedShapeRect, true, patternTransform, &stampResult); QVERIFY(TestUtil::checkQImage(stampResult, "svg_render", "render", "pattern_c_obb_r_obb_rotate_patch1")); QVERIFY(TestUtil::checkQImage(fillResult, "svg_render", "render", "pattern_c_obb_r_obb_rotate_fill1")); QRect shape2OutlineRect(5,5,20,10); QTransform shape2Transform = QTransform::fromScale(2, 2) * QTransform::fromTranslate(5, 5); fillResult.fill(0); renderBakedPath(gc, bakedFillPath, bakedTransform, shape2OutlineRect, shape2Transform, referenceRect, true, bakedShapeRect, true, patternTransform, &stampResult); QVERIFY(TestUtil::checkQImage(stampResult, "svg_render", "render", "pattern_c_obb_r_obb_rotate_patch2")); QVERIFY(TestUtil::checkQImage(fillResult, "svg_render", "render", "pattern_c_obb_r_obb_rotate_fill2")); } void TestSvgParser::testManualRenderPattern_ContentView_RefObb() { const QRectF referenceRect(0, 0, 0.5, 1.0/3.0); const QRectF viewRect(10,10,60,90); QPainterPath fillPath; fillPath.addRect(QRect(30, 10, 20, 60)); fillPath.addRect(QRect(50, 40, 20, 30)); QRect shape1OutlineRect(10,20,40,120); QTransform bakedTransform = QTransform::fromScale(2, 0.5) * QTransform::fromTranslate(40,30); QPainterPath bakedFillPath = bakeShape(fillPath, bakedTransform, true, shape1OutlineRect, true, viewRect, referenceRect); QImage stampResult; QImage fillResult(QSize(220,160), QImage::Format_ARGB32); QPainter gc(&fillResult); fillResult.fill(0); renderBakedPath(gc, bakedFillPath, bakedTransform, shape1OutlineRect, bakedTransform, referenceRect, true, shape1OutlineRect, true, QTransform(), &stampResult); QVERIFY(TestUtil::checkQImage(stampResult, "svg_render", "render", "pattern_c_view_r_obb_patch1")); QVERIFY(TestUtil::checkQImage(fillResult, "svg_render", "render", "pattern_c_view_r_obb_fill1")); QRect shape2OutlineRect(20,10,60,90); QTransform shape2Transform = QTransform::fromScale(2, 1) * QTransform::fromTranslate(50, 50); fillResult.fill(0); renderBakedPath(gc, bakedFillPath, bakedTransform, shape2OutlineRect, shape2Transform, referenceRect, true, shape1OutlineRect, true, rotateTransform(90, QPointF(0, 1.0 / 3.0)), &stampResult); QVERIFY(TestUtil::checkQImage(stampResult, "svg_render", "render", "pattern_c_view_r_obb_patch2")); QVERIFY(TestUtil::checkQImage(fillResult, "svg_render", "render", "pattern_c_view_r_obb_fill2")); } void TestSvgParser::testManualRenderPattern_ContentView_RefUser() { const QRectF referenceRect(60, 0, 30, 20); const QRectF viewRect(10,10,60,90); QPainterPath fillPath; fillPath.addRect(QRect(30, 10, 20, 60)); fillPath.addRect(QRect(50, 40, 20, 30)); QRect shape1OutlineRect(10,20,40,120); QTransform bakedTransform = QTransform::fromScale(2, 0.5) * QTransform::fromTranslate(40,30); QPainterPath bakedFillPath = bakeShape(fillPath, bakedTransform, false, shape1OutlineRect, true, viewRect, referenceRect); QImage stampResult; QImage fillResult(QSize(220,160), QImage::Format_ARGB32); QPainter gc(&fillResult); fillResult.fill(0); renderBakedPath(gc, bakedFillPath, bakedTransform, shape1OutlineRect, bakedTransform, referenceRect, false, shape1OutlineRect, false, QTransform(), &stampResult); QVERIFY(TestUtil::checkQImage(stampResult, "svg_render", "render", "pattern_c_view_r_user_patch1")); QVERIFY(TestUtil::checkQImage(fillResult, "svg_render", "render", "pattern_c_view_r_user_fill1")); QRect shape2OutlineRect(20,10,60,90); QTransform shape2Transform = QTransform::fromScale(2, 1) * QTransform::fromTranslate(50, 50); QTransform patternTransform2 = rotateTransform(90, QPointF()) * QTransform::fromTranslate(40, 10); fillResult.fill(0); renderBakedPath(gc, bakedFillPath, bakedTransform, shape2OutlineRect, shape2Transform, referenceRect, false, shape1OutlineRect, false, patternTransform2, &stampResult); QVERIFY(TestUtil::checkQImage(stampResult, "svg_render", "render", "pattern_c_view_r_user_patch2")); QVERIFY(TestUtil::checkQImage(fillResult, "svg_render", "render", "pattern_c_view_r_user_fill2")); } void TestSvgParser::testRenderPattern_r_User_c_User() { const QString data = "" "" " " " " " " " " " " " " "" "" " " "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_pattern_base", false, QSize(160, 160)); } void TestSvgParser::testRenderPattern_InfiniteRecursionWhenInherited() { const QString data = "" "" " " " " " " " " " " " " "" "" " " "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_pattern_base_black", false, QSize(160, 160)); } void TestSvgParser::testRenderPattern_r_User_c_View() { const QString data = "" "" " " " " " " // y is changed to 39 from 40 to fix a rounding issue! " " " " " " "" "" " " "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_pattern_base", false, QSize(160, 160)); } void TestSvgParser::testRenderPattern_r_User_c_Obb() { const QString data = "" "" " " " " " " " " " " " " "" "" " " "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_pattern_base", false, QSize(160, 160)); } void TestSvgParser::testRenderPattern_r_User_c_View_Rotated() { const QString data = "" "" " " " " " " " " " " " " "" "" " " "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_pattern_rotated", false, QSize(220, 160)); } void TestSvgParser::testRenderPattern_r_Obb_c_View_Rotated() { /** * This test case differs from any application existent in the world :( * * Chrome and Firefox premultiply the patternTransform instead of doing post- * multiplication. Photoshop forgets to multiply the reference rect on it. * * So... */ const QString data = "" "" " " " " " " " " " " " " "" "" " " "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_pattern_rotated_odd", false, QSize(220, 160)); } #include #include #include #include void TestSvgParser::testKoClipPathRendering() { QPainterPath path1; path1.addRect(QRect(5,5,15,15)); QPainterPath path2; path2.addRect(QRect(10,10,15,15)); QPainterPath clipPath1; clipPath1.addRect(QRect(10, 0, 10, 30)); QPainterPath clipPath2; clipPath2.moveTo(0,7); clipPath2.lineTo(30,7); clipPath2.lineTo(15,30); clipPath2.lineTo(0,7); QScopedPointer shape1(KoPathShape::createShapeFromPainterPath(path1)); shape1->setBackground(QSharedPointer(new KoColorBackground(Qt::blue))); QScopedPointer shape2(KoPathShape::createShapeFromPainterPath(path2)); shape2->setBackground(QSharedPointer(new KoColorBackground(Qt::red))); QScopedPointer clipShape1(KoPathShape::createShapeFromPainterPath(clipPath1)); KoClipPath *koClipPath1 = new KoClipPath({clipShape1.take()}, KoFlake::UserSpaceOnUse); koClipPath1->setClipRule(Qt::WindingFill); shape1->setClipPath(koClipPath1); QScopedPointer group(new KoShapeGroup()); { QList shapes({shape1.take(), shape2.take()}); KoShapeGroupCommand cmd(group.data(), shapes, false); cmd.redo(); } QScopedPointer clipShape2(KoPathShape::createShapeFromPainterPath(clipPath2)); KoClipPath *koClipPath2 = new KoClipPath({clipShape2.take()}, KoFlake::UserSpaceOnUse); koClipPath2->setClipRule(Qt::WindingFill); group->setClipPath(koClipPath2); SvgRenderTester::testRender(group.take(), "load", "clip_render_test", QSize(30,30)); } void TestSvgParser::testKoClipPathRelativeRendering() { QPainterPath path1; path1.addRect(QRect(5,5,15,15)); QPainterPath path2; path2.addRect(QRect(10,10,15,15)); QPainterPath clipPath1; clipPath1.addRect(QRect(10, 0, 10, 30)); QPainterPath clipPath2; clipPath2.moveTo(0,0); clipPath2.lineTo(1,0); clipPath2.lineTo(0.5,1); clipPath2.lineTo(0,0); QScopedPointer shape1(KoPathShape::createShapeFromPainterPath(path1)); shape1->setBackground(QSharedPointer(new KoColorBackground(Qt::blue))); QScopedPointer shape2(KoPathShape::createShapeFromPainterPath(path2)); shape2->setBackground(QSharedPointer(new KoColorBackground(Qt::red))); QScopedPointer clipShape1(KoPathShape::createShapeFromPainterPath(clipPath1)); KoClipPath *koClipPath1 = new KoClipPath({clipShape1.take()}, KoFlake::UserSpaceOnUse); koClipPath1->setClipRule(Qt::WindingFill); shape1->setClipPath(koClipPath1); QScopedPointer group(new KoShapeGroup()); { QList shapes({shape1.take(), shape2.take()}); KoShapeGroupCommand cmd(group.data(), shapes, false); cmd.redo(); } QScopedPointer clipShape2(KoPathShape::createShapeFromPainterPath(clipPath2)); KoClipPath *koClipPath2 = new KoClipPath({clipShape2.take()}, KoFlake::ObjectBoundingBox); koClipPath2->setClipRule(Qt::WindingFill); group->setClipPath(koClipPath2); SvgRenderTester::testRender(group.take(), "load", "relative_clip_render_test", QSize(30,30)); } void TestSvgParser::testRenderClipPath_User() { const QString data = "" "" " " " " "" "" " " "" "" " " " " "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("clip_render_test", false); } void TestSvgParser::testRenderClipPath_Obb() { const QString data = "" "" " " " " "" "" " " "" "" " " " " "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("relative_clip_render_test", false); } void TestSvgParser::testRenderClipPath_Obb_Transform() { const QString data = "" "" " " " " "" "" " " "" "" " " " " "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("clip_render_test_rotated", false); } void TestSvgParser::testRenderClipMask_Obb() { const QString data = "" //"" " " " " " " " " " " " " " " //"" "" " " " " "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("clip_mask_obb", false); } void TestSvgParser::testRenderClipMask_User_Clip_Obb() { const QString data = "" //"" " " " " " " " " " " " " " " //"" "" " " " " "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("clip_mask_obb", false); } void TestSvgParser::testRenderClipMask_User_Clip_User() { const QString data = "" //"" " " " " " " " " " " " " " " //"" "" " " " " "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("clip_mask_obb", false); } QByteArray fileFetcherFunc(const QString &name) { const QString fileName = TestUtil::fetchDataFileLazy(name); QFile file(fileName); KIS_ASSERT(file.exists()); file.open(QIODevice::ReadOnly); return file.readAll(); } void TestSvgParser::testRenderImage_AspectDefault() { const QString data = "" "" " " " " " " " My image" " " "" ""; SvgRenderTester t (data); t.parser.setFileFetcher(fileFetcherFunc); t.test_standard_30px_72ppi("image_aspect_default", false); } void TestSvgParser::testRenderImage_AspectNone() { const QString data = "" "" " " " " " " " My image" " " "" ""; SvgRenderTester t (data); t.setFuzzyThreshold(5); t.parser.setFileFetcher(fileFetcherFunc); t.test_standard_30px_72ppi("image_aspect_none", false); } void TestSvgParser::testRenderImage_AspectMeet() { const QString data = "" "" " " " " " " " My image" " " "" ""; SvgRenderTester t (data); t.parser.setFileFetcher(fileFetcherFunc); t.test_standard_30px_72ppi("image_aspect_meet", false); } void TestSvgParser::testRectShapeRoundUniformX() { const QString data = "" " " ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("rect_5_5", false); } void TestSvgParser::testRectShapeRoundUniformY() { const QString data = "" " " ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("rect_5_5", false); } void TestSvgParser::testRectShapeRoundXY() { const QString data = "" " " ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("rect_5_10", false); } void TestSvgParser::testRectShapeRoundXYOverflow() { const QString data = "" " " ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("rect_5_10", false); } void TestSvgParser::testCircleShape() { const QString data = "" " " ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("circle", false); } void TestSvgParser::testEllipseShape() { const QString data = "" " " ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("ellipse", false); } void TestSvgParser::testLineShape() { const QString data = "" " " ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("line", false); } void TestSvgParser::testPolylineShape() { const QString data = "" " " ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("polyline", false); } void TestSvgParser::testPolygonShape() { const QString data = "" " " ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("polygon", false); } void TestSvgParser::testPathShape() { const QString data = "" " " ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("polygon", false); } void TestSvgParser::testDefsHidden() { const QString data = "" "" " " " " " " " " "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("test_defs_hidden", false); } void TestSvgParser::testDefsUseInheritance() { const QString data = "" "" " " " " " " "" /** * NOTES: * 1) width/height attributes for are not implemented yet * 2) x and y are summed up * 3) stroke="white" is overridden by the original templated object * 4) fill="green" attribute from is not inherited */ "" " " " " "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("defs_use_inheritance", false); } void TestSvgParser::testUseWithoutDefs() { const QString data = "" // technical rect for rendering "" "" " " "" /** * NOTES: * 1) width/height attributes for are not implemented yet * 2) x and y are summed up * 3) stroke="white" is overridden by the original templated object * 4) fill="green" attribute from is not inherited */ "" " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("use_without_defs", false); } void TestSvgParser::testMarkersAutoOrientation() { const QString data = "" "" " " " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("markers", false); } void TestSvgParser::testMarkersAutoOrientationScaled() { const QString data = "" "" " " " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("markers_scaled", false); } void TestSvgParser::testMarkersAutoOrientationScaledUserCoordinates() { const QString data = "" "" " " " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("markers_user_coordinates", false); } void TestSvgParser::testMarkersCustomOrientation() { const QString data = "" "" " " " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("markers_custom_orientation", false); } void TestSvgParser::testMarkersDifferent() { const QString data = "" "" " " " " " " "" "" " " " " " " "" "" " " " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("markers_different", false); } void TestSvgParser::testMarkersFillAsShape() { const QString data = "" "" " " " " " " " " " " " " " " " " " " "" "" //" d=\"M5,15 L25,15\"/>" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("markers_scaled_fill_as_shape", false); } void TestSvgParser::testMarkersOnClosedPath() { const QString data = "" "" " " " " " " "" "" " " " " " " "" "" " " " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("markers_on_closed_path", false); } void TestSvgParser::testGradientRecoveringTransform() { // used for experimenting purposes only! QImage image(100,100,QImage::Format_ARGB32); image.fill(0); QPainter painter(&image); painter.setPen(QPen(Qt::black, 0)); QLinearGradient gradient(0, 0.5, 1, 0.5); gradient.setCoordinateMode(QGradient::ObjectBoundingMode); //QLinearGradient gradient(0, 50, 100, 50); //gradient.setCoordinateMode(QGradient::LogicalMode); gradient.setColorAt(0.0, Qt::red); gradient.setColorAt(1.0, Qt::blue); QTransform gradientTransform; gradientTransform.shear(0.2, 0); { QBrush brush(gradient); brush.setTransform(gradientTransform); painter.setBrush(brush); } QRect mainShape(3,3,94,94); painter.drawRect(mainShape); QTransform gradientToUser(mainShape.width(), 0, 0, mainShape.height(), mainShape.x(), mainShape.y()); QRect smallShape(0,0,20,20); QTransform smallShapeTransform; { smallShapeTransform = QTransform::fromTranslate(-smallShape.center().x(), -smallShape.center().y()); QTransform r; r.rotate(90); smallShapeTransform *= r; smallShapeTransform *= QTransform::fromTranslate(mainShape.center().x(), mainShape.center().y()); } { gradient.setCoordinateMode(QGradient::LogicalMode); QBrush brush(gradient); brush.setTransform(gradientTransform * gradientToUser * smallShapeTransform.inverted()); painter.setBrush(brush); painter.setPen(Qt::NoPen); } painter.setTransform(smallShapeTransform); painter.drawRect(smallShape); //image.save("gradient_recovering_transform.png"); } void TestSvgParser::testMarkersAngularUnits() { const QString data = "" // start "" " " " " " " "" // end "" " " " " " " "" // mid "" " " " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("markers_angular_units", false); } #include "KoParameterShape.h" void TestSvgParser::testSodipodiArcShape() { const QString data = "" "" " d=\" some weird unparsable text \" />" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("sodipodi_closed_arc", false); KoShape *shape = t.findShape("testRect"); QVERIFY(dynamic_cast(shape)); } void TestSvgParser::testSodipodiArcShapeOpen() { const QString data = "" "" " d=\" some weird unparsable text \" />" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("sodipodi_open_arc", false); KoShape *shape = t.findShape("testRect"); QVERIFY(dynamic_cast(shape)); } void TestSvgParser::testKritaChordShape() { const QString data = "" "" " d=\" some weird unparsable text \" />" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("sodipodi_chord_arc", false); KoShape *shape = t.findShape("testRect"); QVERIFY(dynamic_cast(shape)); } void TestSvgParser::testSodipodiChordShape() { const QString data = "" "" " d=\" some weird unparsable text \" />" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("sodipodi_chord_arc", false); KoShape *shape = t.findShape("testRect"); QVERIFY(dynamic_cast(shape)); } QTEST_MAIN(TestSvgParser) diff --git a/libs/flake/tests/TestSvgText.cpp b/libs/flake/tests/TestSvgText.cpp index d2bb6bd225..da81be0d53 100644 --- a/libs/flake/tests/TestSvgText.cpp +++ b/libs/flake/tests/TestSvgText.cpp @@ -1,1328 +1,1329 @@ /* * Copyright (c) 2017 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 "TestSvgText.h" #include #include "SvgParserTestingUtils.h" #include #include #include "KoSvgTextShapeMarkupConverter.h" #include #include #include void addProp(SvgLoadingContext &context, KoSvgTextProperties &props, const QString &attribute, const QString &value, KoSvgTextProperties::PropertyId id, int newValue) { props.parseSvgTextAttribute(context, attribute, value); if (props.property(id).toInt() != newValue) { qDebug() << "Failed to load the property:"; qDebug() << ppVar(attribute) << ppVar(value); qDebug() << ppVar(newValue); qDebug() << ppVar(props.property(id)); QFAIL("Fail :("); } } void addProp(SvgLoadingContext &context, KoSvgTextProperties &props, const QString &attribute, const QString &value, KoSvgTextProperties::PropertyId id, KoSvgText::AutoValue newValue) { props.parseSvgTextAttribute(context, attribute, value); if (props.property(id).value() != newValue) { qDebug() << "Failed to load the property:"; qDebug() << ppVar(attribute) << ppVar(value); qDebug() << ppVar(newValue); qDebug() << ppVar(props.property(id)); QFAIL("Fail :("); } QCOMPARE(props.property(id), QVariant::fromValue(newValue)); } void addProp(SvgLoadingContext &context, KoSvgTextProperties &props, const QString &attribute, const QString &value, KoSvgTextProperties::PropertyId id, qreal newValue) { props.parseSvgTextAttribute(context, attribute, value); if (props.property(id).toReal() != newValue) { qDebug() << "Failed to load the property:"; qDebug() << ppVar(attribute) << ppVar(value); qDebug() << ppVar(newValue); qDebug() << ppVar(props.property(id)); QFAIL("Fail :("); } } void TestSvgText::testTextProperties() { KoDocumentResourceManager resourceManager; SvgLoadingContext context(&resourceManager); context.pushGraphicsContext(); KoSvgTextProperties props; addProp(context, props, "writing-mode", "tb-rl", KoSvgTextProperties::WritingModeId, KoSvgText::TopToBottom); addProp(context, props, "writing-mode", "rl", KoSvgTextProperties::WritingModeId, KoSvgText::RightToLeft); addProp(context, props, "glyph-orientation-vertical", "auto", KoSvgTextProperties::GlyphOrientationVerticalId, KoSvgText::AutoValue()); addProp(context, props, "glyph-orientation-vertical", "0", KoSvgTextProperties::GlyphOrientationVerticalId, KoSvgText::AutoValue(0)); addProp(context, props, "glyph-orientation-vertical", "90", KoSvgTextProperties::GlyphOrientationVerticalId, KoSvgText::AutoValue(M_PI_2)); addProp(context, props, "glyph-orientation-vertical", "95", KoSvgTextProperties::GlyphOrientationVerticalId, KoSvgText::AutoValue(M_PI_2)); addProp(context, props, "glyph-orientation-vertical", "175", KoSvgTextProperties::GlyphOrientationVerticalId, KoSvgText::AutoValue(M_PI)); addProp(context, props, "glyph-orientation-vertical", "280", KoSvgTextProperties::GlyphOrientationVerticalId, KoSvgText::AutoValue(3 * M_PI_2)); addProp(context, props, "glyph-orientation-vertical", "350", KoSvgTextProperties::GlyphOrientationVerticalId, KoSvgText::AutoValue(0)); addProp(context, props, "glyph-orientation-vertical", "105", KoSvgTextProperties::GlyphOrientationVerticalId, KoSvgText::AutoValue(M_PI_2)); addProp(context, props, "glyph-orientation-horizontal", "0", KoSvgTextProperties::GlyphOrientationHorizontalId, 0.0); addProp(context, props, "glyph-orientation-horizontal", "90", KoSvgTextProperties::GlyphOrientationHorizontalId, M_PI_2); addProp(context, props, "glyph-orientation-horizontal", "95", KoSvgTextProperties::GlyphOrientationHorizontalId, M_PI_2); addProp(context, props, "glyph-orientation-horizontal", "175", KoSvgTextProperties::GlyphOrientationHorizontalId, M_PI); addProp(context, props, "glyph-orientation-horizontal", "280", KoSvgTextProperties::GlyphOrientationHorizontalId, 3 * M_PI_2); addProp(context, props, "direction", "rtl", KoSvgTextProperties::WritingModeId, KoSvgText::DirectionRightToLeft); addProp(context, props, "unicode-bidi", "embed", KoSvgTextProperties::UnicodeBidiId, KoSvgText::BidiEmbed); addProp(context, props, "unicode-bidi", "bidi-override", KoSvgTextProperties::UnicodeBidiId, KoSvgText::BidiOverride); addProp(context, props, "text-anchor", "middle", KoSvgTextProperties::TextAnchorId, KoSvgText::AnchorMiddle); addProp(context, props, "dominant-baseline", "ideographic", KoSvgTextProperties::DominantBaselineId, KoSvgText::DominantBaselineIdeographic); addProp(context, props, "alignment-baseline", "alphabetic", KoSvgTextProperties::AlignmentBaselineId, KoSvgText::AlignmentBaselineAlphabetic); addProp(context, props, "baseline-shift", "sub", KoSvgTextProperties::BaselineShiftModeId, KoSvgText::ShiftSub); addProp(context, props, "baseline-shift", "super", KoSvgTextProperties::BaselineShiftModeId, KoSvgText::ShiftSuper); addProp(context, props, "baseline-shift", "baseline", KoSvgTextProperties::BaselineShiftModeId, KoSvgText::ShiftNone); addProp(context, props, "baseline-shift", "10%", KoSvgTextProperties::BaselineShiftModeId, KoSvgText::ShiftPercentage); QCOMPARE(props.property(KoSvgTextProperties::BaselineShiftValueId).toDouble(), 0.1); context.currentGC()->font.setPointSizeF(180); addProp(context, props, "baseline-shift", "36", KoSvgTextProperties::BaselineShiftModeId, KoSvgText::ShiftPercentage); QCOMPARE(props.property(KoSvgTextProperties::BaselineShiftValueId).toDouble(), 0.2); addProp(context, props, "kerning", "auto", KoSvgTextProperties::KerningId, KoSvgText::AutoValue()); addProp(context, props, "kerning", "20", KoSvgTextProperties::KerningId, KoSvgText::AutoValue(20.0)); addProp(context, props, "letter-spacing", "normal", KoSvgTextProperties::LetterSpacingId, KoSvgText::AutoValue()); addProp(context, props, "letter-spacing", "20", KoSvgTextProperties::LetterSpacingId, KoSvgText::AutoValue(20.0)); addProp(context, props, "word-spacing", "normal", KoSvgTextProperties::WordSpacingId, KoSvgText::AutoValue()); addProp(context, props, "word-spacing", "20", KoSvgTextProperties::WordSpacingId, KoSvgText::AutoValue(20.0)); } void TestSvgText::testDefaultTextProperties() { KoSvgTextProperties props; QVERIFY(props.isEmpty()); QVERIFY(!props.hasProperty(KoSvgTextProperties::UnicodeBidiId)); QVERIFY(KoSvgTextProperties::defaultProperties().hasProperty(KoSvgTextProperties::UnicodeBidiId)); QCOMPARE(KoSvgTextProperties::defaultProperties().property(KoSvgTextProperties::UnicodeBidiId).toInt(), int(KoSvgText::BidiNormal)); props = KoSvgTextProperties::defaultProperties(); QVERIFY(props.hasProperty(KoSvgTextProperties::UnicodeBidiId)); QCOMPARE(props.property(KoSvgTextProperties::UnicodeBidiId).toInt(), int(KoSvgText::BidiNormal)); } void TestSvgText::testTextPropertiesDifference() { using namespace KoSvgText; KoSvgTextProperties props; props.setProperty(KoSvgTextProperties::WritingModeId, RightToLeft); props.setProperty(KoSvgTextProperties::DirectionId, DirectionRightToLeft); props.setProperty(KoSvgTextProperties::UnicodeBidiId, BidiEmbed); props.setProperty(KoSvgTextProperties::TextAnchorId, AnchorEnd); props.setProperty(KoSvgTextProperties::DominantBaselineId, DominantBaselineNoChange); props.setProperty(KoSvgTextProperties::AlignmentBaselineId, AlignmentBaselineIdeographic); props.setProperty(KoSvgTextProperties::BaselineShiftModeId, ShiftPercentage); props.setProperty(KoSvgTextProperties::BaselineShiftValueId, 0.5); props.setProperty(KoSvgTextProperties::KerningId, fromAutoValue(AutoValue(10))); props.setProperty(KoSvgTextProperties::GlyphOrientationVerticalId, fromAutoValue(AutoValue(90))); props.setProperty(KoSvgTextProperties::GlyphOrientationHorizontalId, fromAutoValue(AutoValue(180))); props.setProperty(KoSvgTextProperties::LetterSpacingId, fromAutoValue(AutoValue(20))); props.setProperty(KoSvgTextProperties::WordSpacingId, fromAutoValue(AutoValue(30))); KoSvgTextProperties newProps = props; newProps.setProperty(KoSvgTextProperties::KerningId, fromAutoValue(AutoValue(11))); newProps.setProperty(KoSvgTextProperties::LetterSpacingId, fromAutoValue(AutoValue(21))); KoSvgTextProperties diff = newProps.ownProperties(props); QVERIFY(diff.hasProperty(KoSvgTextProperties::KerningId)); QVERIFY(diff.hasProperty(KoSvgTextProperties::LetterSpacingId)); QVERIFY(!diff.hasProperty(KoSvgTextProperties::WritingModeId)); QVERIFY(!diff.hasProperty(KoSvgTextProperties::DirectionId)); } void TestSvgText::testParseFontStyles() { const QString data = "" " Hello, out there" ""; KoXmlDocument doc; QVERIFY(doc.setContent(data.toLatin1())); KoXmlElement root = doc.documentElement(); KoDocumentResourceManager resourceManager; SvgLoadingContext context(&resourceManager); context.pushGraphicsContext(); SvgStyles styles = context.styleParser().collectStyles(root); context.styleParser().parseFont(styles); //QCOMPARE(styles.size(), 3); // TODO: multiple fonts! QCOMPARE(context.currentGC()->font.family(), QString("Verdana")); { QStringList expectedFonts = {"Verdana", "Times New Roman", "serif"}; QCOMPARE(context.currentGC()->fontFamiliesList, expectedFonts); } QCOMPARE(context.currentGC()->font.pointSizeF(), 15.0); QCOMPARE(context.currentGC()->font.style(), QFont::StyleOblique); QCOMPARE(context.currentGC()->font.capitalization(), QFont::SmallCaps); QCOMPARE(context.currentGC()->font.weight(), 66); { SvgStyles fontModifier; fontModifier["font-weight"] = "bolder"; context.styleParser().parseFont(fontModifier); QCOMPARE(context.currentGC()->font.weight(), 75); } { SvgStyles fontModifier; fontModifier["font-weight"] = "lighter"; context.styleParser().parseFont(fontModifier); QCOMPARE(context.currentGC()->font.weight(), 66); } QCOMPARE(context.currentGC()->font.stretch(), int(QFont::ExtraCondensed)); { SvgStyles fontModifier; fontModifier["font-stretch"] = "narrower"; context.styleParser().parseFont(fontModifier); QCOMPARE(context.currentGC()->font.stretch(), int(QFont::UltraCondensed)); } { SvgStyles fontModifier; fontModifier["font-stretch"] = "wider"; context.styleParser().parseFont(fontModifier); QCOMPARE(context.currentGC()->font.stretch(), int(QFont::ExtraCondensed)); } { SvgStyles fontModifier; fontModifier["text-decoration"] = "underline"; context.styleParser().parseFont(fontModifier); QCOMPARE(context.currentGC()->font.underline(), true); } { SvgStyles fontModifier; fontModifier["text-decoration"] = "overline"; context.styleParser().parseFont(fontModifier); QCOMPARE(context.currentGC()->font.overline(), true); } { SvgStyles fontModifier; fontModifier["text-decoration"] = "line-through"; context.styleParser().parseFont(fontModifier); QCOMPARE(context.currentGC()->font.strikeOut(), true); } { SvgStyles fontModifier; fontModifier["text-decoration"] = " line-through overline"; context.styleParser().parseFont(fontModifier); QCOMPARE(context.currentGC()->font.underline(), false); QCOMPARE(context.currentGC()->font.strikeOut(), true); QCOMPARE(context.currentGC()->font.overline(), true); } } void TestSvgText::testParseTextStyles() { const QString data = "" " Hello, out there" ""; KoXmlDocument doc; QVERIFY(doc.setContent(data.toLatin1())); KoXmlElement root = doc.documentElement(); KoDocumentResourceManager resourceManager; SvgLoadingContext context(&resourceManager); context.pushGraphicsContext(); SvgStyles styles = context.styleParser().collectStyles(root); context.styleParser().parseFont(styles); QCOMPARE(context.currentGC()->font.family(), QString("Verdana")); KoSvgTextProperties &props = context.currentGC()->textProperties; QCOMPARE(props.property(KoSvgTextProperties::WritingModeId).toInt(), int(KoSvgText::TopToBottom)); QCOMPARE(props.property(KoSvgTextProperties::GlyphOrientationVerticalId).value(), KoSvgText::AutoValue(M_PI_2)); } #include #include #include void TestSvgText::testSimpleText() { const QString data = "" "" " " " " " Hello, out there!" " " "" ""; QFont testFont("DejaVu Sans"); if (!QFontInfo(testFont).exactMatch()) { QEXPECT_FAIL(0, "DejaVu Sans is *not* found! Text rendering might be broken!", Continue); } SvgRenderTester t (data); t.test_standard("text_simple", QSize(175, 40), 72.0); KoShape *shape = t.findShape("testRect"); KoSvgTextChunkShape *chunkShape = dynamic_cast(shape); QVERIFY(chunkShape); // root shape is not just a chunk! QVERIFY(dynamic_cast(shape)); QCOMPARE(chunkShape->shapeCount(), 0); QCOMPARE(chunkShape->layoutInterface()->isTextNode(), true); QCOMPARE(chunkShape->layoutInterface()->numChars(), 17); QCOMPARE(chunkShape->layoutInterface()->nodeText(), QString("Hello, out there!")); QVector transform = chunkShape->layoutInterface()->localCharTransformations(); QCOMPARE(transform.size(), 1); QVERIFY(bool(transform[0].xPos)); QVERIFY(bool(transform[0].yPos)); QVERIFY(!transform[0].dxPos); QVERIFY(!transform[0].dyPos); QVERIFY(!transform[0].rotate); QCOMPARE(*transform[0].xPos, 7.0); QCOMPARE(*transform[0].yPos, 27.0); QVector subChunks = chunkShape->layoutInterface()->collectSubChunks(); QCOMPARE(subChunks.size(), 1); QCOMPARE(subChunks[0].text.size(), 17); //qDebug() << ppVar(subChunks[0].text); //qDebug() << ppVar(subChunks[0].transformation); //qDebug() << ppVar(subChunks[0].format); } inline KoSvgTextChunkShape* toChunkShape(KoShape *shape) { KoSvgTextChunkShape *chunkShape = dynamic_cast(shape); KIS_ASSERT(chunkShape); return chunkShape; } void TestSvgText::testComplexText() { const QString data = "" "" " " " " " Hello, ou" "t there cool cdata --> nice work" " " "" ""; SvgRenderTester t (data); t.test_standard("text_complex", QSize(385, 56), 72.0); KoSvgTextChunkShape *baseShape = toChunkShape(t.findShape("testRect")); QVERIFY(baseShape); // root shape is not just a chunk! QVERIFY(dynamic_cast(baseShape)); QCOMPARE(baseShape->shapeCount(), 4); QCOMPARE(baseShape->layoutInterface()->isTextNode(), false); QCOMPARE(baseShape->layoutInterface()->numChars(), 41); { // chunk 0: "Hello, " KoSvgTextChunkShape *chunk = toChunkShape(baseShape->shapes()[0]); QCOMPARE(chunk->shapeCount(), 0); QCOMPARE(chunk->layoutInterface()->isTextNode(), true); QCOMPARE(chunk->layoutInterface()->numChars(), 7); QCOMPARE(chunk->layoutInterface()->nodeText(), QString("Hello, ")); QVector transform = chunk->layoutInterface()->localCharTransformations(); QCOMPARE(transform.size(), 7); QVERIFY(bool(transform[0].xPos)); QVERIFY(!bool(transform[1].xPos)); for (int i = 0; i < 7; i++) { QVERIFY(!i || bool(transform[i].dxPos)); if (i) { QCOMPARE(*transform[i].dxPos, qreal(i)); } } QVector subChunks = chunk->layoutInterface()->collectSubChunks(); QCOMPARE(subChunks.size(), 7); QCOMPARE(subChunks[0].text.size(), 1); QCOMPARE(*subChunks[0].transformation.xPos, 7.0); QVERIFY(!subChunks[1].transformation.xPos); } { // chunk 1: "out" KoSvgTextChunkShape *chunk = toChunkShape(baseShape->shapes()[1]); QCOMPARE(chunk->shapeCount(), 0); QCOMPARE(chunk->layoutInterface()->isTextNode(), true); QCOMPARE(chunk->layoutInterface()->numChars(), 3); QCOMPARE(chunk->layoutInterface()->nodeText(), QString("out")); QVector transform = chunk->layoutInterface()->localCharTransformations(); QCOMPARE(transform.size(), 2); QVERIFY(bool(transform[0].xPos)); QVERIFY(!bool(transform[1].xPos)); for (int i = 0; i < 2; i++) { QVERIFY(bool(transform[i].dxPos)); QCOMPARE(*transform[i].dxPos, qreal(i + 7)); } QVector subChunks = chunk->layoutInterface()->collectSubChunks(); QCOMPARE(subChunks.size(), 2); QCOMPARE(subChunks[0].text.size(), 1); QCOMPARE(subChunks[1].text.size(), 2); } { // chunk 2: " there " KoSvgTextChunkShape *chunk = toChunkShape(baseShape->shapes()[2]); QCOMPARE(chunk->shapeCount(), 0); QCOMPARE(chunk->layoutInterface()->isTextNode(), true); QCOMPARE(chunk->layoutInterface()->numChars(), 7); QCOMPARE(chunk->layoutInterface()->nodeText(), QString(" there ")); QVector transform = chunk->layoutInterface()->localCharTransformations(); QCOMPARE(transform.size(), 0); QVector subChunks = chunk->layoutInterface()->collectSubChunks(); QCOMPARE(subChunks.size(), 1); QCOMPARE(subChunks[0].text.size(), 7); } { // chunk 3: "cool cdata --> nice work" KoSvgTextChunkShape *chunk = toChunkShape(baseShape->shapes()[3]); QCOMPARE(chunk->shapeCount(), 0); QCOMPARE(chunk->layoutInterface()->isTextNode(), true); QCOMPARE(chunk->layoutInterface()->numChars(), 24); QCOMPARE(chunk->layoutInterface()->nodeText(), QString("cool cdata --> nice work")); QVector transform = chunk->layoutInterface()->localCharTransformations(); QCOMPARE(transform.size(), 0); QVector subChunks = chunk->layoutInterface()->collectSubChunks(); QCOMPARE(subChunks.size(), 1); QCOMPARE(subChunks[0].text.size(), 24); } } void TestSvgText::testHindiText() { const QString data = "" "" " " " " "मौखिक रूप से हिंदी के काफी सामान" " " "" ""; SvgRenderTester t (data); QFont testFont("FreeSans"); if (!QFontInfo(testFont).exactMatch()) { #ifdef USE_ROUND_TRIP return; #else QEXPECT_FAIL(0, "FreeSans found is *not* found! Hindi rendering might be broken!", Continue); #endif } t.test_standard("text_hindi", QSize(260, 30), 72); } void TestSvgText::testTextBaselineShift() { const QString data = "" "" " " " " " textsuper normalsub" " " "" ""; SvgRenderTester t (data); t.test_standard("text_baseline_shift", QSize(180, 40), 72); KoSvgTextChunkShape *baseShape = toChunkShape(t.findShape("testRect")); QVERIFY(baseShape); // root shape is not just a chunk! QVERIFY(dynamic_cast(baseShape)); } void TestSvgText::testTextSpacing() { const QString data = "" "" " " " " " Lorem ipsum" " Lorem ipsum (ls=4)" " Lorem ipsum (ls=-2)" " Lorem ipsum" " Lorem ipsum (ws=4)" " Lorem ipsum (ws=-2)" " Lorem ipsum" " Lorem ipsum (k=0)" " Lorem ipsum (k=2)" " Lorem ipsum (k=2,ls=2)" " " "" ""; SvgRenderTester t (data); t.setFuzzyThreshold(5); t.test_standard("text_letter_word_spacing", QSize(340, 250), 72.0); KoSvgTextChunkShape *baseShape = toChunkShape(t.findShape("testRect")); QVERIFY(baseShape); // root shape is not just a chunk! QVERIFY(dynamic_cast(baseShape)); } void TestSvgText::testTextTabSpacing() { const QString data = "" "" " " " " " Lorem" " ipsum" " dolor sit amet," " consectetur adipiscing elit." " " "" ""; SvgRenderTester t (data); t.setFuzzyThreshold(5); t.test_standard("text_tab_spacing", QSize(400, 170), 72.0); KoSvgTextChunkShape *baseShape = toChunkShape(t.findShape("testRect")); QVERIFY(baseShape); // root shape is not just a chunk! QVERIFY(dynamic_cast(baseShape)); } void TestSvgText::testTextDecorations() { const QString data = "" "" " " " " " Lorem ipsum" " Lorem ipsum" " Lorem ipsum" " Lorem ipsum (WRONG!!!)" " " "" ""; SvgRenderTester t (data); t.setFuzzyThreshold(5); t.test_standard("text_decorations", QSize(290, 135), 72.0); KoSvgTextChunkShape *baseShape = toChunkShape(t.findShape("testRect")); QVERIFY(baseShape); // root shape is not just a chunk! QVERIFY(dynamic_cast(baseShape)); } void TestSvgText::testRightToLeft() { const QString data = "" "" " " " " " aa bb cc dd" " حادثتا السفينتين «بسين Bassein» و«فايبر Viper»" " *" " aa bb حادثتا السفينتين بسين cc dd " " aa bb حادثتا السفينتين بسين cc dd " " *" " aa bb حادثتا السفينتين بسين cc dd " " aa bb حادثتا السفينتين بسين cc dd " " aa bb حادثتا السفينتين بسين cc dd " " *" " الناطقون: 295 مليون - 422 مليون" " Spoken: 295 مليون - 422 مليون " " *" " aa bb c1 c2 c3 c4 dd ee" " " "" ""; SvgRenderTester t (data); t.test_standard("text_right_to_left", QSize(500,450), 72.0); KoSvgTextChunkShape *baseShape = toChunkShape(t.findShape("testRect")); QVERIFY(baseShape); // root shape is not just a chunk! QVERIFY(dynamic_cast(baseShape)); } #include #include +#include void TestSvgText::testQtBidi() { // Arabic text sample from Wikipedia: // https://ar.wikipedia.org/wiki/%D8%A5%D9%85%D8%A7%D8%B1%D8%A7%D8%AA_%D8%A7%D9%84%D8%B3%D8%A7%D8%AD%D9%84_%D8%A7%D9%84%D9%85%D8%AA%D8%B5%D8%A7%D9%84%D8%AD QStringList ltrText; ltrText << "aa bb cc dd"; ltrText << "aa bb حادثتا السفينتين بسين cc dd"; ltrText << "aa bb \u202ec1c2 d3d4\u202C ee ff"; QStringList rtlText; rtlText << "حادثتا السفينتين «بسين Bassein» و«فايبر Viper»"; rtlText << "حادثتا السفينتين «بسين aa bb cc dd» و«فايبر Viper»"; QImage canvas(500,500,QImage::Format_ARGB32); QPainter gc(&canvas); QPointF pos(15,15); QVector textSamples; textSamples << ltrText; textSamples << rtlText; QVector textDirections; textDirections << Qt::LeftToRight; textDirections << Qt::RightToLeft; for (int i = 0; i < textSamples.size(); i++) { Q_FOREACH (const QString str, textSamples[i]) { QTextOption option; option.setTextDirection(textDirections[i]); option.setUseDesignMetrics(true); QTextLayout layout; layout.setText(str); layout.setFont(QFont("serif", 15.0)); layout.setCacheEnabled(true); layout.beginLayout(); QTextLine line = layout.createLine(); line.setPosition(pos); pos.ry() += 25; layout.endLayout(); layout.draw(&gc, QPointF()); } } canvas.save("test_bidi.png"); } void TestSvgText::testQtDxDy() { QImage canvas(500,500,QImage::Format_ARGB32); QPainter gc(&canvas); QPointF pos(15,15); QTextOption option; option.setTextDirection(Qt::LeftToRight); option.setUseDesignMetrics(true); option.setWrapMode(QTextOption::WrapAnywhere); QTextLayout layout; layout.setText("aa bb cc dd ee ff"); layout.setFont(QFont("serif", 15.0)); layout.setCacheEnabled(true); layout.beginLayout(); layout.setTextOption(option); { QTextLine line = layout.createLine(); line.setPosition(pos); line.setNumColumns(4); } pos.ry() += 25; pos.rx() += 30; { QTextLine line = layout.createLine(); line.setPosition(pos); } layout.endLayout(); layout.draw(&gc, QPointF()); canvas.save("test_dxdy.png"); } void TestSvgText::testTextOutlineSolid() { const QString data = "" "" " " " " " SA" " " "" ""; SvgRenderTester t (data); t.test_standard("text_outline_solid", QSize(30, 30), 72.0); } void TestSvgText::testNbspHandling() { const QString data = "" "" " " " " " S\u00A0A" " " "" ""; SvgRenderTester t (data); t.test_standard("text_nbsp", QSize(30, 30), 72.0); } void TestSvgText::testMulticolorText() { const QString data = "" "" " " " " " SA" " " "" ""; SvgRenderTester t (data); t.setFuzzyThreshold(5); t.test_standard("text_multicolor", QSize(30, 30), 72.0); } #include void TestSvgText::testConvertToStrippedSvg() { const QString data = "" "" " " " " " SAsome stuff<><><<<>" " " "" ""; SvgRenderTester t (data); t.parser.setResolution(QRectF(QPointF(), QSizeF(30,30)) /* px */, 72.0/* ppi */); t.run(); KoSvgTextShape *baseShape = dynamic_cast(t.findShape("testRect")); QVERIFY(baseShape); { KoColorBackground *bg = dynamic_cast(baseShape->background().data()); QVERIFY(bg); QCOMPARE(bg->color(), QColor(Qt::blue)); } KoSvgTextShapeMarkupConverter converter(baseShape); QString svgText; QString stylesText; QVERIFY(converter.convertToSvg(&svgText, &stylesText)); QCOMPARE(stylesText, QString("")); QCOMPARE(svgText, QString("SAsome stuff<><><<<>")); // test loading svgText = "SAsome stuff<><><<<>"; QVERIFY(converter.convertFromSvg(svgText, stylesText, QRectF(0,0,30,30), 72.0)); { KoColorBackground *bg = dynamic_cast(baseShape->background().data()); QVERIFY(bg); QCOMPARE(bg->color(), QColor(Qt::green)); } { KoSvgTextProperties props = baseShape->textProperties(); QVERIFY(props.hasProperty(KoSvgTextProperties::FontSizeId)); const qreal fontSize = props.property(KoSvgTextProperties::FontSizeId).toReal(); QCOMPARE(fontSize, 19.0); } QCOMPARE(baseShape->shapeCount(), 3); } void TestSvgText::testConvertToStrippedSvgNullOrigin() { const QString data = "" "" " " " " " SAsome stuff<><><<<>" " " "" ""; SvgRenderTester t (data); t.parser.setResolution(QRectF(QPointF(), QSizeF(30,30)) /* px */, 72.0/* ppi */); t.run(); KoSvgTextShape *baseShape = dynamic_cast(t.findShape("testRect")); QVERIFY(baseShape); KoSvgTextShapeMarkupConverter converter(baseShape); QString svgText; QString stylesText; QVERIFY(converter.convertToSvg(&svgText, &stylesText)); QCOMPARE(stylesText, QString("")); QCOMPARE(svgText, QString("SAsome stuff<><><<<>")); } void TestSvgText::testConvertFromIncorrectStrippedSvg() { QScopedPointer baseShape(new KoSvgTextShape()); KoSvgTextShapeMarkupConverter converter(baseShape.data()); QString svgText; QString stylesText; svgText = "blah text"; QVERIFY(converter.convertFromSvg(svgText, stylesText, QRectF(0,0,30,30), 72.0)); QCOMPARE(converter.errors().size(), 0); svgText = ">><<>"; QVERIFY(!converter.convertFromSvg(svgText, stylesText, QRectF(0,0,30,30), 72.0)); qDebug() << ppVar(converter.errors()); QCOMPARE(converter.errors().size(), 1); svgText = "blah text"; QVERIFY(!converter.convertFromSvg(svgText, stylesText, QRectF(0,0,30,30), 72.0)); qDebug() << ppVar(converter.errors()); QCOMPARE(converter.errors().size(), 1); svgText = ""; QVERIFY(!converter.convertFromSvg(svgText, stylesText, QRectF(0,0,30,30), 72.0)); qDebug() << ppVar(converter.errors()); QCOMPARE(converter.errors().size(), 1); } void TestSvgText::testEmptyTextChunk() { const QString data = "" "" " " " " " " // no actual text! should not crash! " " "" ""; SvgRenderTester t (data); // it just shouldn't assert or fail when seeing an empty text block t.parser.setResolution(QRectF(QPointF(), QSizeF(30,30)) /* px */, 72.0/* ppi */); t.run(); } void TestSvgText::testTrailingWhitespace() { QStringList chunkA; chunkA << "aaa"; chunkA << " aaa"; chunkA << "aaa "; chunkA << " aaa "; QStringList chunkB; chunkB << "bbb"; chunkB << " bbb"; chunkB << "bbb "; chunkB << " bbb "; QStringList linkChunk; linkChunk << ""; linkChunk << " "; linkChunk << ""; linkChunk << " "; const QString dataTemplate = "" "" " " " " " %1%2%3" " " "" ""; for (auto itL = linkChunk.constBegin(); itL != linkChunk.constEnd(); ++itL) { for (auto itA = chunkA.constBegin(); itA != chunkA.constEnd(); ++itA) { for (auto itB = chunkB.constBegin(); itB != chunkB.constEnd(); ++itB) { if (itA->rightRef(1) != " " && itB->leftRef(1) != " " && *itL != " " && *itL != linkChunk.last()) continue; QString cleanLink = *itL; cleanLink.replace('/', '_'); qDebug() << "Testcase:" << *itA << cleanLink << *itB; const QString data = dataTemplate.arg(*itA, *itL, *itB); SvgRenderTester t (data); t.setFuzzyThreshold(5); //t.test_standard(QString("text_trailing_%1_%2_%3").arg(*itA).arg(cleanLink).arg(*itB), QSize(70, 30), 72.0); // all files should look exactly the same! t.test_standard(QString("text_whitespace"), QSize(70, 30), 72.0); } } } } void TestSvgText::testConvertHtmlToSvg() { const QString html = "" "" "" "" "" "" "" "" "

" " Lorem ipsum dolor" "

" "

sit am" "et, consectetur adipiscing

" "

" " elit. " "

" "" ""; KoSvgTextShape shape; KoSvgTextShapeMarkupConverter converter(&shape); QString svg; QString defs; converter.convertFromHtml(html, &svg, &defs); bool r = converter.convertToSvg(&svg, &defs); qDebug() << r << svg << defs; } void TestSvgText::testTextWithMultipleRelativeOffsets() { const QString data = "" "" " " " Lorem ipsum dolor sit amet" " " "" ""; SvgRenderTester t (data); t.setFuzzyThreshold(5); t.test_standard("text_multiple_relative_offsets", QSize(300, 80), 72.0); } void TestSvgText::testTextWithMultipleAbsoluteOffsetsArabic() { /** * According to the standard, each **absolute** offset defines a * new text chunk, therefore, the arabic text must become * ltr reordered */ const QString data = "" "" " " " Lo rem اللغة العربية المعيارية الحديثة ip sum" " " "" ""; SvgRenderTester t (data); t.test_standard("text_multiple_absolute_offsets_arabic", QSize(530, 70), 72.0); } void TestSvgText::testTextWithMultipleRelativeOffsetsArabic() { /** * According to the standard, **relative** offsets must not define a new * text chunk, therefore, the arabic text must be written in native rtl order, * even though the individual letters are split. */ const QString data = "" "" " " " Lo rem اللغة العربية المعيارية الحديثة ip sum" " " "" ""; SvgRenderTester t (data); // we cannot expect more than one failure #ifndef USE_ROUND_TRIP QEXPECT_FAIL("", "WARNING: in Krita relative offsets also define a new text chunk, that doesn't comply with SVG standard and must be fixed", Continue); t.test_standard("text_multiple_relative_offsets_arabic", QSize(530, 70), 72.0); #endif } void TestSvgText::testTextOutline() { const QString data = "" "" " " " " " normal " " strikethrough" " overline" " underline" " " "" ""; QRect renderRect(0, 0, 450, 40); SvgRenderTester t (data); t.setFuzzyThreshold(5); t.test_standard("text_outline", renderRect.size(), 72.0); KoShape *shape = t.findShape("testRect"); KoSvgTextChunkShape *chunkShape = dynamic_cast(shape); QVERIFY(chunkShape); KoSvgTextShape *textShape = dynamic_cast(shape); QImage canvas(renderRect.size(), QImage::Format_ARGB32); canvas.fill(0); QPainter gc(&canvas); gc.setPen(Qt::NoPen); gc.setBrush(Qt::black); gc.setRenderHint(QPainter::Antialiasing, true); gc.drawPath(textShape->textOutline()); QVERIFY(TestUtil::checkQImage(canvas, "svg_render", "load_text_outline", "converted_to_path", 3, 5)); } QTEST_MAIN(TestSvgText) diff --git a/libs/flake/text/KoSvgTextChunkShape_p.h b/libs/flake/text/KoSvgTextChunkShape_p.h index 8c59d969ca..a9fd5c53f2 100644 --- a/libs/flake/text/KoSvgTextChunkShape_p.h +++ b/libs/flake/text/KoSvgTextChunkShape_p.h @@ -1,60 +1,61 @@ /* * Copyright (c) 2017 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 "KoSvgTextChunkShape.h" #include "KoSvgText.h" #include "KoSvgTextProperties.h" #include +#include #include class SvgGraphicsContext; class KoSvgTextChunkShapePrivate : public KoShapeContainerPrivate { public: KoSvgTextChunkShapePrivate(KoSvgTextChunkShape *_q); KoSvgTextChunkShapePrivate(const KoSvgTextChunkShapePrivate &rhs, KoSvgTextChunkShape *q); ~KoSvgTextChunkShapePrivate(); KoSvgTextProperties properties; QFont font; QStringList fontFamiliesList; QVector localTransformations; KoSvgText::AutoValue textLength; KoSvgText::LengthAdjust lengthAdjust = KoSvgText::LengthAdjustSpacing; QString text; struct LayoutInterface; QScopedPointer layoutInterface; QPainterPath associatedOutline; KoSvgText::KoSvgCharChunkFormat fetchCharFormat() const; void applyParentCharTransformations(const QVector transformations); void loadContextBasedProperties(SvgGraphicsContext *gc); bool isRichTextPreferred = true; Q_DECLARE_PUBLIC(KoSvgTextChunkShape) }; diff --git a/libs/flake/text/KoSvgTextShape.cpp b/libs/flake/text/KoSvgTextShape.cpp index 45b9459288..8fd5774399 100644 --- a/libs/flake/text/KoSvgTextShape.cpp +++ b/libs/flake/text/KoSvgTextShape.cpp @@ -1,649 +1,650 @@ /* * Copyright (c) 2017 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 "KoSvgTextShape.h" #include #include #include "KoSvgText.h" #include "KoSvgTextProperties.h" #include #include #include #include #include #include "kis_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include +#include #include #include #include class KoSvgTextShapePrivate : public KoSvgTextChunkShapePrivate { KoSvgTextShapePrivate(KoSvgTextShape *_q) : KoSvgTextChunkShapePrivate(_q) { } KoSvgTextShapePrivate(const KoSvgTextShapePrivate &rhs, KoSvgTextShape *q) : KoSvgTextChunkShapePrivate(rhs, q) { } std::vector> cachedLayouts; std::vector cachedLayoutsOffsets; QThread *cachedLayoutsWorkingThread = 0; void clearAssociatedOutlines(KoShape *rootShape); Q_DECLARE_PUBLIC(KoSvgTextShape) }; KoSvgTextShape::KoSvgTextShape() : KoSvgTextChunkShape(new KoSvgTextShapePrivate(this)) { setShapeId(KoSvgTextShape_SHAPEID); } KoSvgTextShape::KoSvgTextShape(const KoSvgTextShape &rhs) : KoSvgTextChunkShape(new KoSvgTextShapePrivate(*rhs.d_func(), this)) { setShapeId(KoSvgTextShape_SHAPEID); // QTextLayout has no copy-ctor, so just relayout everything! relayout(); } KoSvgTextShape::~KoSvgTextShape() { } KoShape *KoSvgTextShape::cloneShape() const { return new KoSvgTextShape(*this); } void KoSvgTextShape::shapeChanged(ChangeType type, KoShape *shape) { KoSvgTextChunkShape::shapeChanged(type, shape); if (type == StrokeChanged || type == BackgroundChanged || type == ContentChanged) { relayout(); } } void KoSvgTextShape::paintComponent(QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintContext) { Q_D(KoSvgTextShape); Q_UNUSED(paintContext); /** * HACK ALERT: * QTextLayout should only be accessed from the thread it has been created in. * If the cached layout has been created in a different thread, we should just * recreate the layouts in the current thread to be able to render them. */ if (QThread::currentThread() != d->cachedLayoutsWorkingThread) { relayout(); } applyConversion(painter, converter); for (int i = 0; i < (int)d->cachedLayouts.size(); i++) { d->cachedLayouts[i]->draw(&painter, d->cachedLayoutsOffsets[i]); } /** * HACK ALERT: * The layouts of non-gui threads must be destroyed in the same thread * they have been created. Because the thread might be restarted in the * meantime or just destroyed, meaning that the per-thread freetype data * will not be available. */ if (QThread::currentThread() != qApp->thread()) { d->cachedLayouts.clear(); d->cachedLayoutsOffsets.clear(); d->cachedLayoutsWorkingThread = 0; } } void KoSvgTextShape::paintStroke(QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintContext) { Q_UNUSED(painter); Q_UNUSED(converter); Q_UNUSED(paintContext); // do nothing! everything is painted in paintComponent() } QPainterPath KoSvgTextShape::textOutline() { Q_D(KoSvgTextShape); QPainterPath result; result.setFillRule(Qt::WindingFill); for (int i = 0; i < (int)d->cachedLayouts.size(); i++) { const QPointF layoutOffset = d->cachedLayoutsOffsets[i]; const QTextLayout *layout = d->cachedLayouts[i].get(); for (int j = 0; j < layout->lineCount(); j++) { QTextLine line = layout->lineAt(j); Q_FOREACH (const QGlyphRun &run, line.glyphRuns()) { const QVector indexes = run.glyphIndexes(); const QVector positions = run.positions(); const QRawFont font = run.rawFont(); KIS_SAFE_ASSERT_RECOVER(indexes.size() == positions.size()) { continue; } for (int k = 0; k < indexes.size(); k++) { QPainterPath glyph = font.pathForGlyph(indexes[k]); glyph.translate(positions[k] + layoutOffset); result += glyph; } const qreal thickness = font.lineThickness(); const QRectF runBounds = run.boundingRect(); if (run.overline()) { // the offset is calculated to be consistent with the way how Qt renders the text const qreal y = line.y(); QRectF overlineBlob(runBounds.x(), y, runBounds.width(), thickness); overlineBlob.translate(layoutOffset); QPainterPath path; path.addRect(overlineBlob); // don't use direct addRect, because it doesn't care about Qt::WindingFill result += path; } if (run.strikeOut()) { // the offset is calculated to be consistent with the way how Qt renders the text const qreal y = line.y() + 0.5 * line.height(); QRectF strikeThroughBlob(runBounds.x(), y, runBounds.width(), thickness); strikeThroughBlob.translate(layoutOffset); QPainterPath path; path.addRect(strikeThroughBlob); // don't use direct addRect, because it doesn't care about Qt::WindingFill result += path; } if (run.underline()) { const qreal y = line.y() + line.ascent() + font.underlinePosition(); QRectF underlineBlob(runBounds.x(), y, runBounds.width(), thickness); underlineBlob.translate(layoutOffset); QPainterPath path; path.addRect(underlineBlob); // don't use direct addRect, because it doesn't care about Qt::WindingFill result += path; } } } } return result; } void KoSvgTextShape::resetTextShape() { KoSvgTextChunkShape::resetTextShape(); relayout(); } struct TextChunk { QString text; QVector formats; Qt::LayoutDirection direction = Qt::LeftToRight; Qt::Alignment alignment = Qt::AlignLeading; struct SubChunkOffset { QPointF offset; int start = 0; }; QVector offsets; boost::optional xStartPos; boost::optional yStartPos; QPointF applyStartPosOverride(const QPointF &pos) const { QPointF result = pos; if (xStartPos) { result.rx() = *xStartPos; } if (yStartPos) { result.ry() = *yStartPos; } return result; } }; QVector mergeIntoChunks(const QVector &subChunks) { QVector chunks; for (auto it = subChunks.begin(); it != subChunks.end(); ++it) { if (it->transformation.startsNewChunk() || it == subChunks.begin()) { TextChunk newChunk = TextChunk(); newChunk.direction = it->format.layoutDirection(); newChunk.alignment = it->format.calculateAlignment(); newChunk.xStartPos = it->transformation.xPos; newChunk.yStartPos = it->transformation.yPos; chunks.append(newChunk); } TextChunk ¤tChunk = chunks.last(); if (it->transformation.hasRelativeOffset()) { TextChunk::SubChunkOffset o; o.start = currentChunk.text.size(); o.offset = it->transformation.relativeOffset(); KIS_SAFE_ASSERT_RECOVER_NOOP(!o.offset.isNull()); currentChunk.offsets.append(o); } QTextLayout::FormatRange formatRange; formatRange.start = currentChunk.text.size(); formatRange.length = it->text.size(); formatRange.format = it->format; currentChunk.formats.append(formatRange); currentChunk.text += it->text; } return chunks; } /** * Qt's QTextLayout has a weird trait, it doesn't count space characters as * distinct characters in QTextLayout::setNumColumns(), that is, if we want to * position a block of text that starts with a space character in a specific * position, QTextLayout will drop this space and will move the text to the left. * * That is why we have a special wrapper object that ensures that no spaces are * dropped and their horizontal advance parameter is taken into account. */ struct LayoutChunkWrapper { LayoutChunkWrapper(QTextLayout *layout) : m_layout(layout) { } QPointF addTextChunk(int startPos, int length, const QPointF &textChunkStartPos) { QPointF currentTextPos = textChunkStartPos; const int lastPos = startPos + length - 1; KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(startPos == m_addedChars, currentTextPos); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(lastPos < m_layout->text().size(), currentTextPos); // qDebug() << m_layout->text(); QTextLine line; std::swap(line, m_danglingLine); if (!line.isValid()) { line = m_layout->createLine(); } // skip all the space characters that were not included into the Qt's text line const int currentLineStart = line.isValid() ? line.textStart() : startPos + length; while (startPos < currentLineStart && startPos <= lastPos) { currentTextPos.rx() += skipSpaceCharacter(startPos); startPos++; } if (startPos <= lastPos) { // defines the number of columns to look for glyphs const int numChars = lastPos - startPos + 1; // Tabs break the normal column flow // grow to avoid missing glyphs int charOffset = 0; int noChangeCount = 0; while (line.textLength() < numChars) { int tl = line.textLength(); line.setNumColumns(numChars + charOffset); if (tl == line.textLength()) { noChangeCount++; // 5 columns max are needed to discover tab char. Set to 10 to be safe. if (noChangeCount > 10) break; } else { noChangeCount = 0; } charOffset++; } line.setPosition(currentTextPos - QPointF(0, line.ascent())); currentTextPos.rx() += line.horizontalAdvance(); // skip all the space characters that were not included into the Qt's text line for (int i = line.textStart() + line.textLength(); i < lastPos; i++) { currentTextPos.rx() += skipSpaceCharacter(i); } } else { // keep the created but unused line for future use std::swap(line, m_danglingLine); } m_addedChars += length; return currentTextPos; } private: qreal skipSpaceCharacter(int pos) { const QTextCharFormat format = formatForPos(pos, m_layout->formats()); const QChar skippedChar = m_layout->text()[pos]; KIS_SAFE_ASSERT_RECOVER_NOOP(skippedChar.isSpace() || !skippedChar.isPrint()); QFontMetrics metrics(format.font()); #if QT_VERSION >= QT_VERSION_CHECK(5,11,0) return metrics.horizontalAdvance(skippedChar); #else return metrics.width(skippedChar); #endif } static QTextCharFormat formatForPos(int pos, const QVector &formats) { Q_FOREACH (const QTextLayout::FormatRange &range, formats) { if (pos >= range.start && pos < range.start + range.length) { return range.format; } } KIS_SAFE_ASSERT_RECOVER_NOOP(0 && "pos should be within the bounds of the layouted text"); return QTextCharFormat(); } private: int m_addedChars = 0; QTextLayout *m_layout; QTextLine m_danglingLine; }; void KoSvgTextShape::relayout() { Q_D(KoSvgTextShape); d->cachedLayouts.clear(); d->cachedLayoutsOffsets.clear(); d->cachedLayoutsWorkingThread = QThread::currentThread(); QPointF currentTextPos; QVector textChunks = mergeIntoChunks(layoutInterface()->collectSubChunks()); Q_FOREACH (const TextChunk &chunk, textChunks) { std::unique_ptr layout(new QTextLayout()); QTextOption option; // WARNING: never activate this option! It breaks the RTL text layout! //option.setFlags(QTextOption::ShowTabsAndSpaces); option.setWrapMode(QTextOption::WrapAnywhere); option.setUseDesignMetrics(true); // TODO: investigate if it is needed? option.setTextDirection(chunk.direction); layout->setText(chunk.text); layout->setTextOption(option); layout->setFormats(chunk.formats); layout->setCacheEnabled(true); layout->beginLayout(); currentTextPos = chunk.applyStartPosOverride(currentTextPos); const QPointF anchorPointPos = currentTextPos; int lastSubChunkStart = 0; QPointF lastSubChunkOffset; LayoutChunkWrapper wrapper(layout.get()); for (int i = 0; i <= chunk.offsets.size(); i++) { const bool isFinalPass = i == chunk.offsets.size(); const int length = !isFinalPass ? chunk.offsets[i].start - lastSubChunkStart : chunk.text.size() - lastSubChunkStart; if (length > 0) { currentTextPos += lastSubChunkOffset; currentTextPos = wrapper.addTextChunk(lastSubChunkStart, length, currentTextPos); } if (!isFinalPass) { lastSubChunkOffset = chunk.offsets[i].offset; lastSubChunkStart = chunk.offsets[i].start; } } layout->endLayout(); QPointF diff; if (chunk.alignment & Qt::AlignTrailing || chunk.alignment & Qt::AlignHCenter) { if (chunk.alignment & Qt::AlignTrailing) { diff = currentTextPos - anchorPointPos; } else if (chunk.alignment & Qt::AlignHCenter) { diff = 0.5 * (currentTextPos - anchorPointPos); } // TODO: fix after t2b text implemented diff.ry() = 0; } d->cachedLayouts.push_back(std::move(layout)); d->cachedLayoutsOffsets.push_back(-diff); } d->clearAssociatedOutlines(this); for (int i = 0; i < int(d->cachedLayouts.size()); i++) { const QTextLayout &layout = *d->cachedLayouts[i]; const QPointF layoutOffset = d->cachedLayoutsOffsets[i]; using namespace KoSvgText; Q_FOREACH (const QTextLayout::FormatRange &range, layout.formats()) { const KoSvgCharChunkFormat &format = static_cast(range.format); AssociatedShapeWrapper wrapper = format.associatedShapeWrapper(); const int rangeStart = range.start; const int safeRangeLength = range.length > 0 ? range.length : layout.text().size() - rangeStart; if (safeRangeLength <= 0) continue; const int rangeEnd = range.start + safeRangeLength - 1; const int firstLineIndex = layout.lineForTextPosition(rangeStart).lineNumber(); const int lastLineIndex = layout.lineForTextPosition(rangeEnd).lineNumber(); for (int i = firstLineIndex; i <= lastLineIndex; i++) { const QTextLine line = layout.lineAt(i); // It might happen that the range contains only one (or two) // symbol that is a whitespace symbol. In such a case we should // just skip this (invalid) line. if (!line.isValid()) continue; const int posStart = qMax(line.textStart(), rangeStart); const int posEnd = qMin(line.textStart() + line.textLength() - 1, rangeEnd); const QList glyphRuns = line.glyphRuns(posStart, posEnd - posStart + 1); Q_FOREACH (const QGlyphRun &run, glyphRuns) { const QPointF firstPosition = run.positions().first(); const quint32 firstGlyphIndex = run.glyphIndexes().first(); const QPointF lastPosition = run.positions().last(); const quint32 lastGlyphIndex = run.glyphIndexes().last(); const QRawFont rawFont = run.rawFont(); const QRectF firstGlyphRect = rawFont.boundingRect(firstGlyphIndex).translated(firstPosition); const QRectF lastGlyphRect = rawFont.boundingRect(lastGlyphIndex).translated(lastPosition); QRectF rect = run.boundingRect(); /** * HACK ALERT: there is a bug in a way how Qt calculates boundingRect() * of the glyph run. It doesn't care about left and right bearings * of the border chars in the run, therefore it becomes cropped. * * Here we just add a half-char width margin to both sides * of the glyph run to make sure the glyphs are fully painted. * * BUG: 389528 * BUG: 392068 */ rect.setLeft(qMin(rect.left(), lastGlyphRect.left()) - 0.5 * firstGlyphRect.width()); rect.setRight(qMax(rect.right(), lastGlyphRect.right()) + 0.5 * lastGlyphRect.width()); wrapper.addCharacterRect(rect.translated(layoutOffset)); } } } } } void KoSvgTextShapePrivate::clearAssociatedOutlines(KoShape *rootShape) { KoSvgTextChunkShape *chunkShape = dynamic_cast(rootShape); KIS_SAFE_ASSERT_RECOVER_RETURN(chunkShape); chunkShape->layoutInterface()->clearAssociatedOutline(); Q_FOREACH (KoShape *child, chunkShape->shapes()) { clearAssociatedOutlines(child); } } bool KoSvgTextShape::isRootTextNode() const { return true; } KoSvgTextShapeFactory::KoSvgTextShapeFactory() : KoShapeFactoryBase(KoSvgTextShape_SHAPEID, i18n("Text")) { setToolTip(i18n("SVG Text Shape")); setIconName(koIconNameCStr("x-shape-text")); setLoadingPriority(5); setXmlElementNames(KoXmlNS::svg, QStringList("text")); KoShapeTemplate t; t.name = i18n("SVG Text"); t.iconName = koIconName("x-shape-text"); t.toolTip = i18n("SVG Text Shape"); addTemplate(t); } KoShape *KoSvgTextShapeFactory::createDefaultShape(KoDocumentResourceManager *documentResources) const { debugFlake << "Create default svg text shape"; KoSvgTextShape *shape = new KoSvgTextShape(); shape->setShapeId(KoSvgTextShape_SHAPEID); KoSvgTextShapeMarkupConverter converter(shape); converter.convertFromSvg("Lorem ipsum dolor sit amet, consectetur adipiscing elit.", "", QRectF(0, 0, 200, 60), documentResources->documentResolution()); debugFlake << converter.errors() << converter.warnings(); return shape; } KoShape *KoSvgTextShapeFactory::createShape(const KoProperties *params, KoDocumentResourceManager *documentResources) const { KoSvgTextShape *shape = new KoSvgTextShape(); shape->setShapeId(KoSvgTextShape_SHAPEID); QString svgText = params->stringProperty("svgText", "Lorem ipsum dolor sit amet, consectetur adipiscing elit."); QString defs = params->stringProperty("defs" , ""); QRectF shapeRect = QRectF(0, 0, 200, 60); QVariant rect = params->property("shapeRect"); if (rect.type()==QVariant::RectF) { shapeRect = rect.toRectF(); } KoSvgTextShapeMarkupConverter converter(shape); converter.convertFromSvg(svgText, defs, shapeRect, documentResources->documentResolution()); shape->setPosition(shapeRect.topLeft()); return shape; } bool KoSvgTextShapeFactory::supports(const KoXmlElement &/*e*/, KoShapeLoadingContext &/*context*/) const { return false; } diff --git a/libs/flake/tools/KoPathTool.cpp b/libs/flake/tools/KoPathTool.cpp index dae2d081c9..ce043a4574 100644 --- a/libs/flake/tools/KoPathTool.cpp +++ b/libs/flake/tools/KoPathTool.cpp @@ -1,1308 +1,1309 @@ /* This file is part of the KDE project * Copyright (C) 2006-2012 Jan Hambrecht * Copyright (C) 2006,2007 Thorsten Zachmann * Copyright (C) 2007, 2010 Thomas Zander * Copyright (C) 2007 Boudewijn Rempt * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "KoPathTool.h" #include "KoToolBase_p.h" #include "KoPathShape_p.h" #include "KoPathToolHandle.h" #include "KoCanvasBase.h" #include "KoShapeManager.h" #include "KoSelectedShapesProxy.h" #include "KoDocumentResourceManager.h" #include "KoViewConverter.h" #include "KoSelection.h" #include "KoPointerEvent.h" #include "commands/KoPathPointTypeCommand.h" #include "commands/KoPathPointInsertCommand.h" #include "commands/KoPathPointRemoveCommand.h" #include "commands/KoPathSegmentTypeCommand.h" #include "commands/KoPathBreakAtPointCommand.h" #include "commands/KoPathSegmentBreakCommand.h" #include "commands/KoParameterToPathCommand.h" #include "commands/KoSubpathJoinCommand.h" #include #include #include #include "KoParameterShape.h" #include #include "KoPathPoint.h" #include "KoPathPointRubberSelectStrategy.h" #include "KoPathSegmentChangeStrategy.h" #include "KoPathConnectionPointStrategy.h" #include "KoParameterChangeStrategy.h" #include "PathToolOptionWidget.h" #include "KoConnectionShape.h" #include "KoSnapGuide.h" #include "KoShapeController.h" #include "kis_action_registry.h" #include #include #include "kis_command_utils.h" #include "kis_pointer_utils.h" #include #include #include #include #include #include +#include #include #include #include static const unsigned char needle_bits[] = { 0x00, 0x00, 0x10, 0x00, 0x20, 0x00, 0x60, 0x00, 0xc0, 0x00, 0xc0, 0x01, 0x80, 0x03, 0x80, 0x07, 0x00, 0x0f, 0x00, 0x1f, 0x00, 0x3e, 0x00, 0x7e, 0x00, 0x7c, 0x00, 0x1c, 0x00, 0x18, 0x00, 0x00 }; static const unsigned char needle_move_bits[] = { 0x00, 0x00, 0x10, 0x00, 0x20, 0x00, 0x60, 0x00, 0xc0, 0x00, 0xc0, 0x01, 0x80, 0x03, 0x80, 0x07, 0x10, 0x0f, 0x38, 0x1f, 0x54, 0x3e, 0xfe, 0x7e, 0x54, 0x7c, 0x38, 0x1c, 0x10, 0x18, 0x00, 0x00 }; // helper function to calculate the squared distance between two points qreal squaredDistance(const QPointF& p1, const QPointF &p2) { qreal dx = p1.x()-p2.x(); qreal dy = p1.y()-p2.y(); return dx*dx + dy*dy; } struct KoPathTool::PathSegment { PathSegment() : path(0), segmentStart(0), positionOnSegment(0) { } bool isValid() { return path && segmentStart; } KoPathShape *path; KoPathPoint *segmentStart; qreal positionOnSegment; }; KoPathTool::KoPathTool(KoCanvasBase *canvas) : KoToolBase(canvas) , m_pointSelection(this) , m_activeHandle(0) , m_handleRadius(3) , m_activeSegment(0) , m_currentStrategy(0) , m_activatedTemporarily(false) { m_points = new QActionGroup(this); // m_pointTypeGroup->setExclusive(true); m_actionPathPointCorner = action("pathpoint-corner"); if (m_actionPathPointCorner) { m_actionPathPointCorner->setData(KoPathPointTypeCommand::Corner); m_points->addAction(m_actionPathPointCorner); } m_actionPathPointSmooth = action("pathpoint-smooth"); if (m_actionPathPointSmooth) { m_actionPathPointSmooth->setData(KoPathPointTypeCommand::Smooth); m_points->addAction(m_actionPathPointSmooth); } m_actionPathPointSymmetric = action("pathpoint-symmetric"); if (m_actionPathPointSymmetric) { m_actionPathPointSymmetric->setData(KoPathPointTypeCommand::Symmetric); m_points->addAction(m_actionPathPointSymmetric); } m_actionCurvePoint = action("pathpoint-curve"); m_actionLinePoint = action("pathpoint-line"); m_actionLineSegment = action("pathsegment-line"); m_actionCurveSegment = action("pathsegment-curve"); m_actionAddPoint = action("pathpoint-insert"); m_actionRemovePoint = action("pathpoint-remove"); m_actionBreakPoint = action("path-break-point"); m_actionBreakSegment = action("path-break-segment"); m_actionJoinSegment = action("pathpoint-join"); m_actionMergePoints = action("pathpoint-merge"); m_actionConvertToPath = action("convert-to-path"); m_contextMenu.reset(new QMenu()); QBitmap b = QBitmap::fromData(QSize(16, 16), needle_bits); QBitmap m = b.createHeuristicMask(false); m_selectCursor = QCursor(b, m, 2, 0); b = QBitmap::fromData(QSize(16, 16), needle_move_bits); m = b.createHeuristicMask(false); m_moveCursor = QCursor(b, m, 2, 0); } KoPathTool::~KoPathTool() { delete m_activeHandle; delete m_activeSegment; delete m_currentStrategy; } QList > KoPathTool::createOptionWidgets() { QList > list; PathToolOptionWidget * toolOptions = new PathToolOptionWidget(this); connect(this, SIGNAL(typeChanged(int)), toolOptions, SLOT(setSelectionType(int))); connect(this, SIGNAL(singleShapeChanged(KoPathShape*)), toolOptions, SLOT(setCurrentShape(KoPathShape*))); connect(toolOptions, SIGNAL(sigRequestUpdateActions()), this, SLOT(updateActions())); updateOptionsWidget(); toolOptions->setWindowTitle(i18n("Edit Shape")); list.append(toolOptions); return list; } void KoPathTool::pointTypeChanged(QAction *type) { Q_D(KoToolBase); if (m_pointSelection.hasSelection()) { QList selectedPoints = m_pointSelection.selectedPointsData(); KUndo2Command *initialConversionCommand = createPointToCurveCommand(selectedPoints); // conversion should happen before the c-tor // of KoPathPointTypeCommand is executed! if (initialConversionCommand) { initialConversionCommand->redo(); } KUndo2Command *command = new KoPathPointTypeCommand(selectedPoints, static_cast(type->data().toInt())); if (initialConversionCommand) { using namespace KisCommandUtils; CompositeCommand *parent = new CompositeCommand(); parent->setText(command->text()); parent->addCommand(new SkipFirstRedoWrapper(initialConversionCommand)); parent->addCommand(command); command = parent; } d->canvas->addCommand(command); } } void KoPathTool::insertPoints() { Q_D(KoToolBase); QList segments(m_pointSelection.selectedSegmentsData()); if (segments.size() == 1) { qreal positionInSegment = 0.5; if (m_activeSegment && m_activeSegment->isValid()) { positionInSegment = m_activeSegment->positionOnSegment; } KoPathPointInsertCommand *cmd = new KoPathPointInsertCommand(segments, positionInSegment); d->canvas->addCommand(cmd); // TODO: this construction is dangerous. The canvas can remove the command right after // it has been added to it! m_pointSelection.clear(); foreach (KoPathPoint * p, cmd->insertedPoints()) { m_pointSelection.add(p, false); } } } void KoPathTool::removePoints() { Q_D(KoToolBase); if (m_pointSelection.size() > 0) { KUndo2Command *cmd = KoPathPointRemoveCommand::createCommand(m_pointSelection.selectedPointsData(), d->canvas->shapeController()); PointHandle *pointHandle = dynamic_cast(m_activeHandle); if (pointHandle && m_pointSelection.contains(pointHandle->activePoint())) { delete m_activeHandle; m_activeHandle = 0; } clearActivePointSelectionReferences(); d->canvas->addCommand(cmd); } } void KoPathTool::pointToLine() { Q_D(KoToolBase); if (m_pointSelection.hasSelection()) { QList selectedPoints = m_pointSelection.selectedPointsData(); QList pointToChange; QList::const_iterator it(selectedPoints.constBegin()); for (; it != selectedPoints.constEnd(); ++it) { KoPathPoint *point = it->pathShape->pointByIndex(it->pointIndex); if (point && (point->activeControlPoint1() || point->activeControlPoint2())) pointToChange.append(*it); } if (! pointToChange.isEmpty()) { d->canvas->addCommand(new KoPathPointTypeCommand(pointToChange, KoPathPointTypeCommand::Line)); } } } void KoPathTool::pointToCurve() { Q_D(KoToolBase); if (m_pointSelection.hasSelection()) { QList selectedPoints = m_pointSelection.selectedPointsData(); KUndo2Command *command = createPointToCurveCommand(selectedPoints); if (command) { d->canvas->addCommand(command); } } } KUndo2Command* KoPathTool::createPointToCurveCommand(const QList &points) { KUndo2Command *command = 0; QList pointToChange; QList::const_iterator it(points.constBegin()); for (; it != points.constEnd(); ++it) { KoPathPoint *point = it->pathShape->pointByIndex(it->pointIndex); if (point && (! point->activeControlPoint1() || ! point->activeControlPoint2())) pointToChange.append(*it); } if (!pointToChange.isEmpty()) { command = new KoPathPointTypeCommand(pointToChange, KoPathPointTypeCommand::Curve); } return command; } void KoPathTool::segmentToLine() { Q_D(KoToolBase); if (m_pointSelection.size() > 1) { QList segments(m_pointSelection.selectedSegmentsData()); if (segments.size() > 0) { d->canvas->addCommand(new KoPathSegmentTypeCommand(segments, KoPathSegmentTypeCommand::Line)); } } } void KoPathTool::segmentToCurve() { Q_D(KoToolBase); if (m_pointSelection.size() > 1) { QList segments(m_pointSelection.selectedSegmentsData()); if (segments.size() > 0) { d->canvas->addCommand(new KoPathSegmentTypeCommand(segments, KoPathSegmentTypeCommand::Curve)); } } } void KoPathTool::convertToPath() { Q_D(KoToolBase); KoSelection *selection = canvas()->selectedShapesProxy()->selection(); QList parameterShapes; Q_FOREACH (KoShape *shape, m_pointSelection.selectedShapes()) { KoParameterShape * parameteric = dynamic_cast(shape); if (parameteric && parameteric->isParametricShape()) { parameterShapes.append(parameteric); } } if (!parameterShapes.isEmpty()) { d->canvas->addCommand(new KoParameterToPathCommand(parameterShapes)); } QList textShapes; Q_FOREACH (KoShape *shape, selection->selectedEditableShapes()) { if (KoSvgTextShape *text = dynamic_cast(shape)) { textShapes.append(text); } } if (!textShapes.isEmpty()) { KUndo2Command *cmd = new KUndo2Command(kundo2_i18n("Convert to Path")); // TODO: reuse the text from KoParameterToPathCommand const QList oldSelectedShapes = implicitCastList(textShapes); new KoKeepShapesSelectedCommand(oldSelectedShapes, {}, canvas()->selectedShapesProxy(), KisCommandUtils::FlipFlopCommand::State::INITIALIZING, cmd); QList newSelectedShapes; Q_FOREACH (KoSvgTextShape *shape, textShapes) { const QPainterPath path = shape->textOutline(); if (path.isEmpty()) continue; KoPathShape *pathShape = KoPathShape::createShapeFromPainterPath(path); pathShape->setBackground(shape->background()); pathShape->setStroke(shape->stroke()); pathShape->setZIndex(shape->zIndex()); pathShape->setTransformation(shape->transformation()); KoShapeContainer *parent = shape->parent(); canvas()->shapeController()->addShapeDirect(pathShape, parent, cmd); newSelectedShapes << pathShape; } canvas()->shapeController()->removeShapes(oldSelectedShapes, cmd); new KoKeepShapesSelectedCommand({}, newSelectedShapes, canvas()->selectedShapesProxy(), KisCommandUtils::FlipFlopCommand::State::FINALIZING, cmd); canvas()->addCommand(cmd); } updateOptionsWidget(); } namespace { bool checkCanJoinToPoints(const KoPathPointData & pd1, const KoPathPointData & pd2) { const KoPathPointIndex & index1 = pd1.pointIndex; const KoPathPointIndex & index2 = pd2.pointIndex; KoPathShape *path1 = pd1.pathShape; KoPathShape *path2 = pd2.pathShape; // check if subpaths are already closed if (path1->isClosedSubpath(index1.first) || path2->isClosedSubpath(index2.first)) return false; // check if first point is an endpoint if (index1.second != 0 && index1.second != path1->subpathPointCount(index1.first)-1) return false; // check if second point is an endpoint if (index2.second != 0 && index2.second != path2->subpathPointCount(index2.first)-1) return false; return true; } } void KoPathTool::mergePointsImpl(bool doJoin) { Q_D(KoToolBase); if (m_pointSelection.size() != 2) return; QList pointData = m_pointSelection.selectedPointsData(); if (pointData.size() != 2) return; const KoPathPointData & pd1 = pointData.at(0); const KoPathPointData & pd2 = pointData.at(1); if (!checkCanJoinToPoints(pd1, pd2)) { return; } clearActivePointSelectionReferences(); KUndo2Command *cmd = 0; if (doJoin) { cmd = new KoMultiPathPointJoinCommand(pd1, pd2, d->canvas->shapeController()->documentBase(), d->canvas->shapeManager()->selection()); } else { cmd = new KoMultiPathPointMergeCommand(pd1, pd2, d->canvas->shapeController()->documentBase(), d->canvas->shapeManager()->selection()); } d->canvas->addCommand(cmd); } void KoPathTool::joinPoints() { mergePointsImpl(true); } void KoPathTool::mergePoints() { mergePointsImpl(false); } void KoPathTool::breakAtPoint() { Q_D(KoToolBase); if (m_pointSelection.hasSelection()) { d->canvas->addCommand(new KoPathBreakAtPointCommand(m_pointSelection.selectedPointsData())); } } void KoPathTool::breakAtSegment() { Q_D(KoToolBase); // only try to break a segment when 2 points of the same object are selected if (m_pointSelection.objectCount() == 1 && m_pointSelection.size() == 2) { QList segments(m_pointSelection.selectedSegmentsData()); if (segments.size() == 1) { d->canvas->addCommand(new KoPathSegmentBreakCommand(segments.at(0))); } } } void KoPathTool::paint(QPainter &painter, const KoViewConverter &converter) { Q_D(KoToolBase); Q_FOREACH (KoPathShape *shape, m_pointSelection.selectedShapes()) { KisHandlePainterHelper helper = KoShape::createHandlePainterHelper(&painter, shape, converter, m_handleRadius); helper.setHandleStyle(KisHandleStyle::primarySelection()); KoParameterShape * parameterShape = dynamic_cast(shape); if (parameterShape && parameterShape->isParametricShape()) { parameterShape->paintHandles(helper); } else { shape->paintPoints(helper); } if (!shape->stroke() || !shape->stroke()->isVisible()) { helper.setHandleStyle(KisHandleStyle::secondarySelection()); helper.drawPath(shape->outline()); } } if (m_currentStrategy) { painter.save(); m_currentStrategy->paint(painter, converter); painter.restore(); } m_pointSelection.paint(painter, converter, m_handleRadius); if (m_activeHandle) { if (m_activeHandle->check(m_pointSelection.selectedShapes())) { m_activeHandle->paint(painter, converter, m_handleRadius); } else { delete m_activeHandle; m_activeHandle = 0; } } else if (m_activeSegment && m_activeSegment->isValid()) { KoPathShape *shape = m_activeSegment->path; // if the stroke is invisible, then we already painted the outline of the shape! if (shape->stroke() && shape->stroke()->isVisible()) { KoPathPointIndex index = shape->pathPointIndex(m_activeSegment->segmentStart); KoPathSegment segment = shape->segmentByIndex(index).toCubic(); KIS_SAFE_ASSERT_RECOVER_RETURN(segment.isValid()); KisHandlePainterHelper helper = KoShape::createHandlePainterHelper(&painter, shape, converter, m_handleRadius); helper.setHandleStyle(KisHandleStyle::secondarySelection()); QPainterPath path; path.moveTo(segment.first()->point()); path.cubicTo(segment.first()->controlPoint2(), segment.second()->controlPoint1(), segment.second()->point()); helper.drawPath(path); } } if (m_currentStrategy) { painter.save(); KoShape::applyConversion(painter, converter); d->canvas->snapGuide()->paint(painter, converter); painter.restore(); } } void KoPathTool::repaintDecorations() { Q_FOREACH (KoShape *shape, m_pointSelection.selectedShapes()) { repaint(shape->boundingRect()); } m_pointSelection.repaint(); updateOptionsWidget(); } void KoPathTool::mousePressEvent(KoPointerEvent *event) { // we are moving if we hit a point and use the left mouse button event->ignore(); if (m_activeHandle) { m_currentStrategy = m_activeHandle->handleMousePress(event); event->accept(); } else { if (event->button() & Qt::LeftButton) { // check if we hit a path segment if (m_activeSegment && m_activeSegment->isValid()) { KoPathShape *shape = m_activeSegment->path; KoPathPointIndex index = shape->pathPointIndex(m_activeSegment->segmentStart); KoPathSegment segment = shape->segmentByIndex(index); m_pointSelection.add(segment.first(), !(event->modifiers() & Qt::ShiftModifier)); m_pointSelection.add(segment.second(), false); KoPathPointData data(shape, index); m_currentStrategy = new KoPathSegmentChangeStrategy(this, event->point, data, m_activeSegment->positionOnSegment); event->accept(); } else { KoShapeManager *shapeManager = canvas()->shapeManager(); KoSelection *selection = shapeManager->selection(); KoShape *shape = shapeManager->shapeAt(event->point, KoFlake::ShapeOnTop); if (shape && !selection->isSelected(shape)) { if (!(event->modifiers() & Qt::ShiftModifier)) { selection->deselectAll(); } selection->select(shape); } else { KIS_ASSERT_RECOVER_RETURN(m_currentStrategy == 0); m_currentStrategy = new KoPathPointRubberSelectStrategy(this, event->point); event->accept(); } } } } } void KoPathTool::mouseMoveEvent(KoPointerEvent *event) { if (event->button() & Qt::RightButton) return; if (m_currentStrategy) { m_lastPoint = event->point; m_currentStrategy->handleMouseMove(event->point, event->modifiers()); // repaint new handle positions m_pointSelection.repaint(); if (m_activeHandle) { m_activeHandle->repaint(); } if (m_activeSegment) { repaintSegment(m_activeSegment); } return; } if (m_activeSegment) { KoPathPointIndex index = m_activeSegment->path->pathPointIndex(m_activeSegment->segmentStart); KoPathSegment segment = m_activeSegment->path->segmentByIndex(index); repaint(segment.boundingRect()); delete m_activeSegment; m_activeSegment = 0; } Q_FOREACH (KoPathShape *shape, m_pointSelection.selectedShapes()) { QRectF roi = handleGrabRect(shape->documentToShape(event->point)); KoParameterShape * parameterShape = dynamic_cast(shape); if (parameterShape && parameterShape->isParametricShape()) { int handleId = parameterShape->handleIdAt(roi); if (handleId != -1) { useCursor(m_moveCursor); emit statusTextChanged(i18n("Drag to move handle.")); if (m_activeHandle) m_activeHandle->repaint(); delete m_activeHandle; if (KoConnectionShape * connectionShape = dynamic_cast(parameterShape)) { //debugFlake << "handleId" << handleId; m_activeHandle = new ConnectionHandle(this, connectionShape, handleId); m_activeHandle->repaint(); return; } else { //debugFlake << "handleId" << handleId; m_activeHandle = new ParameterHandle(this, parameterShape, handleId); m_activeHandle->repaint(); return; } } } else { QList points = shape->pointsAt(roi); if (! points.empty()) { // find the nearest control point from all points within the roi KoPathPoint * bestPoint = 0; KoPathPoint::PointType bestPointType = KoPathPoint::Node; qreal minDistance = HUGE_VAL; Q_FOREACH (KoPathPoint *p, points) { // the node point must be hit if the point is not selected yet if (! m_pointSelection.contains(p) && ! roi.contains(p->point())) continue; // check for the control points first as otherwise it is no longer // possible to change the control points when they are the same as the point if (p->activeControlPoint1() && roi.contains(p->controlPoint1())) { qreal dist = squaredDistance(roi.center(), p->controlPoint1()); if (dist < minDistance) { bestPoint = p; bestPointType = KoPathPoint::ControlPoint1; minDistance = dist; } } if (p->activeControlPoint2() && roi.contains(p->controlPoint2())) { qreal dist = squaredDistance(roi.center(), p->controlPoint2()); if (dist < minDistance) { bestPoint = p; bestPointType = KoPathPoint::ControlPoint2; minDistance = dist; } } // check the node point at last qreal dist = squaredDistance(roi.center(), p->point()); if (dist < minDistance) { bestPoint = p; bestPointType = KoPathPoint::Node; minDistance = dist; } } if (! bestPoint) return; useCursor(m_moveCursor); if (bestPointType == KoPathPoint::Node) emit statusTextChanged(i18n("Drag to move point. Shift click to change point type.")); else emit statusTextChanged(i18n("Drag to move control point.")); PointHandle *prev = dynamic_cast(m_activeHandle); if (prev && prev->activePoint() == bestPoint && prev->activePointType() == bestPointType) return; // no change; if (m_activeHandle) m_activeHandle->repaint(); delete m_activeHandle; m_activeHandle = new PointHandle(this, bestPoint, bestPointType); m_activeHandle->repaint(); return; } } } useCursor(m_selectCursor); if (m_activeHandle) { m_activeHandle->repaint(); } delete m_activeHandle; m_activeHandle = 0; PathSegment *hoveredSegment = segmentAtPoint(event->point); if(hoveredSegment) { useCursor(Qt::PointingHandCursor); emit statusTextChanged(i18n("Drag to change curve directly. Double click to insert new path point.")); m_activeSegment = hoveredSegment; repaintSegment(m_activeSegment); } else { uint selectedPointCount = m_pointSelection.size(); if (selectedPointCount == 0) emit statusTextChanged(QString()); else if (selectedPointCount == 1) emit statusTextChanged(i18n("Press B to break path at selected point.")); else emit statusTextChanged(i18n("Press B to break path at selected segments.")); } } void KoPathTool::repaintSegment(PathSegment *pathSegment) { if (!pathSegment || !pathSegment->isValid()) return; KoPathPointIndex index = pathSegment->path->pathPointIndex(pathSegment->segmentStart); KoPathSegment segment = pathSegment->path->segmentByIndex(index); repaint(segment.boundingRect()); } void KoPathTool::mouseReleaseEvent(KoPointerEvent *event) { Q_D(KoToolBase); if (m_currentStrategy) { const bool hadNoSelection = !m_pointSelection.hasSelection(); m_currentStrategy->finishInteraction(event->modifiers()); KUndo2Command *command = m_currentStrategy->createCommand(); if (command) d->canvas->addCommand(command); if (hadNoSelection && dynamic_cast(m_currentStrategy) && !m_pointSelection.hasSelection()) { // the click didn't do anything at all. Allow it to be used by others. event->ignore(); } delete m_currentStrategy; m_currentStrategy = 0; } } void KoPathTool::keyPressEvent(QKeyEvent *event) { if (m_currentStrategy) { switch (event->key()) { case Qt::Key_Control: case Qt::Key_Alt: case Qt::Key_Shift: case Qt::Key_Meta: if (! event->isAutoRepeat()) { m_currentStrategy->handleMouseMove(m_lastPoint, event->modifiers()); } break; case Qt::Key_Escape: m_currentStrategy->cancelInteraction(); delete m_currentStrategy; m_currentStrategy = 0; break; default: event->ignore(); return; } } else { switch (event->key()) { #ifndef NDEBUG case Qt::Key_D: if (m_pointSelection.objectCount() == 1) { QList selectedPoints = m_pointSelection.selectedPointsData(); KoPathShapePrivate *p = static_cast(selectedPoints[0].pathShape->priv()); p->debugPath(); } break; #endif case Qt::Key_B: if (m_pointSelection.size() == 1) breakAtPoint(); else if (m_pointSelection.size() >= 2) breakAtSegment(); break; default: event->ignore(); return; } } event->accept(); } void KoPathTool::keyReleaseEvent(QKeyEvent *event) { if (m_currentStrategy) { switch (event->key()) { case Qt::Key_Control: case Qt::Key_Alt: case Qt::Key_Shift: case Qt::Key_Meta: if (! event->isAutoRepeat()) { m_currentStrategy->handleMouseMove(m_lastPoint, Qt::NoModifier); } break; default: break; } } event->accept(); } void KoPathTool::mouseDoubleClickEvent(KoPointerEvent *event) { Q_D(KoToolBase); event->ignore(); // check if we are doing something else at the moment if (m_currentStrategy) return; if (!m_activeHandle && m_activeSegment && m_activeSegment->isValid()) { QList segments; segments.append( KoPathPointData(m_activeSegment->path, m_activeSegment->path->pathPointIndex(m_activeSegment->segmentStart))); KoPathPointInsertCommand *cmd = new KoPathPointInsertCommand(segments, m_activeSegment->positionOnSegment); d->canvas->addCommand(cmd); m_pointSelection.clear(); foreach (KoPathPoint * p, cmd->insertedPoints()) { m_pointSelection.add(p, false); } updateActions(); event->accept(); } else if (!m_activeHandle && !m_activeSegment && m_activatedTemporarily) { emit done(); event->accept(); } else if (!m_activeHandle && !m_activeSegment) { KoShapeManager *shapeManager = canvas()->shapeManager(); KoSelection *selection = shapeManager->selection(); selection->deselectAll(); event->accept(); } } KoPathTool::PathSegment* KoPathTool::segmentAtPoint(const QPointF &point) { // the max allowed distance from a segment const QRectF grabRoi = handleGrabRect(point); const qreal distanceThreshold = 0.5 * KisAlgebra2D::maxDimension(grabRoi); QScopedPointer segment(new PathSegment); Q_FOREACH (KoPathShape *shape, m_pointSelection.selectedShapes()) { KoParameterShape * parameterShape = dynamic_cast(shape); if (parameterShape && parameterShape->isParametricShape()) continue; // convert document point to shape coordinates const QPointF p = shape->documentToShape(point); // our region of interest, i.e. a region around our mouse position const QRectF roi = shape->documentToShape(grabRoi); qreal minDistance = std::numeric_limits::max(); // check all segments of this shape which intersect the region of interest const QList segments = shape->segmentsAt(roi); foreach (const KoPathSegment &s, segments) { const qreal nearestPointParam = s.nearestPoint(p); const QPointF nearestPoint = s.pointAt(nearestPointParam); const qreal distance = kisDistance(p, nearestPoint); // are we within the allowed distance ? if (distance > distanceThreshold) continue; // are we closer to the last closest point ? if (distance < minDistance) { segment->path = shape; segment->segmentStart = s.first(); segment->positionOnSegment = nearestPointParam; } } } if (!segment->isValid()) { segment.reset(); } return segment.take(); } void KoPathTool::activate(ToolActivation activation, const QSet &shapes) { KoToolBase::activate(activation, shapes); Q_D(KoToolBase); m_activatedTemporarily = activation == TemporaryActivation; // retrieve the actual global handle radius m_handleRadius = handleRadius(); d->canvas->snapGuide()->reset(); useCursor(m_selectCursor); m_canvasConnections.addConnection(d->canvas->selectedShapesProxy(), SIGNAL(selectionChanged()), this, SLOT(slotSelectionChanged())); m_canvasConnections.addConnection(d->canvas->selectedShapesProxy(), SIGNAL(selectionContentChanged()), this, SLOT(updateActions())); m_shapeFillResourceConnector.connectToCanvas(d->canvas); initializeWithShapes(shapes.toList()); connect(m_actionCurvePoint, SIGNAL(triggered()), this, SLOT(pointToCurve()), Qt::UniqueConnection); connect(m_actionLinePoint, SIGNAL(triggered()), this, SLOT(pointToLine()), Qt::UniqueConnection); connect(m_actionLineSegment, SIGNAL(triggered()), this, SLOT(segmentToLine()), Qt::UniqueConnection); connect(m_actionCurveSegment, SIGNAL(triggered()), this, SLOT(segmentToCurve()), Qt::UniqueConnection); connect(m_actionAddPoint, SIGNAL(triggered()), this, SLOT(insertPoints()), Qt::UniqueConnection); connect(m_actionRemovePoint, SIGNAL(triggered()), this, SLOT(removePoints()), Qt::UniqueConnection); connect(m_actionBreakPoint, SIGNAL(triggered()), this, SLOT(breakAtPoint()), Qt::UniqueConnection); connect(m_actionBreakSegment, SIGNAL(triggered()), this, SLOT(breakAtSegment()), Qt::UniqueConnection); connect(m_actionJoinSegment, SIGNAL(triggered()), this, SLOT(joinPoints()), Qt::UniqueConnection); connect(m_actionMergePoints, SIGNAL(triggered()), this, SLOT(mergePoints()), Qt::UniqueConnection); connect(m_actionConvertToPath, SIGNAL(triggered()), this, SLOT(convertToPath()), Qt::UniqueConnection); connect(m_points, SIGNAL(triggered(QAction*)), this, SLOT(pointTypeChanged(QAction*)), Qt::UniqueConnection); connect(&m_pointSelection, SIGNAL(selectionChanged()), this, SLOT(pointSelectionChanged()), Qt::UniqueConnection); } void KoPathTool::slotSelectionChanged() { Q_D(KoToolBase); QList shapes = d->canvas->selectedShapesProxy()->selection()->selectedEditableShapesAndDelegates(); initializeWithShapes(shapes); } void KoPathTool::notifyPathPointsChanged(KoPathShape *shape) { Q_UNUSED(shape); // active handle and selection might have already become invalid, so just // delete them without dereferencing anything... delete m_activeHandle; m_activeHandle = 0; delete m_activeSegment; m_activeSegment = 0; } void KoPathTool::clearActivePointSelectionReferences() { delete m_activeHandle; m_activeHandle = 0; delete m_activeSegment; m_activeSegment = 0; m_pointSelection.clear(); } void KoPathTool::initializeWithShapes(const QList shapes) { QList selectedShapes; Q_FOREACH (KoShape *shape, shapes) { KoPathShape *pathShape = dynamic_cast(shape); if (pathShape && pathShape->isShapeEditable()) { selectedShapes.append(pathShape); } } const QRectF oldBoundingRect = KoShape::boundingRect(implicitCastList(m_pointSelection.selectedShapes())); if (selectedShapes != m_pointSelection.selectedShapes()) { clearActivePointSelectionReferences(); m_pointSelection.setSelectedShapes(selectedShapes); repaintDecorations(); } Q_FOREACH (KoPathShape *shape, selectedShapes) { // as the tool is just in activation repaintDecorations does not yet get called // so we need to use repaint of the tool and it is only needed to repaint the // current canvas repaint(shape->boundingRect()); } repaint(oldBoundingRect); updateOptionsWidget(); updateActions(); } void KoPathTool::updateOptionsWidget() { PathToolOptionWidget::Types type; QList selectedShapes = m_pointSelection.selectedShapes(); Q_FOREACH (KoPathShape *shape, selectedShapes) { KoParameterShape * parameterShape = dynamic_cast(shape); type |= parameterShape && parameterShape->isParametricShape() ? PathToolOptionWidget::ParametricShape : PathToolOptionWidget::PlainPath; } emit singleShapeChanged(selectedShapes.size() == 1 ? selectedShapes.first() : 0); emit typeChanged(type); } void KoPathTool::updateActions() { QList pointData = m_pointSelection.selectedPointsData(); bool canBreakAtPoint = false; bool hasNonSmoothPoints = false; bool hasNonSymmetricPoints = false; bool hasNonSplitPoints = false; bool hasNonLinePoints = false; bool hasNonCurvePoints = false; bool canJoinSubpaths = false; if (!pointData.isEmpty()) { Q_FOREACH (const KoPathPointData &pd, pointData) { const int subpathIndex = pd.pointIndex.first; const int pointIndex = pd.pointIndex.second; canBreakAtPoint |= pd.pathShape->isClosedSubpath(subpathIndex) || (pointIndex > 0 && pointIndex < pd.pathShape->subpathPointCount(subpathIndex) - 1); KoPathPoint *point = pd.pathShape->pointByIndex(pd.pointIndex); hasNonSmoothPoints |= !(point->properties() & KoPathPoint::IsSmooth); hasNonSymmetricPoints |= !(point->properties() & KoPathPoint::IsSymmetric); hasNonSplitPoints |= point->properties() & KoPathPoint::IsSymmetric || point->properties() & KoPathPoint::IsSmooth; hasNonLinePoints |= point->activeControlPoint1() || point->activeControlPoint2(); hasNonCurvePoints |= !point->activeControlPoint1() && !point->activeControlPoint2(); } if (pointData.size() == 2) { const KoPathPointData & pd1 = pointData.at(0); const KoPathPointData & pd2 = pointData.at(1); canJoinSubpaths = checkCanJoinToPoints(pd1, pd2); } } m_actionPathPointCorner->setEnabled(hasNonSplitPoints); m_actionPathPointSmooth->setEnabled(hasNonSmoothPoints); m_actionPathPointSymmetric->setEnabled(hasNonSymmetricPoints); m_actionRemovePoint->setEnabled(!pointData.isEmpty()); m_actionBreakPoint->setEnabled(canBreakAtPoint); m_actionCurvePoint->setEnabled(hasNonCurvePoints); m_actionLinePoint->setEnabled(hasNonLinePoints); m_actionJoinSegment->setEnabled(canJoinSubpaths); m_actionMergePoints->setEnabled(canJoinSubpaths); QList segments(m_pointSelection.selectedSegmentsData()); bool canSplitAtSegment = false; bool canConvertSegmentToLine = false; bool canConvertSegmentToCurve= false; if (!segments.isEmpty()) { canSplitAtSegment = segments.size() == 1; bool hasLines = false; bool hasCurves = false; Q_FOREACH (const KoPathPointData &pd, segments) { KoPathSegment segment = pd.pathShape->segmentByIndex(pd.pointIndex); hasLines |= segment.degree() == 1; hasCurves |= segment.degree() > 1; } canConvertSegmentToLine = !segments.isEmpty() && hasCurves; canConvertSegmentToCurve= !segments.isEmpty() && hasLines; } m_actionAddPoint->setEnabled(canSplitAtSegment); m_actionLineSegment->setEnabled(canConvertSegmentToLine); m_actionCurveSegment->setEnabled(canConvertSegmentToCurve); m_actionBreakSegment->setEnabled(canSplitAtSegment); KoSelection *selection = canvas()->selectedShapesProxy()->selection(); bool haveConvertibleShapes = false; Q_FOREACH (KoShape *shape, selection->selectedEditableShapes()) { KoParameterShape * parameterShape = dynamic_cast(shape); KoSvgTextShape *textShape = dynamic_cast(shape); if (textShape || (parameterShape && parameterShape->isParametricShape())) { haveConvertibleShapes = true; break; } } m_actionConvertToPath->setEnabled(haveConvertibleShapes); } void KoPathTool::deactivate() { Q_D(KoToolBase); m_shapeFillResourceConnector.disconnect(); m_canvasConnections.clear(); m_pointSelection.clear(); m_pointSelection.setSelectedShapes(QList()); delete m_activeHandle; m_activeHandle = 0; delete m_activeSegment; m_activeSegment = 0; delete m_currentStrategy; m_currentStrategy = 0; d->canvas->snapGuide()->reset(); disconnect(m_actionCurvePoint, 0, this, 0); disconnect(m_actionLinePoint, 0, this, 0); disconnect(m_actionLineSegment, 0, this, 0); disconnect(m_actionCurveSegment, 0, this, 0); disconnect(m_actionAddPoint, 0, this, 0); disconnect(m_actionRemovePoint, 0, this, 0); disconnect(m_actionBreakPoint, 0, this, 0); disconnect(m_actionBreakSegment, 0, this, 0); disconnect(m_actionJoinSegment, 0, this, 0); disconnect(m_actionMergePoints, 0, this, 0); disconnect(m_actionConvertToPath, 0, this, 0); disconnect(m_points, 0, this, 0); disconnect(&m_pointSelection, 0, this, 0); KoToolBase::deactivate(); } void KoPathTool::documentResourceChanged(int key, const QVariant & res) { if (key == KoDocumentResourceManager::HandleRadius) { int oldHandleRadius = m_handleRadius; m_handleRadius = res.toUInt(); // repaint with the bigger of old and new handle radius int maxRadius = qMax(m_handleRadius, oldHandleRadius); Q_FOREACH (KoPathShape *shape, m_pointSelection.selectedShapes()) { QRectF controlPointRect = shape->absoluteTransformation(0).map(shape->outline()).controlPointRect(); repaint(controlPointRect.adjusted(-maxRadius, -maxRadius, maxRadius, maxRadius)); } } } void KoPathTool::pointSelectionChanged() { Q_D(KoToolBase); updateActions(); d->canvas->snapGuide()->setIgnoredPathPoints(m_pointSelection.selectedPoints().toList()); emit selectionChanged(m_pointSelection.hasSelection()); } void KoPathTool::repaint(const QRectF &repaintRect) { Q_D(KoToolBase); //debugFlake <<"KoPathTool::repaint(" << repaintRect <<")" << m_handleRadius; // widen border to take antialiasing into account qreal radius = m_handleRadius + 1; d->canvas->updateCanvas(repaintRect.adjusted(-radius, -radius, radius, radius)); } namespace { void addActionsGroupIfEnabled(QMenu *menu, QAction *a1, QAction *a2) { if (a1->isEnabled() || a2->isEnabled()) { menu->addAction(a1); menu->addAction(a2); menu->addSeparator(); } } void addActionsGroupIfEnabled(QMenu *menu, QAction *a1, QAction *a2, QAction *a3) { if (a1->isEnabled() || a2->isEnabled()) { menu->addAction(a1); menu->addAction(a2); menu->addAction(a3); menu->addSeparator(); } } } QMenu *KoPathTool::popupActionsMenu() { if (m_activeHandle) { m_activeHandle->trySelectHandle(); } if (m_activeSegment && m_activeSegment->isValid()) { KoPathShape *shape = m_activeSegment->path; KoPathSegment segment = shape->segmentByIndex(shape->pathPointIndex(m_activeSegment->segmentStart)); m_pointSelection.add(segment.first(), true); m_pointSelection.add(segment.second(), false); } if (m_contextMenu) { m_contextMenu->clear(); addActionsGroupIfEnabled(m_contextMenu.data(), m_actionPathPointCorner, m_actionPathPointSmooth, m_actionPathPointSymmetric); addActionsGroupIfEnabled(m_contextMenu.data(), m_actionCurvePoint, m_actionLinePoint); addActionsGroupIfEnabled(m_contextMenu.data(), m_actionAddPoint, m_actionRemovePoint); addActionsGroupIfEnabled(m_contextMenu.data(), m_actionLineSegment, m_actionCurveSegment); addActionsGroupIfEnabled(m_contextMenu.data(), m_actionBreakPoint, m_actionBreakSegment); addActionsGroupIfEnabled(m_contextMenu.data(), m_actionJoinSegment, m_actionMergePoints); m_contextMenu->addAction(m_actionConvertToPath); m_contextMenu->addSeparator(); } return m_contextMenu.data(); } void KoPathTool::deleteSelection() { removePoints(); } KoToolSelection * KoPathTool::selection() { return &m_pointSelection; } void KoPathTool::requestUndoDuringStroke() { // noop! } void KoPathTool::requestStrokeCancellation() { explicitUserStrokeEndRequest(); } void KoPathTool::requestStrokeEnd() { // noop! } void KoPathTool::explicitUserStrokeEndRequest() { if (m_activatedTemporarily) { emit done(); } } diff --git a/libs/global/KisHandlePainterHelper.cpp b/libs/global/KisHandlePainterHelper.cpp index 7d6662977e..24b8137186 100644 --- a/libs/global/KisHandlePainterHelper.cpp +++ b/libs/global/KisHandlePainterHelper.cpp @@ -1,329 +1,330 @@ /* * 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. */ #include "KisHandlePainterHelper.h" #include +#include #include "kis_algebra_2d.h" #include "kis_painting_tweaks.h" using KisPaintingTweaks::PenBrushSaver; KisHandlePainterHelper::KisHandlePainterHelper(QPainter *_painter, qreal handleRadius) : m_painter(_painter), m_originalPainterTransform(m_painter->transform()), m_painterTransform(m_painter->transform()), m_handleRadius(handleRadius), m_decomposedMatrix(m_painterTransform) { init(); } KisHandlePainterHelper::KisHandlePainterHelper(QPainter *_painter, const QTransform &originalPainterTransform, qreal handleRadius) : m_painter(_painter), m_originalPainterTransform(originalPainterTransform), m_painterTransform(m_painter->transform()), m_handleRadius(handleRadius), m_decomposedMatrix(m_painterTransform) { init(); } KisHandlePainterHelper::KisHandlePainterHelper(KisHandlePainterHelper &&rhs) : m_painter(rhs.m_painter), m_originalPainterTransform(rhs.m_originalPainterTransform), m_painterTransform(rhs.m_painterTransform), m_handleRadius(rhs.m_handleRadius), m_decomposedMatrix(rhs.m_decomposedMatrix), m_handleTransform(rhs.m_handleTransform), m_handlePolygon(rhs.m_handlePolygon), m_handleStyle(rhs.m_handleStyle) { // disable the source helper rhs.m_painter = 0; } void KisHandlePainterHelper::init() { m_handleStyle = KisHandleStyle::inheritStyle(); m_painter->setTransform(QTransform()); m_handleTransform = m_decomposedMatrix.shearTransform() * m_decomposedMatrix.rotateTransform(); if (m_handleRadius > 0.0) { const QRectF handleRect(-m_handleRadius, -m_handleRadius, 2 * m_handleRadius, 2 * m_handleRadius); m_handlePolygon = m_handleTransform.map(QPolygonF(handleRect)); } } KisHandlePainterHelper::~KisHandlePainterHelper() { if (m_painter) { m_painter->setTransform(m_originalPainterTransform); } } void KisHandlePainterHelper::setHandleStyle(const KisHandleStyle &style) { m_handleStyle = style; } void KisHandlePainterHelper::drawHandleRect(const QPointF ¢er, qreal radius, QPoint offset = QPoint(0,0)) { KIS_SAFE_ASSERT_RECOVER_RETURN(m_painter); QRectF handleRect(-radius, -radius, 2 * radius, 2 * radius); QPolygonF handlePolygon = m_handleTransform.map(QPolygonF(handleRect)); handlePolygon.translate(m_painterTransform.map(center)); handlePolygon.translate(offset); const QPen originalPen = m_painter->pen(); // temporarily set the pen width to 2 to avoid pixel shifting dropping pixels the border QPen *tempPen = new QPen(m_painter->pen()); tempPen->setWidth(4); const QPen customPen = *tempPen; m_painter->setPen(customPen); Q_FOREACH (KisHandleStyle::IterationStyle it, m_handleStyle.handleIterations) { PenBrushSaver saver(it.isValid ? m_painter : 0, it.stylePair, PenBrushSaver::allow_noop); m_painter->drawPolygon(handlePolygon); } m_painter->setPen(originalPen); } void KisHandlePainterHelper::drawHandleCircle(const QPointF ¢er, qreal radius) { KIS_SAFE_ASSERT_RECOVER_RETURN(m_painter); QRectF handleRect(-radius, -radius, 2 * radius, 2 * radius); handleRect.translate(m_painterTransform.map(center)); Q_FOREACH (KisHandleStyle::IterationStyle it, m_handleStyle.handleIterations) { PenBrushSaver saver(it.isValid ? m_painter : 0, it.stylePair, PenBrushSaver::allow_noop); m_painter->drawEllipse(handleRect); } } void KisHandlePainterHelper::drawHandleCircle(const QPointF ¢er) { drawHandleCircle(center, m_handleRadius); } void KisHandlePainterHelper::drawHandleSmallCircle(const QPointF ¢er) { drawHandleCircle(center, 0.7 * m_handleRadius); } void KisHandlePainterHelper::drawHandleRect(const QPointF ¢er) { KIS_SAFE_ASSERT_RECOVER_RETURN(m_painter); QPolygonF paintingPolygon = m_handlePolygon.translated(m_painterTransform.map(center)); Q_FOREACH (KisHandleStyle::IterationStyle it, m_handleStyle.handleIterations) { PenBrushSaver saver(it.isValid ? m_painter : 0, it.stylePair, PenBrushSaver::allow_noop); m_painter->drawPolygon(paintingPolygon); } } void KisHandlePainterHelper::drawGradientHandle(const QPointF ¢er, qreal radius) { KIS_SAFE_ASSERT_RECOVER_RETURN(m_painter); QPolygonF handlePolygon; handlePolygon << QPointF(-radius, 0); handlePolygon << QPointF(0, radius); handlePolygon << QPointF(radius, 0); handlePolygon << QPointF(0, -radius); handlePolygon = m_handleTransform.map(handlePolygon); handlePolygon.translate(m_painterTransform.map(center)); Q_FOREACH (KisHandleStyle::IterationStyle it, m_handleStyle.handleIterations) { PenBrushSaver saver(it.isValid ? m_painter : 0, it.stylePair, PenBrushSaver::allow_noop); m_painter->drawPolygon(handlePolygon); } } void KisHandlePainterHelper::drawGradientHandle(const QPointF ¢er) { drawGradientHandle(center, 1.41 * m_handleRadius); } void KisHandlePainterHelper::drawGradientCrossHandle(const QPointF ¢er, qreal radius) { KIS_SAFE_ASSERT_RECOVER_RETURN(m_painter); { // Draw a cross QPainterPath p; p.moveTo(-radius, -radius); p.lineTo(radius, radius); p.moveTo(radius, -radius); p.lineTo(-radius, radius); p = m_handleTransform.map(p); p.translate(m_painterTransform.map(center)); Q_FOREACH (KisHandleStyle::IterationStyle it, m_handleStyle.handleIterations) { PenBrushSaver saver(it.isValid ? m_painter : 0, it.stylePair, PenBrushSaver::allow_noop); m_painter->drawPath(p); } } { // Draw a square const qreal halfRadius = 0.5 * radius; QPolygonF handlePolygon; handlePolygon << QPointF(-halfRadius, 0); handlePolygon << QPointF(0, halfRadius); handlePolygon << QPointF(halfRadius, 0); handlePolygon << QPointF(0, -halfRadius); handlePolygon = m_handleTransform.map(handlePolygon); handlePolygon.translate(m_painterTransform.map(center)); Q_FOREACH (KisHandleStyle::IterationStyle it, m_handleStyle.handleIterations) { PenBrushSaver saver(it.isValid ? m_painter : 0, it.stylePair, PenBrushSaver::allow_noop); m_painter->drawPolygon(handlePolygon); } } } void KisHandlePainterHelper::drawArrow(const QPointF &pos, const QPointF &from, qreal radius) { KIS_SAFE_ASSERT_RECOVER_RETURN(m_painter); QPainterPath p; QLineF line(pos, from); line.setLength(radius); QPointF norm = KisAlgebra2D::leftUnitNormal(pos - from); norm *= 0.34 * radius; p.moveTo(line.p2() + norm); p.lineTo(line.p1()); p.lineTo(line.p2() - norm); p.translate(-pos); p = m_handleTransform.map(p).translated(m_painterTransform.map(pos)); Q_FOREACH (KisHandleStyle::IterationStyle it, m_handleStyle.handleIterations) { PenBrushSaver saver(it.isValid ? m_painter : 0, it.stylePair, PenBrushSaver::allow_noop); m_painter->drawPath(p); } } void KisHandlePainterHelper::drawGradientArrow(const QPointF &start, const QPointF &end, qreal radius) { KIS_SAFE_ASSERT_RECOVER_RETURN(m_painter); QPainterPath p; p.moveTo(start); p.lineTo(end); p = m_painterTransform.map(p); Q_FOREACH (KisHandleStyle::IterationStyle it, m_handleStyle.lineIterations) { PenBrushSaver saver(it.isValid ? m_painter : 0, it.stylePair, PenBrushSaver::allow_noop); m_painter->drawPath(p); } const qreal length = kisDistance(start, end); const QPointF diff = end - start; if (length > 5 * radius) { drawArrow(start + 0.33 * diff, start, radius); drawArrow(start + 0.66 * diff, start, radius); } else if (length > 3 * radius) { drawArrow(start + 0.5 * diff, start, radius); } } void KisHandlePainterHelper::drawRubberLine(const QPolygonF &poly) { KIS_SAFE_ASSERT_RECOVER_RETURN(m_painter); QPolygonF paintingPolygon = m_painterTransform.map(poly); Q_FOREACH (KisHandleStyle::IterationStyle it, m_handleStyle.lineIterations) { PenBrushSaver saver(it.isValid ? m_painter : 0, it.stylePair, PenBrushSaver::allow_noop); m_painter->drawPolygon(paintingPolygon); } } void KisHandlePainterHelper::drawConnectionLine(const QLineF &line) { drawConnectionLine(line.p1(), line.p2()); } void KisHandlePainterHelper::drawConnectionLine(const QPointF &p1, const QPointF &p2) { KIS_SAFE_ASSERT_RECOVER_RETURN(m_painter); QPointF realP1 = m_painterTransform.map(p1); QPointF realP2 = m_painterTransform.map(p2); Q_FOREACH (KisHandleStyle::IterationStyle it, m_handleStyle.lineIterations) { PenBrushSaver saver(it.isValid ? m_painter : 0, it.stylePair, PenBrushSaver::allow_noop); m_painter->drawLine(realP1, realP2); } } void KisHandlePainterHelper::drawPath(const QPainterPath &path) { const QPainterPath realPath = m_painterTransform.map(path); Q_FOREACH (KisHandleStyle::IterationStyle it, m_handleStyle.lineIterations) { PenBrushSaver saver(it.isValid ? m_painter : 0, it.stylePair, PenBrushSaver::allow_noop); m_painter->drawPath(realPath); } } void KisHandlePainterHelper::drawPixmap(const QPixmap &pixmap, QPointF position, int size, QRectF sourceRect) { QPointF handlePolygon = m_painterTransform.map(position); QPoint offsetPosition(0, 40); handlePolygon += offsetPosition; handlePolygon -= QPointF(size*0.5,size*0.5); m_painter->drawPixmap(QRect(handlePolygon.x(), handlePolygon.y(), size, size), pixmap, sourceRect); } void KisHandlePainterHelper::fillHandleRect(const QPointF ¢er, qreal radius, QColor fillColor, QPoint offset = QPoint(0,0)) { KIS_SAFE_ASSERT_RECOVER_RETURN(m_painter); QRectF handleRect(-radius, -radius, 2 * radius, 2 * radius); QPolygonF handlePolygon = m_handleTransform.map(QPolygonF(handleRect)); handlePolygon.translate(m_painterTransform.map(center)); QPainterPath painterPath; painterPath.addPolygon(handlePolygon); // offset that happens after zoom transform. This means the offset will be the same, no matter the zoom level // this is good for UI elements that need to be below the bounding box painterPath.translate(offset); const QPainterPath pathToSend = painterPath; const QBrush brushStyle(fillColor); m_painter->fillPath(pathToSend, brushStyle); } diff --git a/libs/image/brushengine/kis_paintop_settings.cpp b/libs/image/brushengine/kis_paintop_settings.cpp index 87f5c53a25..0e43fc9eeb 100644 --- a/libs/image/brushengine/kis_paintop_settings.cpp +++ b/libs/image/brushengine/kis_paintop_settings.cpp @@ -1,513 +1,514 @@ /* * Copyright (c) 2007 Boudewijn Rempt * Copyright (c) 2008 Lukáš Tvrdý * Copyright (c) 2014 Mohit Goyal * 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 #include #include +#include #include #include #include #include #include #include "kis_paint_layer.h" #include "kis_image.h" #include "kis_painter.h" #include "kis_paint_device.h" #include "kis_paintop_registry.h" #include "kis_timing_information.h" #include #include "kis_paintop_config_widget.h" #include #include "kis_paintop_settings_update_proxy.h" #include #include #include #include #include #include "KisPaintopSettingsIds.h" #include "kis_algebra_2d.h" struct Q_DECL_HIDDEN KisPaintOpSettings::Private { Private() : disableDirtyNotifications(false) {} QPointer settingsWidget; QString modelName; KisPaintOpPresetWSP preset; QList uniformProperties; bool disableDirtyNotifications; class DirtyNotificationsLocker { public: DirtyNotificationsLocker(KisPaintOpSettings::Private *d) : m_d(d), m_oldNotificationsState(d->disableDirtyNotifications) { m_d->disableDirtyNotifications = true; } ~DirtyNotificationsLocker() { m_d->disableDirtyNotifications = m_oldNotificationsState; } private: KisPaintOpSettings::Private *m_d; bool m_oldNotificationsState; Q_DISABLE_COPY(DirtyNotificationsLocker) }; KisPaintopSettingsUpdateProxy* updateProxyNoCreate() const { auto presetSP = preset.toStrongRef(); return presetSP ? presetSP->updateProxyNoCreate() : 0; } KisPaintopSettingsUpdateProxy* updateProxyCreate() const { auto presetSP = preset.toStrongRef(); return presetSP ? presetSP->updateProxy() : 0; } }; KisPaintOpSettings::KisPaintOpSettings() : d(new Private) { d->preset = 0; } KisPaintOpSettings::~KisPaintOpSettings() { } KisPaintOpSettings::KisPaintOpSettings(const KisPaintOpSettings &rhs) : KisPropertiesConfiguration(rhs) , d(new Private) { d->settingsWidget = 0; d->preset = rhs.preset(); d->modelName = rhs.modelName(); } void KisPaintOpSettings::setOptionsWidget(KisPaintOpConfigWidget* widget) { d->settingsWidget = widget; } void KisPaintOpSettings::setPreset(KisPaintOpPresetWSP preset) { d->preset = preset; } KisPaintOpPresetWSP KisPaintOpSettings::preset() const { return d->preset; } bool KisPaintOpSettings::mousePressEvent(const KisPaintInformation &paintInformation, Qt::KeyboardModifiers modifiers, KisNodeWSP currentNode) { Q_UNUSED(modifiers); Q_UNUSED(currentNode); setRandomOffset(paintInformation); return true; // ignore the event by default } bool KisPaintOpSettings::mouseReleaseEvent() { return true; // ignore the event by default } void KisPaintOpSettings::setRandomOffset(const KisPaintInformation &paintInformation) { bool disableDirtyBefore = d->disableDirtyNotifications; d->disableDirtyNotifications = true; if (getBool("Texture/Pattern/Enabled")) { if (getBool("Texture/Pattern/isRandomOffsetX")) { setProperty("Texture/Pattern/OffsetX", paintInformation.randomSource()->generate(0, KisPropertiesConfiguration::getInt("Texture/Pattern/MaximumOffsetX"))); } if (getBool("Texture/Pattern/isRandomOffsetY")) { setProperty("Texture/Pattern/OffsetY", paintInformation.randomSource()->generate(0, KisPropertiesConfiguration::getInt("Texture/Pattern/MaximumOffsetY"))); } } d->disableDirtyNotifications = disableDirtyBefore; } bool KisPaintOpSettings::hasMaskingSettings() const { return getBool(KisPaintOpUtils::MaskingBrushEnabledTag, false); } KisPaintOpSettingsSP KisPaintOpSettings::createMaskingSettings() const { if (!hasMaskingSettings()) return KisPaintOpSettingsSP(); const KoID pixelBrushId(KisPaintOpUtils::MaskingBrushPaintOpId, QString()); KisPaintOpSettingsSP maskingSettings = KisPaintOpRegistry::instance()->settings(pixelBrushId); this->getPrefixedProperties(KisPaintOpUtils::MaskingBrushPresetPrefix, maskingSettings); const bool useMasterSize = this->getBool(KisPaintOpUtils::MaskingBrushUseMasterSizeTag, true); if (useMasterSize) { const qreal masterSizeCoeff = getDouble(KisPaintOpUtils::MaskingBrushMasterSizeCoeffTag, 1.0); maskingSettings->setPaintOpSize(masterSizeCoeff * paintOpSize()); } return maskingSettings; } QString KisPaintOpSettings::maskingBrushCompositeOp() const { return getString(KisPaintOpUtils::MaskingBrushCompositeOpTag, COMPOSITE_MULT); } KisPaintOpSettingsSP KisPaintOpSettings::clone() const { QString paintopID = getString("paintop"); if (paintopID.isEmpty()) return 0; KisPaintOpSettingsSP settings = KisPaintOpRegistry::instance()->settings(KoID(paintopID)); QMapIterator i(getProperties()); while (i.hasNext()) { i.next(); settings->setProperty(i.key(), QVariant(i.value())); } settings->setPreset(this->preset()); return settings; } void KisPaintOpSettings::resetSettings(const QStringList &preserveProperties) { QStringList allKeys = preserveProperties; allKeys << "paintop"; QHash preserved; Q_FOREACH (const QString &key, allKeys) { if (hasProperty(key)) { preserved[key] = getProperty(key); } } clearProperties(); for (auto it = preserved.constBegin(); it != preserved.constEnd(); ++it) { setProperty(it.key(), it.value()); } } void KisPaintOpSettings::activate() { } void KisPaintOpSettings::setPaintOpOpacity(qreal value) { KisLockedPropertiesProxySP proxy( KisLockedPropertiesServer::instance()->createLockedPropertiesProxy(this)); proxy->setProperty("OpacityValue", value); } void KisPaintOpSettings::setPaintOpFlow(qreal value) { KisLockedPropertiesProxySP proxy( KisLockedPropertiesServer::instance()->createLockedPropertiesProxy(this)); proxy->setProperty("FlowValue", value); } void KisPaintOpSettings::setPaintOpCompositeOp(const QString &value) { KisLockedPropertiesProxySP proxy( KisLockedPropertiesServer::instance()->createLockedPropertiesProxy(this)); proxy->setProperty("CompositeOp", value); } qreal KisPaintOpSettings::paintOpOpacity() { KisLockedPropertiesProxySP proxy = KisLockedPropertiesServer::instance()->createLockedPropertiesProxy(this); return proxy->getDouble("OpacityValue", 1.0); } qreal KisPaintOpSettings::paintOpFlow() { KisLockedPropertiesProxySP proxy( KisLockedPropertiesServer::instance()->createLockedPropertiesProxy(this)); return proxy->getDouble("FlowValue", 1.0); } QString KisPaintOpSettings::paintOpCompositeOp() { KisLockedPropertiesProxySP proxy( KisLockedPropertiesServer::instance()->createLockedPropertiesProxy(this)); return proxy->getString("CompositeOp", COMPOSITE_OVER); } void KisPaintOpSettings::setEraserMode(bool value) { KisLockedPropertiesProxySP proxy( KisLockedPropertiesServer::instance()->createLockedPropertiesProxy(this)); proxy->setProperty("EraserMode", value); } bool KisPaintOpSettings::eraserMode() { KisLockedPropertiesProxySP proxy( KisLockedPropertiesServer::instance()->createLockedPropertiesProxy(this)); return proxy->getBool("EraserMode", false); } QString KisPaintOpSettings::effectivePaintOpCompositeOp() { return !eraserMode() ? paintOpCompositeOp() : COMPOSITE_ERASE; } qreal KisPaintOpSettings::savedEraserSize() const { return getDouble("SavedEraserSize", 0.0); } void KisPaintOpSettings::setSavedEraserSize(qreal value) { setProperty("SavedEraserSize", value); setPropertyNotSaved("SavedEraserSize"); } qreal KisPaintOpSettings::savedBrushSize() const { return getDouble("SavedBrushSize", 0.0); } void KisPaintOpSettings::setSavedBrushSize(qreal value) { setProperty("SavedBrushSize", value); setPropertyNotSaved("SavedBrushSize"); } qreal KisPaintOpSettings::savedEraserOpacity() const { return getDouble("SavedEraserOpacity", 0.0); } void KisPaintOpSettings::setSavedEraserOpacity(qreal value) { setProperty("SavedEraserOpacity", value); setPropertyNotSaved("SavedEraserOpacity"); } qreal KisPaintOpSettings::savedBrushOpacity() const { return getDouble("SavedBrushOpacity", 0.0); } void KisPaintOpSettings::setSavedBrushOpacity(qreal value) { setProperty("SavedBrushOpacity", value); setPropertyNotSaved("SavedBrushOpacity"); } QString KisPaintOpSettings::modelName() const { return d->modelName; } void KisPaintOpSettings::setModelName(const QString & modelName) { d->modelName = modelName; } KisPaintOpConfigWidget* KisPaintOpSettings::optionsWidget() const { if (d->settingsWidget.isNull()) return 0; return d->settingsWidget.data(); } bool KisPaintOpSettings::isValid() const { return true; } bool KisPaintOpSettings::isLoadable() { return isValid(); } QString KisPaintOpSettings::indirectPaintingCompositeOp() const { return COMPOSITE_ALPHA_DARKEN; } bool KisPaintOpSettings::isAirbrushing() const { return getBool(AIRBRUSH_ENABLED, false); } qreal KisPaintOpSettings::airbrushInterval() const { qreal rate = getDouble(AIRBRUSH_RATE, 1.0); if (rate == 0.0) { return LONG_TIME; } else { return 1000.0 / rate; } } bool KisPaintOpSettings::useSpacingUpdates() const { return getBool(SPACING_USE_UPDATES, false); } bool KisPaintOpSettings::needsAsynchronousUpdates() const { return false; } QPainterPath KisPaintOpSettings::brushOutline(const KisPaintInformation &info, const OutlineMode &mode, qreal alignForZoom) { QPainterPath path; if (mode.isVisible) { path = ellipseOutline(10, 10, 1.0, 0); if (mode.showTiltDecoration) { path.addPath(makeTiltIndicator(info, QPointF(0.0, 0.0), 0.0, 2.0)); } path.translate(KisAlgebra2D::alignForZoom(info.pos(), alignForZoom)); } return path; } QPainterPath KisPaintOpSettings::ellipseOutline(qreal width, qreal height, qreal scale, qreal rotation) { QPainterPath path; QRectF ellipse(0, 0, width * scale, height * scale); ellipse.translate(-ellipse.center()); path.addEllipse(ellipse); QTransform m; m.reset(); m.rotate(rotation); path = m.map(path); return path; } QPainterPath KisPaintOpSettings::makeTiltIndicator(KisPaintInformation const& info, QPointF const& start, qreal maxLength, qreal angle) { if (maxLength == 0.0) maxLength = 50.0; maxLength = qMax(maxLength, 50.0); qreal const length = maxLength * (1 - info.tiltElevation(info, 60.0, 60.0, true)); qreal const baseAngle = 360.0 - fmod(KisPaintInformation::tiltDirection(info, true) * 360.0 + 270.0, 360.0); QLineF guideLine = QLineF::fromPolar(length, baseAngle + angle); guideLine.translate(start); QPainterPath ret; ret.moveTo(guideLine.p1()); ret.lineTo(guideLine.p2()); guideLine.setAngle(baseAngle - angle); ret.lineTo(guideLine.p2()); ret.lineTo(guideLine.p1()); return ret; } void KisPaintOpSettings::setProperty(const QString & name, const QVariant & value) { if (value != KisPropertiesConfiguration::getProperty(name) && !d->disableDirtyNotifications) { KisPaintOpPresetSP presetSP = preset().toStrongRef(); if (presetSP) { presetSP->setDirty(true); } } KisPropertiesConfiguration::setProperty(name, value); onPropertyChanged(); } void KisPaintOpSettings::onPropertyChanged() { KisPaintopSettingsUpdateProxy *proxy = d->updateProxyNoCreate(); if (proxy) { proxy->notifySettingsChanged(); } } bool KisPaintOpSettings::isLodUserAllowed(const KisPropertiesConfigurationSP config) { return config->getBool("lodUserAllowed", true); } void KisPaintOpSettings::setLodUserAllowed(KisPropertiesConfigurationSP config, bool value) { config->setProperty("lodUserAllowed", value); } bool KisPaintOpSettings::lodSizeThresholdSupported() const { return true; } qreal KisPaintOpSettings::lodSizeThreshold() const { return getDouble("lodSizeThreshold", 100.0); } void KisPaintOpSettings::setLodSizeThreshold(qreal value) { setProperty("lodSizeThreshold", value); } #include "kis_standard_uniform_properties_factory.h" QList KisPaintOpSettings::uniformProperties(KisPaintOpSettingsSP settings) { QList props = listWeakToStrong(d->uniformProperties); if (props.isEmpty()) { using namespace KisStandardUniformPropertiesFactory; props.append(createProperty(opacity, settings, d->updateProxyCreate())); props.append(createProperty(size, settings, d->updateProxyCreate())); props.append(createProperty(flow, settings, d->updateProxyCreate())); d->uniformProperties = listStrongToWeak(props); } return props; } diff --git a/libs/ui/flake/kis_shape_selection.h b/libs/ui/flake/kis_shape_selection.h index 3b2e8530e1..805d5c6f6f 100644 --- a/libs/ui/flake/kis_shape_selection.h +++ b/libs/ui/flake/kis_shape_selection.h @@ -1,140 +1,142 @@ /* * Copyright (c) 2007 Sven Langkamp * * 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_SHAPE_SELECTION_H #define KIS_SHAPE_SELECTION_H +#include + #include #include #include #include #include #include #include class KoStore; class KoShapeManager; class KisShapeSelectionCanvas; class KisShapeSelectionModel; class KisImageViewConverter; class KUndo2Command; /** * The marker class. * It is added to the shape's user data to show this shape * is a part of a shape selection */ class KisShapeSelectionMarker : public KoShapeUserData { KoShapeUserData* clone() const override { return new KisShapeSelectionMarker(*this); } }; class KRITAUI_EXPORT KisShapeSelection : public QObject, public KoShapeLayer, public KisSelectionComponent { Q_OBJECT KisShapeSelection(const KisShapeSelection& rhs); public: KisShapeSelection(KoShapeControllerBase *shapeControllerBase, KisImageWSP image, KisSelectionWSP selection); ~KisShapeSelection() override; KisShapeSelection(const KisShapeSelection& rhs, KisSelection* selection); KisSelectionComponent* clone(KisSelection* selection) override; bool saveSelection(KoStore * store) const; bool loadSelection(KoStore * store); /** * Renders the shapes to a selection. This method should only be called * by KisSelection to update it's projection. * * @param projection the target selection */ void renderToProjection(KisPaintDeviceSP projection) override; void renderToProjection(KisPaintDeviceSP projection, const QRect& r) override; KUndo2Command* resetToEmpty() override; bool isEmpty() const override; QPainterPath outlineCache() const override; bool outlineCacheValid() const override; void recalculateOutlineCache() override; KoShapeManager *shapeManager() const; void moveX(qint32 x) override; void moveY(qint32 y) override; KUndo2Command* transform(const QTransform &transform) override; Q_SIGNALS: void sigMoveShapes(const QPointF &diff); private Q_SLOTS: void slotMoveShapes(const QPointF &diff); protected: void paintComponent(QPainter& painter, const KoViewConverter& converter, KoShapePaintingContext &paintcontext) override; private: friend class KisTakeAllShapesCommand; void setUpdatesEnabled(bool enabled); bool updatesEnabled() const; private: void renderSelection(KisPaintDeviceSP projection, const QRect& requestedRect); KisImageWSP m_image; QPainterPath m_outline; KisImageViewConverter *m_converter; KisShapeSelectionCanvas *m_canvas; KisShapeSelectionModel *m_model; KoShapeControllerBase *m_shapeControllerBase; friend class KisShapeSelectionModel; }; class KRITAUI_EXPORT KisShapeSelectionFactory : public KoShapeFactoryBase { public: KisShapeSelectionFactory(); ~KisShapeSelectionFactory() override {} KoShape *createDefaultShape(KoDocumentResourceManager *documentResources = 0) const override { Q_UNUSED(documentResources); return 0; } bool supports(const KoXmlElement & e, KoShapeLoadingContext &context) const override { Q_UNUSED(e); Q_UNUSED(context); return false; } }; #endif diff --git a/libs/ui/kis_painting_assistants_decoration.cpp b/libs/ui/kis_painting_assistants_decoration.cpp index 11838e6079..a0a684c330 100644 --- a/libs/ui/kis_painting_assistants_decoration.cpp +++ b/libs/ui/kis_painting_assistants_decoration.cpp @@ -1,498 +1,499 @@ /* * Copyright (c) 2009 Cyrille Berger * Copyright (c) 2017 Scott Petrovic * * 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_painting_assistants_decoration.h" #include #include #include #include #include #include #include "kis_debug.h" #include "KisDocument.h" #include "kis_canvas2.h" #include "kis_icon_utils.h" #include "KisViewManager.h" #include +#include #include struct KisPaintingAssistantsDecoration::Private { Private() : assistantVisible(false) , outlineVisible(false) , snapOnlyOneAssistant(true) , firstAssistant(0) , aFirstStroke(false) , m_handleSize(14) {} bool assistantVisible; bool outlineVisible; bool snapOnlyOneAssistant; KisPaintingAssistantSP firstAssistant; KisPaintingAssistantSP selectedAssistant; bool aFirstStroke; bool m_isEditingAssistants = false; bool m_outlineVisible = false; int m_handleSize; // size of editor handles on assistants // move, visibility, delete icons for each assistant. These only display while the assistant tool is active // these icons will be covered by the kis_paintint_assistant_decoration with things like the perspective assistant AssistantEditorData toolData; QPixmap m_iconDelete = KisIconUtils::loadIcon("dialog-cancel").pixmap(toolData.deleteIconSize, toolData.deleteIconSize); QPixmap m_iconSnapOn = KisIconUtils::loadIcon("visible").pixmap(toolData.snapIconSize, toolData.snapIconSize); QPixmap m_iconSnapOff = KisIconUtils::loadIcon("novisible").pixmap(toolData.snapIconSize, toolData.snapIconSize); QPixmap m_iconMove = KisIconUtils::loadIcon("transform-move").pixmap(toolData.moveIconSize, toolData.moveIconSize); KisCanvas2 * m_canvas = 0; }; KisPaintingAssistantsDecoration::KisPaintingAssistantsDecoration(QPointer parent) : KisCanvasDecoration("paintingAssistantsDecoration", parent), d(new Private) { setAssistantVisible(true); setOutlineVisible(true); setPriority(95); d->snapOnlyOneAssistant = true; //turn on by default. } KisPaintingAssistantsDecoration::~KisPaintingAssistantsDecoration() { delete d; } void KisPaintingAssistantsDecoration::addAssistant(KisPaintingAssistantSP assistant) { QList assistants = view()->document()->assistants(); if (assistants.contains(assistant)) return; assistants.append(assistant); assistant->setAssistantGlobalColorCache(view()->document()->assistantsGlobalColor()); view()->document()->setAssistants(assistants); setVisible(!assistants.isEmpty()); emit assistantChanged(); } void KisPaintingAssistantsDecoration::removeAssistant(KisPaintingAssistantSP assistant) { QList assistants = view()->document()->assistants(); KIS_ASSERT_RECOVER_NOOP(assistants.contains(assistant)); if (assistants.removeAll(assistant)) { view()->document()->setAssistants(assistants); setVisible(!assistants.isEmpty()); emit assistantChanged(); } } void KisPaintingAssistantsDecoration::removeAll() { QList assistants = view()->document()->assistants(); assistants.clear(); view()->document()->setAssistants(assistants); setVisible(!assistants.isEmpty()); emit assistantChanged(); } void KisPaintingAssistantsDecoration::setAssistants(const QList &assistants) { Q_FOREACH (KisPaintingAssistantSP assistant, assistants) { assistant->setAssistantGlobalColorCache(view()->document()->assistantsGlobalColor()); } view()->document()->setAssistants(assistants); setVisible(!assistants.isEmpty()); emit assistantChanged(); } QPointF KisPaintingAssistantsDecoration::adjustPosition(const QPointF& point, const QPointF& strokeBegin) { if (assistants().empty()) { return point; } if (assistants().count() == 1) { if(assistants().first()->isSnappingActive() == true){ QPointF newpoint = assistants().first()->adjustPosition(point, strokeBegin); // check for NaN if (newpoint.x() != newpoint.x()) return point; return newpoint; } } QPointF best = point; double distance = DBL_MAX; //the following tries to find the closest point to stroke-begin. It checks all assistants for the closest point// if(!d->snapOnlyOneAssistant){ Q_FOREACH (KisPaintingAssistantSP assistant, assistants()) { if(assistant->isSnappingActive() == true){//this checks if the assistant in question has it's snapping boolean turned on// QPointF pt = assistant->adjustPosition(point, strokeBegin); if (pt.x() != pt.x()) continue; double dist = qAbs(pt.x() - point.x()) + qAbs(pt.y() - point.y()); if (dist < distance) { best = pt; distance = dist; } } } } else if (d->aFirstStroke==false) { Q_FOREACH (KisPaintingAssistantSP assistant, assistants()) { if(assistant->isSnappingActive() == true){//this checks if the assistant in question has it's snapping boolean turned on// QPointF pt = assistant->adjustPosition(point, strokeBegin); if (pt.x() != pt.x()) continue; double dist = qAbs(pt.x() - point.x()) + qAbs(pt.y() - point.y()); if (dist < distance) { best = pt; distance = dist; d->firstAssistant = assistant; } } } } else if(d->firstAssistant) { //make sure there's a first assistant to begin with.// QPointF newpoint = d->firstAssistant->adjustPosition(point, strokeBegin); // BUGFIX: 402535 // assistants might return (NaN,NaN), must always check for that if (newpoint.x() == newpoint.x()) { // not a NaN best = newpoint; } } else { d->aFirstStroke=false; } //this is here to be compatible with the movement in the perspective tool. qreal dx = point.x() - strokeBegin.x(), dy = point.y() - strokeBegin.y(); if (dx * dx + dy * dy >= 4.0) { // allow some movement before snapping d->aFirstStroke=true; } return best; } void KisPaintingAssistantsDecoration::endStroke() { d->aFirstStroke = false; Q_FOREACH (KisPaintingAssistantSP assistant, assistants()) { assistant->endStroke(); } } void KisPaintingAssistantsDecoration::drawDecoration(QPainter& gc, const QRectF& updateRect, const KisCoordinatesConverter *converter,KisCanvas2* canvas) { if(assistants().length() == 0) { return; // no assistants to worry about, ok to exit } if (!canvas) { dbgFile<<"canvas does not exist in painting assistant decoration, you may have passed arguments incorrectly:"<m_canvas = canvas; } // the preview functionality for assistants. do not show while editing if (d->m_isEditingAssistants) { d->m_outlineVisible = false; } else { d->m_outlineVisible = outlineVisibility(); } Q_FOREACH (KisPaintingAssistantSP assistant, assistants()) { assistant->drawAssistant(gc, updateRect, converter, true, canvas, assistantVisibility(), d->m_outlineVisible); if (isEditingAssistants()) { drawHandles(assistant, gc, converter); } } // draw editor controls on top of all assistant lines (why this code is last) if (isEditingAssistants()) { Q_FOREACH (KisPaintingAssistantSP assistant, assistants()) { drawEditorWidget(assistant, gc, converter); } } } void KisPaintingAssistantsDecoration::drawHandles(KisPaintingAssistantSP assistant, QPainter& gc, const KisCoordinatesConverter *converter) { QTransform initialTransform = converter->documentToWidgetTransform(); QColor colorToPaint = assistant->effectiveAssistantColor(); Q_FOREACH (const KisPaintingAssistantHandleSP handle, assistant->handles()) { QPointF transformedHandle = initialTransform.map(*handle); QRectF ellipse(transformedHandle - QPointF(handleSize() * 0.5, handleSize() * 0.5), QSizeF(handleSize(), handleSize())); QPainterPath path; path.addEllipse(ellipse); gc.save(); gc.setPen(Qt::NoPen); gc.setBrush(colorToPaint); gc.drawPath(path); gc.restore(); } // some assistants have side handles like the vanishing point assistant Q_FOREACH (const KisPaintingAssistantHandleSP handle, assistant->sideHandles()) { QPointF transformedHandle = initialTransform.map(*handle); QRectF ellipse(transformedHandle - QPointF(handleSize() * 0.5, handleSize() * 0.5), QSizeF(handleSize(), handleSize())); QPainterPath path; path.addEllipse(ellipse); gc.save(); gc.setPen(Qt::NoPen); gc.setBrush(colorToPaint); gc.drawPath(path); gc.restore(); } } int KisPaintingAssistantsDecoration::handleSize() { return d->m_handleSize; } void KisPaintingAssistantsDecoration::setHandleSize(int handleSize) { d->m_handleSize = handleSize; } QList KisPaintingAssistantsDecoration::handles() { QList hs; Q_FOREACH (KisPaintingAssistantSP assistant, assistants()) { Q_FOREACH (const KisPaintingAssistantHandleSP handle, assistant->handles()) { if (!hs.contains(handle)) { hs.push_back(handle); } } Q_FOREACH (const KisPaintingAssistantHandleSP handle, assistant->sideHandles()) { if (!hs.contains(handle)) { hs.push_back(handle); } } } return hs; } QList KisPaintingAssistantsDecoration::assistants() const { QList assistants = view()->document()->assistants(); return assistants; } KisPaintingAssistantSP KisPaintingAssistantsDecoration::selectedAssistant() { return d->selectedAssistant; } void KisPaintingAssistantsDecoration::setSelectedAssistant(KisPaintingAssistantSP assistant) { d->selectedAssistant = assistant; emit selectedAssistantChanged(); } void KisPaintingAssistantsDecoration::deselectAssistant() { d->selectedAssistant.clear(); } void KisPaintingAssistantsDecoration::setAssistantVisible(bool set) { d->assistantVisible=set; } void KisPaintingAssistantsDecoration::setOutlineVisible(bool set) { d->outlineVisible=set; } void KisPaintingAssistantsDecoration::setOnlyOneAssistantSnap(bool assistant) { d->snapOnlyOneAssistant = assistant; } bool KisPaintingAssistantsDecoration::assistantVisibility() { return d->assistantVisible; } bool KisPaintingAssistantsDecoration::outlineVisibility() { return d->outlineVisible; } void KisPaintingAssistantsDecoration::uncache() { Q_FOREACH (KisPaintingAssistantSP assistant, assistants()) { assistant->uncache(); } } void KisPaintingAssistantsDecoration::toggleAssistantVisible() { setAssistantVisible(!assistantVisibility()); uncache(); } void KisPaintingAssistantsDecoration::toggleOutlineVisible() { setOutlineVisible(!outlineVisibility()); } QColor KisPaintingAssistantsDecoration::globalAssistantsColor() { return view()->document()->assistantsGlobalColor(); } void KisPaintingAssistantsDecoration::setGlobalAssistantsColor(QColor color) { // view()->document() is referenced multiple times in this class // it is used to later store things in the KRA file when saving. view()->document()->setAssistantsGlobalColor(color); Q_FOREACH (KisPaintingAssistantSP assistant, assistants()) { assistant->setAssistantGlobalColorCache(color); } uncache(); } void KisPaintingAssistantsDecoration::activateAssistantsEditor() { setVisible(true); // this turns on the decorations in general. we leave it on at this point d->m_isEditingAssistants = true; uncache(); // updates visuals when editing } void KisPaintingAssistantsDecoration::deactivateAssistantsEditor() { if (!d->m_canvas) { return; } d->m_isEditingAssistants = false; // some elements are hidden when we aren't editing uncache(); // updates visuals when not editing } bool KisPaintingAssistantsDecoration::isEditingAssistants() { return d->m_isEditingAssistants; } QPointF KisPaintingAssistantsDecoration::snapToGuide(KoPointerEvent *e, const QPointF &offset, bool useModifiers) { if (!d->m_canvas || !d->m_canvas->currentImage()) { return e->point; } KoSnapGuide *snapGuide = d->m_canvas->snapGuide(); QPointF pos = snapGuide->snap(e->point, offset, useModifiers ? e->modifiers() : Qt::NoModifier); return pos; } QPointF KisPaintingAssistantsDecoration::snapToGuide(const QPointF& pt, const QPointF &offset) { if (!d->m_canvas) { return pt; } KoSnapGuide *snapGuide = d->m_canvas->snapGuide(); QPointF pos = snapGuide->snap(pt, offset, Qt::NoModifier); return pos; } /* * functions only used internally in this class * we potentially could make some of these inline to speed up performance */ void KisPaintingAssistantsDecoration::drawEditorWidget(KisPaintingAssistantSP assistant, QPainter& gc, const KisCoordinatesConverter *converter) { if (!assistant->isAssistantComplete()) { return; } AssistantEditorData toolData; // shared const data for positioning and sizing QTransform initialTransform = converter->documentToWidgetTransform(); QPointF actionsPosition = initialTransform.map(assistant->viewportConstrainedEditorPosition(converter, toolData.boundingSize)); QPointF iconMovePosition(actionsPosition + toolData.moveIconPosition); QPointF iconSnapPosition(actionsPosition + toolData.snapIconPosition); QPointF iconDeletePosition(actionsPosition + toolData.deleteIconPosition); // Background container for helpers QBrush backgroundColor = d->m_canvas->viewManager()->mainWindow()->palette().window(); QPointF actionsBGRectangle(actionsPosition + QPointF(10, 10)); gc.setRenderHint(QPainter::Antialiasing); QPainterPath bgPath; bgPath.addRoundedRect(QRectF(actionsBGRectangle.x(), actionsBGRectangle.y(), toolData.boundingSize.width(), toolData.boundingSize.height()), 6, 6); QPen stroke(QColor(60, 60, 60, 80), 2); // if the assistant is selected, make outline stroke fatter and use theme's highlight color // for better visual feedback if (selectedAssistant()) { // there might not be a selected assistant, so do not seg fault if (assistant->getEditorPosition() == selectedAssistant()->getEditorPosition()) { stroke.setWidth(4); stroke.setColor(qApp->palette().color(QPalette::Highlight)); } } // draw the final result gc.setPen(stroke); gc.fillPath(bgPath, backgroundColor); gc.drawPath(bgPath); // Move Assistant Tool helper gc.drawPixmap(iconMovePosition, d->m_iconMove); // active toggle if (assistant->isSnappingActive() == true) { gc.drawPixmap(iconSnapPosition, d->m_iconSnapOn); } else { gc.drawPixmap(iconSnapPosition, d->m_iconSnapOff); } gc.drawPixmap(iconDeletePosition, d->m_iconDelete); } diff --git a/libs/ui/kis_selection_decoration.h b/libs/ui/kis_selection_decoration.h index ea90ebb475..557328df71 100644 --- a/libs/ui/kis_selection_decoration.h +++ b/libs/ui/kis_selection_decoration.h @@ -1,76 +1,77 @@ /* * Copyright (c) 2008 Sven Langkamp * * 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 of the License, or * (at your option) any later version. * * 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 _KIS_SELECTION_DECORATION_H_ #define _KIS_SELECTION_DECORATION_H_ +#include #include #include #include #include #include "canvas/kis_canvas_decoration.h" class KisView; class KRITAUI_EXPORT KisSelectionDecoration : public KisCanvasDecoration { Q_OBJECT public: KisSelectionDecoration(QPointer view); ~KisSelectionDecoration() override; enum Mode { Ants, Mask }; Mode mode() const; void setMode(Mode mode); void setVisible(bool v) override; protected: void drawDecoration(QPainter& gc, const QRectF& updateRect, const KisCoordinatesConverter *converter,KisCanvas2* canvas) override; private Q_SLOTS: void slotStartUpdateSelection(); void slotConfigChanged(); public Q_SLOTS: void selectionChanged(); void antsAttackEvent(); private: bool selectionIsActive(); private: KisSignalCompressor m_signalCompressor; QPainterPath m_outlinePath; QImage m_thumbnailImage; QTransform m_thumbnailImageTransform; QTimer* m_antsTimer; int m_offset; QPen m_antsPen; QPen m_outlinePen; Mode m_mode; QColor m_maskColor; bool m_antialiasSelectionOutline; }; #endif diff --git a/libs/ui/tool/kis_shape_tool_helper.cpp b/libs/ui/tool/kis_shape_tool_helper.cpp index 4edab2a3d7..d914ff9598 100644 --- a/libs/ui/tool/kis_shape_tool_helper.cpp +++ b/libs/ui/tool/kis_shape_tool_helper.cpp @@ -1,79 +1,81 @@ /* * Copyright (c) 2009 Sven Langkamp * * 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_shape_tool_helper.h" +#include + #include #include #include #include KoShape* KisShapeToolHelper::createRectangleShape(const QRectF& rect, qreal roundCornersX, qreal roundCornersY) { KoShape* shape; KoShapeFactoryBase *rectFactory = KoShapeRegistry::instance()->value("RectangleShape"); if (rectFactory) { KoProperties props; props.setProperty("x", rect.x()); props.setProperty("y", rect.y()); props.setProperty("width", rect.width()); props.setProperty("height", rect.height()); props.setProperty("rx", 2 * 100.0 * roundCornersX / rect.width()); props.setProperty("ry", 2 * 100.0 * roundCornersY / rect.height()); shape = rectFactory->createShape(&props); } else { //Fallback if the plugin wasn't found QPainterPath path; if (roundCornersX > 0 || roundCornersY > 0) { path.addRoundedRect(rect, roundCornersX, roundCornersY); } else { path.addRect(rect); } KoPathShape *pathShape = KoPathShape::createShapeFromPainterPath(path); pathShape->normalize(); shape = pathShape; } return shape; } KoShape* KisShapeToolHelper::createEllipseShape(const QRectF& rect) { KoShape* shape; KoShapeFactoryBase *rectFactory = KoShapeRegistry::instance()->value("EllipseShape"); if (rectFactory) { shape = rectFactory->createDefaultShape(); shape->setSize(rect.size()); shape->setPosition(rect.topLeft()); } else { //Fallback if the plugin wasn't found KoPathShape* path = new KoPathShape(); path->setShapeId(KoPathShapeId); QPointF rightMiddle = QPointF(rect.left() + rect.width(), rect.top() + rect.height() / 2); path->moveTo(rightMiddle); path->arcTo(rect.width() / 2, rect.height() / 2, 0, 360.0); path->close(); path->normalize(); shape = path; } return shape; } diff --git a/libs/ui/tool/kis_tool_paint.h b/libs/ui/tool/kis_tool_paint.h index d3ea6f69eb..084fd10127 100644 --- a/libs/ui/tool/kis_tool_paint.h +++ b/libs/ui/tool/kis_tool_paint.h @@ -1,200 +1,201 @@ /* * Copyright (c) 2003 Boudewijn Rempt * * 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_PAINT_H_ #define KIS_TOOL_PAINT_H_ #include "kis_tool.h" #include +#include #include #include #include #include #include #include #include #include #include "kis_signal_compressor_with_param.h" #include #include class QGridLayout; class KoCompositeOp; class KoCanvasBase; class KRITAUI_EXPORT KisToolPaint : public KisTool { Q_OBJECT public: KisToolPaint(KoCanvasBase *canvas, const QCursor &cursor); ~KisToolPaint() override; int flags() const override; void mousePressEvent(KoPointerEvent *event) override; void mouseReleaseEvent(KoPointerEvent *event) override; void mouseMoveEvent(KoPointerEvent *event) override; protected: void setMode(ToolMode mode) override; void canvasResourceChanged(int key, const QVariant &v) override; void paint(QPainter &gc, const KoViewConverter &converter) override; void activatePrimaryAction() override; void deactivatePrimaryAction() override; void activateAlternateAction(AlternateAction action) override; void deactivateAlternateAction(AlternateAction action) override; void beginAlternateAction(KoPointerEvent *event, AlternateAction action) override; void continueAlternateAction(KoPointerEvent *event, AlternateAction action) override; void endAlternateAction(KoPointerEvent *event, AlternateAction action) override; virtual void requestUpdateOutline(const QPointF &outlineDocPoint, const KoPointerEvent *event); /** If the paint tool support outline like brushes, set to true. * If not (e.g. gradient tool), set to false. Default is false. */ void setSupportOutline(bool supportOutline) { m_supportOutline = supportOutline; } virtual QPainterPath getOutlinePath(const QPointF &documentPos, const KoPointerEvent *event, KisPaintOpSettings::OutlineMode outlineMode); protected: bool isOutlineEnabled() const; void setOutlineEnabled(bool enabled); bool pickColor(const QPointF &documentPixel, AlternateAction action); /// Add the tool-specific layout to the default option widget layout. void addOptionWidgetLayout(QLayout *layout); /// Add a widget and a label to the current option widget layout. virtual void addOptionWidgetOption(QWidget *control, QWidget *label = 0); void showControl(QWidget *control, bool value); void enableControl(QWidget *control, bool value); QWidget * createOptionWidget() override; /** * Quick help is a short help text about the way the tool functions. */ virtual QString quickHelp() const { return QString(); } const KoCompositeOp* compositeOp(); public Q_SLOTS: void activate(ToolActivation toolActivation, const QSet &shapes) override; void deactivate() override; private Q_SLOTS: void slotPopupQuickHelp(); void increaseBrushSize(); void decreaseBrushSize(); void activatePickColorDelayed(); void slotColorPickingFinished(KoColor color); protected: quint8 m_opacity; bool m_paintOutline; QPointF m_outlineDocPoint; QPainterPath m_currentOutline; QRectF m_oldOutlineRect; bool m_showColorPreview; QRectF m_oldColorPreviewRect; QRectF m_oldColorPreviewUpdateRect; QColor m_colorPreviewCurrentColor; bool m_colorPreviewShowComparePlate; QColor m_colorPreviewBaseColor; private: QPainterPath tryFixBrushOutline(const QPainterPath &originalOutline); void setOpacity(qreal opacity); void activatePickColor(AlternateAction action); void deactivatePickColor(AlternateAction action); void pickColorWasOverridden(); int colorPreviewResourceId(AlternateAction action); QRectF colorPreviewDocRect(const QPointF &outlineDocPoint); bool isPickingAction(AlternateAction action); struct PickingJob { PickingJob() {} PickingJob(QPointF _documentPixel, AlternateAction _action) : documentPixel(_documentPixel), action(_action) {} QPointF documentPixel; AlternateAction action; }; void addPickerJob(const PickingJob &pickingJob); private: bool m_specialHoverModifier; QGridLayout *m_optionsWidgetLayout; bool m_supportOutline; /** * Used as a switch for pickColor */ // used to skip some of the tablet events and don't update the colour that often QTimer m_colorPickerDelayTimer; AlternateAction delayedAction; bool m_isOutlineEnabled; std::vector m_standardBrushSizes; KisStrokeId m_pickerStrokeId; int m_pickingResource; typedef KisSignalCompressorWithParam PickingCompressor; QScopedPointer m_colorPickingCompressor; qreal m_localOpacity {1.0}; qreal m_oldOpacity {1.0}; Q_SIGNALS: void sigPaintingFinished(); }; #endif // KIS_TOOL_PAINT_H_ diff --git a/libs/ui/widgets/kis_cie_tongue_widget.cpp b/libs/ui/widgets/kis_cie_tongue_widget.cpp index 79a8bfc684..bbec19d70d 100644 --- a/libs/ui/widgets/kis_cie_tongue_widget.cpp +++ b/libs/ui/widgets/kis_cie_tongue_widget.cpp @@ -1,732 +1,733 @@ /* * Copyright (C) 2015 by Wolthera van Hövell tot Westerflier * * Based on the Digikam CIE Tongue widget * Copyright (C) 2006-2013 by Gilles Caulier * * Any source code are inspired from lprof project and * Copyright (C) 1998-2001 Marti Maria * * 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, 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. **/ /** The following table gives the CIE color matching functions \f$\bar{x}(\lambda)\f$, \f$\bar{y}(\lambda)\f$, and \f$\bar{z}(\lambda)\f$, for wavelengths \f$\lambda\f$ at 5 nanometer increments from 380 nm through 780 nm. This table is used in conjunction with Planck's law for the energy spectrum of a black body at a given temperature to plot the black body curve on the CIE chart. The following table gives the spectral chromaticity co-ordinates \f$x(\lambda)\f$ and \f$y(\lambda)\f$ for wavelengths in 5 nanometer increments from 380 nm through 780 nm. These coordinates represent the position in the CIE x-y space of pure spectral colors of the given wavelength, and thus define the outline of the CIE "tongue" diagram. */ #include #include +#include #include #include #include #include #include #include #include #include #include #include "kis_cie_tongue_widget.h" static const double spectral_chromaticity[81][3] = { { 0.1741, 0.0050 }, // 380 nm { 0.1740, 0.0050 }, { 0.1738, 0.0049 }, { 0.1736, 0.0049 }, { 0.1733, 0.0048 }, { 0.1730, 0.0048 }, { 0.1726, 0.0048 }, { 0.1721, 0.0048 }, { 0.1714, 0.0051 }, { 0.1703, 0.0058 }, { 0.1689, 0.0069 }, { 0.1669, 0.0086 }, { 0.1644, 0.0109 }, { 0.1611, 0.0138 }, { 0.1566, 0.0177 }, { 0.1510, 0.0227 }, { 0.1440, 0.0297 }, { 0.1355, 0.0399 }, { 0.1241, 0.0578 }, { 0.1096, 0.0868 }, { 0.0913, 0.1327 }, { 0.0687, 0.2007 }, { 0.0454, 0.2950 }, { 0.0235, 0.4127 }, { 0.0082, 0.5384 }, { 0.0039, 0.6548 }, { 0.0139, 0.7502 }, { 0.0389, 0.8120 }, { 0.0743, 0.8338 }, { 0.1142, 0.8262 }, { 0.1547, 0.8059 }, { 0.1929, 0.7816 }, { 0.2296, 0.7543 }, { 0.2658, 0.7243 }, { 0.3016, 0.6923 }, { 0.3373, 0.6589 }, { 0.3731, 0.6245 }, { 0.4087, 0.5896 }, { 0.4441, 0.5547 }, { 0.4788, 0.5202 }, { 0.5125, 0.4866 }, { 0.5448, 0.4544 }, { 0.5752, 0.4242 }, { 0.6029, 0.3965 }, { 0.6270, 0.3725 }, { 0.6482, 0.3514 }, { 0.6658, 0.3340 }, { 0.6801, 0.3197 }, { 0.6915, 0.3083 }, { 0.7006, 0.2993 }, { 0.7079, 0.2920 }, { 0.7140, 0.2859 }, { 0.7190, 0.2809 }, { 0.7230, 0.2770 }, { 0.7260, 0.2740 }, { 0.7283, 0.2717 }, { 0.7300, 0.2700 }, { 0.7311, 0.2689 }, { 0.7320, 0.2680 }, { 0.7327, 0.2673 }, { 0.7334, 0.2666 }, { 0.7340, 0.2660 }, { 0.7344, 0.2656 }, { 0.7346, 0.2654 }, { 0.7347, 0.2653 }, { 0.7347, 0.2653 }, { 0.7347, 0.2653 }, { 0.7347, 0.2653 }, { 0.7347, 0.2653 }, { 0.7347, 0.2653 }, { 0.7347, 0.2653 }, { 0.7347, 0.2653 }, { 0.7347, 0.2653 }, { 0.7347, 0.2653 }, { 0.7347, 0.2653 }, { 0.7347, 0.2653 }, { 0.7347, 0.2653 }, { 0.7347, 0.2653 }, { 0.7347, 0.2653 }, { 0.7347, 0.2653 }, { 0.7347, 0.2653 } // 780 nm }; class Q_DECL_HIDDEN KisCIETongueWidget::Private { public: Private() : profileDataAvailable(false), needUpdatePixmap(false), cieTongueNeedsUpdate(true), uncalibratedColor(false), xBias(0), yBias(0), pxcols(0), pxrows(0), progressCount(0), gridside(0), progressTimer(0), Primaries(9), whitePoint(3) { progressPix = KPixmapSequence("process-working", KisIconUtils::SizeSmallMedium); } bool profileDataAvailable; bool needUpdatePixmap; bool cieTongueNeedsUpdate; bool uncalibratedColor; int xBias; int yBias; int pxcols; int pxrows; int progressCount; // Position of animation during loading/calculation. double gridside; QPainter painter; QTimer* progressTimer; QPixmap pixmap; QPixmap cietongue; QPixmap gamutMap; KPixmapSequence progressPix; QVector Primaries; QVector whitePoint; QPolygonF gamut; model colorModel; }; KisCIETongueWidget::KisCIETongueWidget(QWidget *parent) : QWidget(parent), d(new Private) { d->progressTimer = new QTimer(this); setAttribute(Qt::WA_DeleteOnClose); d->Primaries.resize(9); d->Primaries.fill(0.0); d->whitePoint.resize(3); d->whitePoint<<0.34773<<0.35952<<1.0; d->gamut = QPolygonF(); connect(d->progressTimer, SIGNAL(timeout()), this, SLOT(slotProgressTimerDone())); } KisCIETongueWidget::~KisCIETongueWidget() { delete d; } int KisCIETongueWidget::grids(double val) const { return (int) floor(val * d->gridside + 0.5); } void KisCIETongueWidget::setProfileData(QVector p, QVector w, bool profileData) { d->profileDataAvailable = profileData; if (profileData){ d->Primaries= p; d->whitePoint = w; d->needUpdatePixmap = true; } else { return; } } void KisCIETongueWidget::setGamut(QPolygonF gamut) { d->gamut=gamut; } void KisCIETongueWidget::setRGBData(QVector whitepoint, QVector colorants) { if (colorants.size()==9){ d->Primaries= colorants; d->whitePoint = whitepoint; d->needUpdatePixmap = true; d->colorModel = KisCIETongueWidget::RGBA; d->profileDataAvailable = true; } else { return; } } void KisCIETongueWidget::setCMYKData(QVector whitepoint) { if (whitepoint.size()==3){ //d->Primaries= colorants; d->whitePoint = whitepoint; d->needUpdatePixmap = true; d->colorModel = KisCIETongueWidget::CMYKA; d->profileDataAvailable = true; } else { return; } } void KisCIETongueWidget::setXYZData(QVector whitepoint) { if (whitepoint.size()==3){ d->whitePoint = whitepoint; d->needUpdatePixmap = true; d->colorModel = KisCIETongueWidget::XYZA; d->profileDataAvailable = true; } else { return; } } void KisCIETongueWidget::setGrayData(QVector whitepoint) { if (whitepoint.size()==3){ d->whitePoint = whitepoint; d->needUpdatePixmap = true; d->colorModel = KisCIETongueWidget::GRAYA; d->profileDataAvailable = true; } else { return; } } void KisCIETongueWidget::setLABData(QVector whitepoint) { if (whitepoint.size()==3){ d->whitePoint = whitepoint; d->needUpdatePixmap = true; d->colorModel = KisCIETongueWidget::LABA; d->profileDataAvailable = true; } else { return; } } void KisCIETongueWidget::setYCbCrData(QVector whitepoint) { if (whitepoint.size()==3){ d->whitePoint = whitepoint; d->needUpdatePixmap = true; d->colorModel = KisCIETongueWidget::YCbCrA; d->profileDataAvailable = true; } else { return; } } void KisCIETongueWidget::setProfileDataAvailable(bool dataAvailable) { d->profileDataAvailable = dataAvailable; } void KisCIETongueWidget::mapPoint(int& icx, int& icy, QPointF xy) { icx = (int) floor((xy.x() * (d->pxcols - 1)) + .5); icy = (int) floor(((d->pxrows - 1) - xy.y() * (d->pxrows - 1)) + .5); } void KisCIETongueWidget::biasedLine(int x1, int y1, int x2, int y2) { d->painter.drawLine(x1 + d->xBias, y1, x2 + d->xBias, y2); } void KisCIETongueWidget::biasedText(int x, int y, const QString& txt) { d->painter.drawText(QPoint(d->xBias + x, y), txt); } QRgb KisCIETongueWidget::colorByCoord(double x, double y) { // Get xyz components scaled from coordinates double cx = ((double) x) / (d->pxcols - 1); double cy = 1.0 - ((double) y) / (d->pxrows - 1); double cz = 1.0 - cx - cy; // Project xyz to XYZ space. Note that in this // particular case we are substituting XYZ with xyz //Need to use KoColor here. const KoColorSpace* xyzColorSpace = KoColorSpaceRegistry::instance()->colorSpace("XYZA", "U8"); quint8 data[4]; data[0]= cx*255; data[1]= cy*255; data[2]= cz*255; data[3]= 1.0*255; KoColor colXYZ(data, xyzColorSpace); QColor colRGB = colXYZ.toQColor(); return qRgb(colRGB.red(), colRGB.green(), colRGB.blue()); } void KisCIETongueWidget::outlineTongue() { int lx=0, ly=0; int fx=0, fy=0; for (int x = 380; x <= 700; x += 5) { int ix = (x - 380) / 5; QPointF p(spectral_chromaticity[ix][0], spectral_chromaticity[ix][1]); int icx, icy; mapPoint(icx, icy, p); if (x > 380) { biasedLine(lx, ly, icx, icy); } else { fx = icx; fy = icy; } lx = icx; ly = icy; } biasedLine(lx, ly, fx, fy); } void KisCIETongueWidget::fillTongue() { QImage Img = d->cietongue.toImage(); int x; for (int y = 0; y < d->pxrows; ++y) { int xe = 0; // Find horizontal extents of tongue on this line. for (x = 0; x < d->pxcols; ++x) { if (QColor(Img.pixel(x + d->xBias, y)) != QColor(Qt::black)) { for (xe = d->pxcols - 1; xe >= x; --xe) { if (QColor(Img.pixel(xe + d->xBias, y)) != QColor(Qt::black)) { break; } } break; } } if (x < d->pxcols) { for ( ; x <= xe; ++x) { QRgb Color = colorByCoord(x, y); Img.setPixel(x + d->xBias, y, Color); } } } d->cietongue = QPixmap::fromImage(Img, Qt::AvoidDither); } void KisCIETongueWidget::drawTongueAxis() { QFont font; font.setPointSize(6); d->painter.setFont(font); d->painter.setPen(qRgb(255, 255, 255)); biasedLine(0, 0, 0, d->pxrows - 1); biasedLine(0, d->pxrows-1, d->pxcols-1, d->pxrows - 1); for (int y = 1; y <= 9; y += 1) { QString s; int xstart = (y * (d->pxcols - 1)) / 10; int ystart = (y * (d->pxrows - 1)) / 10; s.sprintf("0.%d", y); biasedLine(xstart, d->pxrows - grids(1), xstart, d->pxrows - grids(4)); biasedText(xstart - grids(11), d->pxrows + grids(15), s); s.sprintf("0.%d", 10 - y); biasedLine(0, ystart, grids(3), ystart); biasedText(grids(-25), ystart + grids(5), s); } } void KisCIETongueWidget::drawTongueGrid() { d->painter.setPen(qRgb(128, 128, 128)); d->painter.setOpacity(0.5); for (int y = 1; y <= 9; y += 1) { int xstart = (y * (d->pxcols - 1)) / 10; int ystart = (y * (d->pxrows - 1)) / 10; biasedLine(xstart, grids(4), xstart, d->pxrows - grids(4) - 1); biasedLine(grids(7), ystart, d->pxcols-1-grids(7), ystart); } d->painter.setOpacity(1.0); } void KisCIETongueWidget::drawLabels() { QFont font; font.setPointSize(5); d->painter.setFont(font); for (int x = 450; x <= 650; x += (x > 470 && x < 600) ? 5 : 10) { QString wl; int bx = 0, by = 0, tx, ty; if (x < 520) { bx = grids(-22); by = grids(2); } else if (x < 535) { bx = grids(-8); by = grids(-6); } else { bx = grids(4); } int ix = (x - 380) / 5; QPointF p(spectral_chromaticity[ix][0], spectral_chromaticity[ix][1]); int icx, icy; mapPoint(icx, icy, p); tx = icx + ((x < 520) ? grids(-2) : ((x >= 535) ? grids(2) : 0)); ty = icy + ((x < 520) ? 0 : ((x >= 535) ? grids(-1) : grids(-2))); d->painter.setPen(qRgb(255, 255, 255)); biasedLine(icx, icy, tx, ty); QRgb Color = colorByCoord(icx, icy); d->painter.setPen(Color); wl.sprintf("%d", x); biasedText(icx+bx, icy+by, wl); } } void KisCIETongueWidget::drawSmallEllipse(QPointF xy, int r, int g, int b, int sz) { int icx, icy; mapPoint(icx, icy, xy); d->painter.save(); d->painter.setRenderHint(QPainter::Antialiasing); d->painter.setPen(qRgb(r, g, b)); d->painter.drawEllipse(icx + d->xBias- sz/2, icy-sz/2, sz, sz); d->painter.setPen(qRgb(r/2, g/2, b/2)); int sz2 = sz-2; d->painter.drawEllipse(icx + d->xBias- sz2/2, icy-sz2/2, sz2, sz2); d->painter.restore(); } void KisCIETongueWidget::drawColorantTriangle() { d->painter.save(); d->painter.setPen(qRgb(80, 80, 80)); d->painter.setRenderHint(QPainter::Antialiasing); if (d->colorModel ==KisCIETongueWidget::RGBA) { drawSmallEllipse((QPointF(d->Primaries[0],d->Primaries[1])), 255, 128, 128, 6); drawSmallEllipse((QPointF(d->Primaries[3],d->Primaries[4])), 128, 255, 128, 6); drawSmallEllipse((QPointF(d->Primaries[6],d->Primaries[7])), 128, 128, 255, 6); int x1, y1, x2, y2, x3, y3; mapPoint(x1, y1, (QPointF(d->Primaries[0],d->Primaries[1])) ); mapPoint(x2, y2, (QPointF(d->Primaries[3],d->Primaries[4])) ); mapPoint(x3, y3, (QPointF(d->Primaries[6],d->Primaries[7])) ); biasedLine(x1, y1, x2, y2); biasedLine(x2, y2, x3, y3); biasedLine(x3, y3, x1, y1); } /*else if (d->colorModel ==CMYK){ for (i=0; iPrimaries.size();i+++){ drawSmallEllipse((QPointF(d->Primaries[0],d->Primaries[1])), 160, 160, 160, 6);//greyscale for now //int x1, y1, x2, y2; //mapPoint(x1, y1, (QPointF(d->Primaries[i],d->Primaries[i+1])) ); //mapPoint(x2, y2, (QPointF(d->Primaries[i+3],d->Primaries[i+4])) ); //biasedLine(x1, y1, x2, y2); } } */ d->painter.restore(); } void KisCIETongueWidget::drawWhitePoint() { drawSmallEllipse(QPointF (d->whitePoint[0],d->whitePoint[1]), 255, 255, 255, 8); } void KisCIETongueWidget::drawGamut() { d->gamutMap=QPixmap(size()); d->gamutMap.fill(Qt::black); QPainter gamutPaint; gamutPaint.begin(&d->gamutMap); QPainterPath path; //gamutPaint.setCompositionMode(QPainter::CompositionMode_Clear); gamutPaint.setRenderHint(QPainter::Antialiasing); path.setFillRule(Qt::WindingFill); gamutPaint.setBrush(Qt::white); gamutPaint.setPen(Qt::white); int x, y = 0; if (!d->gamut.empty()) { gamutPaint.setOpacity(0.5); if (d->colorModel == KisCIETongueWidget::RGBA) { mapPoint(x, y, (QPointF(d->Primaries[0],d->Primaries[1])) ); path.moveTo(QPointF(x + d->xBias,y)); mapPoint(x, y, (QPointF(d->Primaries[3],d->Primaries[4])) ); path.lineTo(QPointF(x + d->xBias,y)); mapPoint(x, y, (QPointF(d->Primaries[6],d->Primaries[7])) ); path.lineTo(QPointF(x + d->xBias,y)); mapPoint(x, y, (QPointF(d->Primaries[0],d->Primaries[1])) ); path.lineTo(QPointF(x + d->xBias,y)); } gamutPaint.drawPath(path); gamutPaint.setOpacity(1.0); foreach (QPointF Point, d->gamut) { mapPoint(x, y, Point); gamutPaint.drawEllipse(x + d->xBias- 2, y-2, 4, 4); //Point.setX(x); //Point.setY(y); //path.lineTo(Point); } } gamutPaint.end(); d->painter.save(); d->painter.setOpacity(0.5); d->painter.setCompositionMode(QPainter::CompositionMode_Multiply); QRect area(d->xBias, 0, d->pxcols, d->pxrows); d->painter.drawPixmap(area,d->gamutMap, area); d->painter.setOpacity(1.0); d->painter.restore(); } void KisCIETongueWidget::updatePixmap() { d->needUpdatePixmap = false; d->pixmap = QPixmap(size()); if (d->cieTongueNeedsUpdate){ // Draw the CIE tongue curve. I don't see why we need to redraw it every time the whitepoint and such changes so we cache it. d->cieTongueNeedsUpdate = false; d->cietongue = QPixmap(size()); d->cietongue.fill(Qt::black); d->painter.begin(&d->cietongue); int pixcols = d->pixmap.width(); int pixrows = d->pixmap.height(); d->gridside = (qMin(pixcols, pixrows)) / 512.0; d->xBias = grids(32); d->yBias = grids(20); d->pxcols = pixcols - d->xBias; d->pxrows = pixrows - d->yBias; d->painter.setBackground(QBrush(qRgb(0, 0, 0))); d->painter.setPen(qRgb(255, 255, 255)); outlineTongue(); d->painter.end(); fillTongue(); d->painter.begin(&d->cietongue); drawTongueAxis(); drawLabels(); drawTongueGrid(); d->painter.end(); } d->pixmap = d->cietongue; d->painter.begin(&d->pixmap); //draw whitepoint and colorants if (d->whitePoint[2] > 0.0) { drawWhitePoint(); } if (d->Primaries[2] != 0.0) { drawColorantTriangle(); } drawGamut(); d->painter.end(); } void KisCIETongueWidget::paintEvent(QPaintEvent*) { QPainter p(this); // Widget is disable : drawing grayed frame. if ( !isEnabled() ) { p.fillRect(0, 0, width(), height(), palette().color(QPalette::Disabled, QPalette::Background)); QPen pen(palette().color(QPalette::Disabled, QPalette::Foreground)); pen.setStyle(Qt::SolidLine); pen.setWidth(1); p.setPen(pen); p.drawRect(0, 0, width(), height()); return; } // No profile data to show, or RAW file if (!d->profileDataAvailable) { p.fillRect(0, 0, width(), height(), palette().color(QPalette::Active, QPalette::Background)); QPen pen(palette().color(QPalette::Active, QPalette::Text)); pen.setStyle(Qt::SolidLine); pen.setWidth(1); p.setPen(pen); p.drawRect(0, 0, width(), height()); if (d->uncalibratedColor) { p.drawText(0, 0, width(), height(), Qt::AlignCenter, i18n("Uncalibrated color space")); } else { p.setPen(Qt::red); p.drawText(0, 0, width(), height(), Qt::AlignCenter, i18n("No profile available...")); } return; } // Create CIE tongue if needed if (d->needUpdatePixmap) { updatePixmap(); } // draw prerendered tongue p.drawPixmap(0, 0, d->pixmap); } void KisCIETongueWidget::resizeEvent(QResizeEvent* event) { Q_UNUSED(event); setMinimumHeight(width()); setMaximumHeight(width()); d->needUpdatePixmap = true; d->cieTongueNeedsUpdate = true; } void KisCIETongueWidget::slotProgressTimerDone() { update(); d->progressTimer->start(200); } diff --git a/libs/ui/widgets/kis_curve_widget.cpp b/libs/ui/widgets/kis_curve_widget.cpp index 1c5292d21f..09de36eea8 100644 --- a/libs/ui/widgets/kis_curve_widget.cpp +++ b/libs/ui/widgets/kis_curve_widget.cpp @@ -1,552 +1,553 @@ /* * Copyright (c) 2005 C. Boemann * Copyright (c) 2009 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. */ // C++ includes. #include #include // Qt includes. #include #include +#include #include #include #include #include #include #include #include #include #include #include #include #include // KDE includes. #include #include #include #include #include // Local includes. #include "widgets/kis_curve_widget.h" #define bounds(x,a,b) (x
b ? b :x)) #define MOUSE_AWAY_THRES 15 #define POINT_AREA 1E-4 #define CURVE_AREA 1E-4 #include "kis_curve_widget_p.h" KisCurveWidget::KisCurveWidget(QWidget *parent, Qt::WindowFlags f) : QWidget(parent, f), d(new KisCurveWidget::Private(this)) { setObjectName("KisCurveWidget"); d->m_grab_point_index = -1; d->m_readOnlyMode = false; d->m_guideVisible = false; d->m_pixmapDirty = true; d->m_pixmapCache = 0; d->setState(ST_NORMAL); d->m_intIn = 0; d->m_intOut = 0; connect(&d->m_modifiedSignalsCompressor, SIGNAL(timeout()), SLOT(notifyModified())); connect(this, SIGNAL(compressorShouldEmitModified()), SLOT(slotCompressorShouldEmitModified())); setMouseTracking(true); setAutoFillBackground(false); setAttribute(Qt::WA_OpaquePaintEvent); setMinimumSize(150, 50); setMaximumSize(250, 250); setFocusPolicy(Qt::StrongFocus); } KisCurveWidget::~KisCurveWidget() { delete d->m_pixmapCache; delete d; } void KisCurveWidget::setupInOutControls(QSpinBox *in, QSpinBox *out, int inMin, int inMax, int outMin, int outMax) { dropInOutControls(); d->m_intIn = in; d->m_intOut = out; if (!d->m_intIn || !d->m_intOut) return; d->m_inMin = inMin; d->m_inMax = inMax; d->m_outMin = outMin; d->m_outMax = outMax; int realInMin = qMin(inMin, inMax); // tilt elevation has range (90, 0), which QSpinBox can't handle int realInMax = qMax(inMin, inMax); d->m_intIn->setRange(realInMin, realInMax); d->m_intOut->setRange(d->m_outMin, d->m_outMax); connect(d->m_intIn, SIGNAL(valueChanged(int)), this, SLOT(inOutChanged(int)), Qt::UniqueConnection); connect(d->m_intOut, SIGNAL(valueChanged(int)), this, SLOT(inOutChanged(int)), Qt::UniqueConnection); d->syncIOControls(); } void KisCurveWidget::dropInOutControls() { if (!d->m_intIn || !d->m_intOut) return; disconnect(d->m_intIn, SIGNAL(valueChanged(int)), this, SLOT(inOutChanged(int))); disconnect(d->m_intOut, SIGNAL(valueChanged(int)), this, SLOT(inOutChanged(int))); d->m_intIn = d->m_intOut = 0; } void KisCurveWidget::inOutChanged(int) { QPointF pt; Q_ASSERT(d->m_grab_point_index >= 0); pt.setX(d->io2sp(d->m_intIn->value(), d->m_inMin, d->m_inMax)); pt.setY(d->io2sp(d->m_intOut->value(), d->m_outMin, d->m_outMax)); bool newPoint = false; if (d->jumpOverExistingPoints(pt, d->m_grab_point_index)) { newPoint = true; d->m_curve.setPoint(d->m_grab_point_index, pt); d->m_grab_point_index = d->m_curve.points().indexOf(pt); emit pointSelectedChanged(); } else { pt = d->m_curve.points()[d->m_grab_point_index]; } if (!newPoint) { // if there is a new Point, no point in rewriting values in spinboxes d->m_intIn->blockSignals(true); d->m_intOut->blockSignals(true); d->m_intIn->setValue(d->sp2io(pt.x(), d->m_inMin, d->m_inMax)); d->m_intOut->setValue(d->sp2io(pt.y(), d->m_outMin, d->m_outMax)); d->m_intIn->blockSignals(false); d->m_intOut->blockSignals(false); } d->setCurveModified(false); } void KisCurveWidget::reset(void) { d->m_grab_point_index = -1; emit pointSelectedChanged(); d->m_guideVisible = false; //remove total - 2 points. while (d->m_curve.points().count() - 2 ) { d->m_curve.removePoint(d->m_curve.points().count() - 2); } d->setCurveModified(); } void KisCurveWidget::setCurveGuide(const QColor & color) { d->m_guideVisible = true; d->m_colorGuide = color; } void KisCurveWidget::setPixmap(const QPixmap & pix) { d->m_pix = pix; d->m_pixmapDirty = true; d->setCurveRepaint(); } QPixmap KisCurveWidget::getPixmap() { return d->m_pix; } void KisCurveWidget::setBasePixmap(const QPixmap &pix) { d->m_pixmapBase = pix; } QPixmap KisCurveWidget::getBasePixmap() { return d->m_pixmapBase; } bool KisCurveWidget::pointSelected() const { return d->m_grab_point_index > 0 && d->m_grab_point_index < d->m_curve.points().count() - 1; } void KisCurveWidget::keyPressEvent(QKeyEvent *e) { if (e->key() == Qt::Key_Delete || e->key() == Qt::Key_Backspace) { if (d->m_grab_point_index > 0 && d->m_grab_point_index < d->m_curve.points().count() - 1) { //x() find closest point to get focus afterwards double grab_point_x = d->m_curve.points()[d->m_grab_point_index].x(); int left_of_grab_point_index = d->m_grab_point_index - 1; int right_of_grab_point_index = d->m_grab_point_index + 1; int new_grab_point_index; if (fabs(d->m_curve.points()[left_of_grab_point_index].x() - grab_point_x) < fabs(d->m_curve.points()[right_of_grab_point_index].x() - grab_point_x)) { new_grab_point_index = left_of_grab_point_index; } else { new_grab_point_index = d->m_grab_point_index; } d->m_curve.removePoint(d->m_grab_point_index); d->m_grab_point_index = new_grab_point_index; emit pointSelectedChanged(); setCursor(Qt::ArrowCursor); d->setState(ST_NORMAL); } e->accept(); d->setCurveModified(); } else if (e->key() == Qt::Key_Escape && d->state() != ST_NORMAL) { d->m_curve.setPoint(d->m_grab_point_index, QPointF(d->m_grabOriginalX, d->m_grabOriginalY) ); setCursor(Qt::ArrowCursor); d->setState(ST_NORMAL); e->accept(); d->setCurveModified(); } else if ((e->key() == Qt::Key_A || e->key() == Qt::Key_Insert) && d->state() == ST_NORMAL) { /* FIXME: Lets user choose the hotkeys */ addPointInTheMiddle(); e->accept(); } else QWidget::keyPressEvent(e); } void KisCurveWidget::addPointInTheMiddle() { QPointF pt(0.5, d->m_curve.value(0.5)); if (!d->jumpOverExistingPoints(pt, -1)) return; d->m_grab_point_index = d->m_curve.addPoint(pt); emit pointSelectedChanged(); if (d->m_intIn) d->m_intIn->setFocus(Qt::TabFocusReason); d->setCurveModified(); } void KisCurveWidget::resizeEvent(QResizeEvent *e) { d->m_pixmapDirty = true; QWidget::resizeEvent(e); } void KisCurveWidget::paintEvent(QPaintEvent *) { int wWidth = width() - 1; int wHeight = height() - 1; QPainter p(this); // Antialiasing is not a good idea here, because // the grid will drift one pixel to any side due to rounding of int // FIXME: let's user tell the last word (in config) //p.setRenderHint(QPainter::Antialiasing); QPalette appPalette = QApplication::palette(); p.fillRect(rect(), appPalette.color(QPalette::Base)); // clear out previous paint call results // make the entire widget grayed out if it is disabled if (!this->isEnabled()) { p.setOpacity(0.2); } // draw background if (!d->m_pix.isNull()) { if (d->m_pixmapDirty || !d->m_pixmapCache) { delete d->m_pixmapCache; d->m_pixmapCache = new QPixmap(width(), height()); QPainter cachePainter(d->m_pixmapCache); cachePainter.scale(1.0*width() / d->m_pix.width(), 1.0*height() / d->m_pix.height()); cachePainter.drawPixmap(0, 0, d->m_pix); d->m_pixmapDirty = false; } p.drawPixmap(0, 0, *d->m_pixmapCache); } d->drawGrid(p, wWidth, wHeight); KisConfig cfg(true); if (cfg.antialiasCurves()) { p.setRenderHint(QPainter::Antialiasing); } // Draw curve. double curY; double normalizedX; int x; QPolygonF poly; p.setPen(QPen(appPalette.color(QPalette::Text), 2, Qt::SolidLine)); for (x = 0 ; x < wWidth ; x++) { normalizedX = double(x) / wWidth; curY = wHeight - d->m_curve.value(normalizedX) * wHeight; /** * Keep in mind that QLineF rounds doubles * to ints mathematically, not just rounds down * like in C */ poly.append(QPointF(x, curY)); } poly.append(QPointF(x, wHeight - d->m_curve.value(1.0) * wHeight)); p.drawPolyline(poly); QPainterPath fillCurvePath; QPolygonF fillPoly = poly; fillPoly.append(QPoint(rect().width(), rect().height())); fillPoly.append(QPointF(0,rect().height())); // add a couple points to the edges so it fills in below always QColor fillColor = appPalette.color(QPalette::Text); fillColor.setAlphaF(0.2); fillCurvePath.addPolygon(fillPoly); p.fillPath(fillCurvePath, fillColor); // Drawing curve handles. double curveX; double curveY; if (!d->m_readOnlyMode) { for (int i = 0; i < d->m_curve.points().count(); ++i) { curveX = d->m_curve.points().at(i).x(); curveY = d->m_curve.points().at(i).y(); int handleSize = 12; // how big should control points be (diamater size) if (i == d->m_grab_point_index) { // active point is slightly more "bold" p.setPen(QPen(appPalette.color(QPalette::Text), 4, Qt::SolidLine)); p.drawEllipse(QRectF(curveX * wWidth - (handleSize*0.5), wHeight - (handleSize*0.5) - curveY * wHeight, handleSize, handleSize)); } else { p.setPen(QPen(appPalette.color(QPalette::Text), 2, Qt::SolidLine)); p.drawEllipse(QRectF(curveX * wWidth - (handleSize*0.5), wHeight - (handleSize*0.5) - curveY * wHeight, handleSize, handleSize)); } } } // add border around widget to help contain everything QPainterPath widgetBoundsPath; widgetBoundsPath.addRect(rect()); p.strokePath(widgetBoundsPath, appPalette.color(QPalette::Text)); p.setOpacity(1.0); // reset to 1.0 in case we were drawing a disabled widget before } void KisCurveWidget::mousePressEvent(QMouseEvent * e) { if (d->m_readOnlyMode) return; if (e->button() != Qt::LeftButton) return; double x = e->pos().x() / (double)(width() - 1); double y = 1.0 - e->pos().y() / (double)(height() - 1); int closest_point_index = d->nearestPointInRange(QPointF(x, y), width(), height()); if (closest_point_index < 0) { QPointF newPoint(x, y); if (!d->jumpOverExistingPoints(newPoint, -1)) return; d->m_grab_point_index = d->m_curve.addPoint(newPoint); emit pointSelectedChanged(); } else { d->m_grab_point_index = closest_point_index; emit pointSelectedChanged(); } d->m_grabOriginalX = d->m_curve.points()[d->m_grab_point_index].x(); d->m_grabOriginalY = d->m_curve.points()[d->m_grab_point_index].y(); d->m_grabOffsetX = d->m_curve.points()[d->m_grab_point_index].x() - x; d->m_grabOffsetY = d->m_curve.points()[d->m_grab_point_index].y() - y; d->m_curve.setPoint(d->m_grab_point_index, QPointF(x + d->m_grabOffsetX, y + d->m_grabOffsetY)); d->m_draggedAwayPointIndex = -1; d->setState(ST_DRAG); d->setCurveModified(); } void KisCurveWidget::mouseReleaseEvent(QMouseEvent *e) { if (d->m_readOnlyMode) return; if (e->button() != Qt::LeftButton) return; setCursor(Qt::ArrowCursor); d->setState(ST_NORMAL); d->setCurveModified(); } void KisCurveWidget::mouseMoveEvent(QMouseEvent * e) { if (d->m_readOnlyMode) return; double x = e->pos().x() / (double)(width() - 1); double y = 1.0 - e->pos().y() / (double)(height() - 1); if (d->state() == ST_NORMAL) { // If no point is selected set the cursor shape if on top int nearestPointIndex = d->nearestPointInRange(QPointF(x, y), width(), height()); if (nearestPointIndex < 0) setCursor(Qt::ArrowCursor); else setCursor(Qt::CrossCursor); } else { // Else, drag the selected point bool crossedHoriz = e->pos().x() - width() > MOUSE_AWAY_THRES || e->pos().x() < -MOUSE_AWAY_THRES; bool crossedVert = e->pos().y() - height() > MOUSE_AWAY_THRES || e->pos().y() < -MOUSE_AWAY_THRES; bool removePoint = (crossedHoriz || crossedVert); if (!removePoint && d->m_draggedAwayPointIndex >= 0) { // point is no longer dragged away so reinsert it QPointF newPoint(d->m_draggedAwayPoint); d->m_grab_point_index = d->m_curve.addPoint(newPoint); d->m_draggedAwayPointIndex = -1; } if (removePoint && (d->m_draggedAwayPointIndex >= 0)) return; setCursor(Qt::CrossCursor); x += d->m_grabOffsetX; y += d->m_grabOffsetY; double leftX; double rightX; if (d->m_grab_point_index == 0) { leftX = 0.0; if (d->m_curve.points().count() > 1) rightX = d->m_curve.points()[d->m_grab_point_index + 1].x() - POINT_AREA; else rightX = 1.0; } else if (d->m_grab_point_index == d->m_curve.points().count() - 1) { leftX = d->m_curve.points()[d->m_grab_point_index - 1].x() + POINT_AREA; rightX = 1.0; } else { Q_ASSERT(d->m_grab_point_index > 0 && d->m_grab_point_index < d->m_curve.points().count() - 1); // the 1E-4 addition so we can grab the dot later. leftX = d->m_curve.points()[d->m_grab_point_index - 1].x() + POINT_AREA; rightX = d->m_curve.points()[d->m_grab_point_index + 1].x() - POINT_AREA; } x = bounds(x, leftX, rightX); y = bounds(y, 0., 1.); d->m_curve.setPoint(d->m_grab_point_index, QPointF(x, y)); if (removePoint && d->m_curve.points().count() > 2) { d->m_draggedAwayPoint = d->m_curve.points()[d->m_grab_point_index]; d->m_draggedAwayPointIndex = d->m_grab_point_index; d->m_curve.removePoint(d->m_grab_point_index); d->m_grab_point_index = bounds(d->m_grab_point_index, 0, d->m_curve.points().count() - 1); emit pointSelectedChanged(); } d->setCurveModified(); } } KisCubicCurve KisCurveWidget::curve() { return d->m_curve; } void KisCurveWidget::setCurve(KisCubicCurve inlist) { d->m_curve = inlist; d->m_grab_point_index = qBound(0, d->m_grab_point_index, d->m_curve.points().count() - 1); d->setCurveModified(); emit pointSelectedChanged(); } void KisCurveWidget::leaveEvent(QEvent *) { } void KisCurveWidget::notifyModified() { emit modified(); } void KisCurveWidget::slotCompressorShouldEmitModified() { d->m_modifiedSignalsCompressor.start(); } diff --git a/libs/ui/widgets/kis_tone_curve_widget.cpp b/libs/ui/widgets/kis_tone_curve_widget.cpp index 679fed4fd5..6eaca4b329 100644 --- a/libs/ui/widgets/kis_tone_curve_widget.cpp +++ b/libs/ui/widgets/kis_tone_curve_widget.cpp @@ -1,362 +1,363 @@ /* * Copyright (C) 2015 by Wolthera van Hövell tot Westerflier * * Based on the Digikam CIE Tongue widget * Copyright (C) 2006-2013 by Gilles Caulier * * Any source code are inspired from lprof project and * Copyright (C) 1998-2001 Marti Maria * * 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, 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 #include #include +#include #include #include #include #include #include "kis_tone_curve_widget.h" class Q_DECL_HIDDEN KisToneCurveWidget::Private { public: Private() : profileDataAvailable(false), needUpdatePixmap(false), TRCGray(true), xBias(0), yBias(0), pxcols(0), pxrows(0), gridside(0) { } bool profileDataAvailable; bool needUpdatePixmap; bool TRCGray; bool TRCRGB; int xBias; int yBias; int pxcols; int pxrows; QPolygonF ToneCurveGray; QPolygonF ToneCurveRed; QPolygonF ToneCurveGreen; QPolygonF ToneCurveBlue; double gridside; QPainter painter; QPainter painter2; QPixmap pixmap; QPixmap curvemap; }; KisToneCurveWidget::KisToneCurveWidget(QWidget *parent) : QWidget(parent), d(new Private) { /*this is a tone curve widget*/ } KisToneCurveWidget::~KisToneCurveWidget() { delete d; } void KisToneCurveWidget::setGreyscaleCurve(QPolygonF poly) { d->ToneCurveGray = poly; d->TRCGray = true; d->TRCRGB = false; d->profileDataAvailable = true; d->needUpdatePixmap = true; } void KisToneCurveWidget::setRGBCurve(QPolygonF red, QPolygonF green, QPolygonF blue) { d->ToneCurveRed = red; d->ToneCurveGreen = green; d->ToneCurveBlue = blue; d->profileDataAvailable = true; d->TRCGray = false; d->TRCRGB = true; d->needUpdatePixmap = true; } void KisToneCurveWidget::setCMYKCurve(QPolygonF cyan, QPolygonF magenta, QPolygonF yellow, QPolygonF key) { d->ToneCurveRed = cyan; d->ToneCurveGreen = magenta; d->ToneCurveBlue = yellow; d->ToneCurveGray = key; d->profileDataAvailable = true; d->TRCGray = false; d->TRCRGB = false; d->needUpdatePixmap = true; } void KisToneCurveWidget::setProfileDataAvailable(bool dataAvailable) { d->profileDataAvailable = dataAvailable; } int KisToneCurveWidget::grids(double val) const { return (int) floor(val * d->gridside + 0.5); } void KisToneCurveWidget::mapPoint(QPointF & xy) { QPointF dummy = xy; xy.setX( (int) floor((dummy.x() * (d->pxcols - 1)) + .5) + d->xBias); xy.setY( (int) floor(((d->pxrows - 1) - dummy.y() * (d->pxrows - 1)) + .5) ); } void KisToneCurveWidget::biasedLine(int x1, int y1, int x2, int y2) { d->painter.drawLine(x1 + d->xBias, y1, x2 + d->xBias, y2); } void KisToneCurveWidget::biasedText(int x, int y, const QString& txt) { d->painter.drawText(QPoint(d->xBias + x, y), txt); } void KisToneCurveWidget::drawGrid() { d->painter.setOpacity(1.0); d->painter.setPen(qRgb(255, 255, 255)); biasedLine(0, 0, 0, d->pxrows - 1); biasedLine(0, d->pxrows-1, d->pxcols-1, d->pxrows - 1); QFont font; font.setPointSize(6); d->painter.setFont(font); for (int y = 1; y <= 9; y += 1) { QString s; int xstart = (y * (d->pxcols - 1)) / 10; int ystart = (y * (d->pxrows - 1)) / 10; s.sprintf("0.%d", y); biasedLine(xstart, d->pxrows - grids(1), xstart, d->pxrows - grids(4)); biasedText(xstart - grids(11), d->pxrows + grids(15), s); s.sprintf("0.%d", 10 - y); biasedLine(0, ystart, grids(3), ystart); biasedText(grids(-25), ystart + grids(5), s); } d->painter.setPen(qRgb(128, 128, 128)); d->painter.setOpacity(0.5); for (int y = 1; y <= 9; y += 1) { int xstart = (y * (d->pxcols - 1)) / 10; int ystart = (y * (d->pxrows - 1)) / 10; biasedLine(xstart, grids(4), xstart, d->pxrows - grids(4) - 1); biasedLine(grids(7), ystart, d->pxcols-1-grids(7), ystart); } d->painter.setOpacity(1.0); d->painter.setOpacity(1.0); } void KisToneCurveWidget::updatePixmap() { d->needUpdatePixmap = false; d->pixmap = QPixmap(size()); d->curvemap = QPixmap(size()); d->pixmap.fill(Qt::black); d->curvemap.fill(Qt::transparent); d->painter.begin(&d->pixmap); int pixcols = d->pixmap.width(); int pixrows = d->pixmap.height(); d->gridside = (qMin(pixcols, pixrows)) / 512.0; d->xBias = grids(32); d->yBias = grids(20); d->pxcols = pixcols - d->xBias; d->pxrows = pixrows - d->yBias; d->painter.setBackground(QBrush(qRgb(0, 0, 0))); QPointF start; drawGrid(); d->painter.setRenderHint(QPainter::Antialiasing); if (d->TRCGray && d->ToneCurveGray.size()>0){ QPainterPath path; start = d->ToneCurveGray.at(0); mapPoint(start); path.moveTo(start); foreach (QPointF Point, d->ToneCurveGray) { mapPoint(Point); path.lineTo(Point); } d->painter.setPen(qRgb(255, 255, 255)); d->painter.drawPath(path); } else if (d->TRCRGB && d->ToneCurveRed.size()>0 && d->ToneCurveGreen.size()>0 && d->ToneCurveBlue.size()>0){ d->painter.save(); d->painter.setCompositionMode(QPainter::CompositionMode_Screen); QPainterPath path; start = d->ToneCurveRed.at(0); mapPoint(start); path.moveTo(start); foreach (QPointF Point, d->ToneCurveRed) { mapPoint(Point); path.lineTo(Point); } d->painter.setPen(qRgb(255, 0, 0)); d->painter.drawPath(path); QPainterPath path2; start = d->ToneCurveGreen.at(0); mapPoint(start); path2.moveTo(start); foreach (QPointF Point, d->ToneCurveGreen) { mapPoint(Point); path2.lineTo(Point); } d->painter.setPen(qRgb(0, 255, 0)); d->painter.drawPath(path2); QPainterPath path3; start = d->ToneCurveBlue.at(0); mapPoint(start); path3.moveTo(start); foreach (QPointF Point, d->ToneCurveBlue) { mapPoint(Point); path3.lineTo(Point); } d->painter.setPen(qRgb(0, 0, 255)); d->painter.drawPath(path3); d->painter.restore(); } else { d->painter2.begin(&d->curvemap); d->painter2.setRenderHint(QPainter::Antialiasing); //d->painter2.setCompositionMode(QPainter::CompositionMode_Multiply); QPainterPath path; start = d->ToneCurveRed.at(0); mapPoint(start); path.moveTo(start); foreach (QPointF Point, d->ToneCurveRed) { mapPoint(Point); path.lineTo(Point); } d->painter2.setPen(qRgb(0, 255, 255)); d->painter2.drawPath(path); QPainterPath path2; start = d->ToneCurveGreen.at(0); mapPoint(start); path2.moveTo(start); foreach (QPointF Point, d->ToneCurveGreen) { mapPoint(Point); path2.lineTo(Point); } d->painter2.setPen(qRgb(255, 0, 255)); d->painter2.drawPath(path2); QPainterPath path3; start = d->ToneCurveBlue.at(0); mapPoint(start); path3.moveTo(start); foreach (QPointF Point, d->ToneCurveBlue) { mapPoint(Point); path3.lineTo(Point); } d->painter2.setPen(qRgb(255, 255, 0)); d->painter2.drawPath(path3); QPainterPath path4; start = d->ToneCurveGray.at(0); mapPoint(start); path4.moveTo(start); foreach (QPointF Point, d->ToneCurveGray) { mapPoint(Point); path4.lineTo(Point); } d->painter2.setPen(qRgb(80, 80, 80)); d->painter2.drawPath(path4); d->painter2.end(); QRect area(d->xBias, 0, d->pxcols, d->pxrows); d->painter.drawPixmap(area,d->curvemap, area); } d->painter.end(); } void KisToneCurveWidget::paintEvent(QPaintEvent*) { QPainter p(this); // Widget is disable : drawing grayed frame. if ( !isEnabled() ) { p.fillRect(0, 0, width(), height(), palette().color(QPalette::Disabled, QPalette::Background)); QPen pen(palette().color(QPalette::Disabled, QPalette::Foreground)); pen.setStyle(Qt::SolidLine); pen.setWidth(1); p.setPen(pen); p.drawRect(0, 0, width(), height()); return; } // No profile data to show, or RAW file if (!d->profileDataAvailable) { p.fillRect(0, 0, width(), height(), palette().color(QPalette::Active, QPalette::Background)); QPen pen(palette().color(QPalette::Active, QPalette::Text)); pen.setStyle(Qt::SolidLine); pen.setWidth(1); p.setPen(pen); p.drawRect(0, 0, width(), height()); p.setPen(Qt::red); p.drawText(0, 0, width(), height(), Qt::AlignCenter, i18n("No tone curve available...")); return; } // Create CIE tongue if needed if (d->needUpdatePixmap) { updatePixmap(); } // draw prerendered tongue p.drawPixmap(0, 0, d->pixmap); } void KisToneCurveWidget::resizeEvent(QResizeEvent* event) { Q_UNUSED(event); setMinimumWidth(height()); setMaximumWidth(height()); d->needUpdatePixmap = true; } diff --git a/plugins/assistants/Assistants/ConcentricEllipseAssistant.cc b/plugins/assistants/Assistants/ConcentricEllipseAssistant.cc index 8c14b969bf..44078f323a 100644 --- a/plugins/assistants/Assistants/ConcentricEllipseAssistant.cc +++ b/plugins/assistants/Assistants/ConcentricEllipseAssistant.cc @@ -1,208 +1,209 @@ /* * Copyright (c) 2008 Cyrille Berger * Copyright (c) 2010 Geoffry Song * Copyright (c) 2017 Scott Petrovic * * 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 of the License, or * (at your option) any later version. * * 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 "ConcentricEllipseAssistant.h" #include #include "kis_debug.h" #include +#include #include #include #include #include #include #include ConcentricEllipseAssistant::ConcentricEllipseAssistant() : KisPaintingAssistant("concentric ellipse", i18n("Concentric Ellipse assistant")) { } KisPaintingAssistantSP ConcentricEllipseAssistant::clone(QMap &handleMap) const { return KisPaintingAssistantSP(new ConcentricEllipseAssistant(*this, handleMap)); } ConcentricEllipseAssistant::ConcentricEllipseAssistant(const ConcentricEllipseAssistant &rhs, QMap &handleMap) : KisPaintingAssistant(rhs, handleMap) , m_ellipse(rhs.m_ellipse) , m_extraEllipse(rhs.m_extraEllipse) { } QPointF ConcentricEllipseAssistant::project(const QPointF& pt, const QPointF& strokeBegin) const { Q_ASSERT(isAssistantComplete()); m_ellipse.set(*handles()[0], *handles()[1], *handles()[2]); qreal dx = pt.x() - strokeBegin.x(), dy = pt.y() - strokeBegin.y(); if (dx * dx + dy * dy < 4.0) { // allow some movement before snapping return strokeBegin; } //calculate ratio QPointF initial = m_ellipse.project(strokeBegin); QPointF center = m_ellipse.boundingRect().center(); qreal Ratio = QLineF(center, strokeBegin).length() /QLineF(center, initial).length(); //calculate the points of the extrapolated ellipse. QLineF extrapolate0 = QLineF(center, *handles()[0]); extrapolate0.setLength(extrapolate0.length()*Ratio); QLineF extrapolate1 = QLineF(center, *handles()[1]); extrapolate1.setLength(extrapolate1.length()*Ratio); QLineF extrapolate2 = QLineF(center, *handles()[2]); extrapolate2.setLength(extrapolate2.length()*Ratio); //set the extrapolation ellipse. m_extraEllipse.set(extrapolate0.p2(), extrapolate1.p2(), extrapolate2.p2()); return m_extraEllipse.project(pt); } QPointF ConcentricEllipseAssistant::adjustPosition(const QPointF& pt, const QPointF& strokeBegin) { return project(pt, strokeBegin); } void ConcentricEllipseAssistant::drawAssistant(QPainter& gc, const QRectF& updateRect, const KisCoordinatesConverter* converter, bool cached, KisCanvas2* canvas, bool assistantVisible, bool previewVisible) { gc.save(); gc.resetTransform(); QPointF mousePos; if (canvas){ //simplest, cheapest way to get the mouse-position// mousePos= canvas->canvasWidget()->mapFromGlobal(QCursor::pos()); } else { //...of course, you need to have access to a canvas-widget for that.// mousePos = QCursor::pos();//this'll give an offset// dbgFile<<"canvas does not exist in the ellipse assistant, you may have passed arguments incorrectly:"<documentToWidgetTransform(); if (isSnappingActive() && previewVisible == true){ if (isAssistantComplete()){ if (m_ellipse.set(*handles()[0], *handles()[1], *handles()[2])) { QPointF initial = m_ellipse.project(initialTransform.inverted().map(mousePos)); QPointF center = m_ellipse.boundingRect().center(); qreal Ratio = QLineF(center, initialTransform.inverted().map(mousePos)).length() /QLineF(center, initial).length(); //line from center to handle 1 * difference. //set handle1 translated to // valid ellipse gc.setTransform(initialTransform); gc.setTransform(m_ellipse.getInverse(), true); QPainterPath path; // Draw the ellipse path.addEllipse(QPointF(0, 0), m_ellipse.semiMajor()*Ratio, m_ellipse.semiMinor()*Ratio); drawPreview(gc, path); } } } gc.restore(); KisPaintingAssistant::drawAssistant(gc, updateRect, converter, cached, canvas, assistantVisible, previewVisible); } void ConcentricEllipseAssistant::drawCache(QPainter& gc, const KisCoordinatesConverter *converter, bool assistantVisible) { if (assistantVisible == false || handles().size() < 2) { // 2 points means a line, so we can continue after 1 point return; } QTransform initialTransform = converter->documentToWidgetTransform(); if (handles().size() == 2) { // just draw the axis gc.setTransform(initialTransform); QPainterPath path; path.moveTo(*handles()[0]); path.lineTo(*handles()[1]); drawPath(gc, path, isSnappingActive()); return; } if (m_ellipse.set(*handles()[0], *handles()[1], *handles()[2])) { // valid ellipse gc.setTransform(initialTransform); gc.setTransform(m_ellipse.getInverse(), true); QPainterPath path; path.moveTo(QPointF(-m_ellipse.semiMajor(), 0)); path.lineTo(QPointF(m_ellipse.semiMajor(), 0)); path.moveTo(QPointF(0, -m_ellipse.semiMinor())); path.lineTo(QPointF(0, m_ellipse.semiMinor())); // Draw the ellipse path.addEllipse(QPointF(0, 0), m_ellipse.semiMajor(), m_ellipse.semiMinor()); drawPath(gc, path, isSnappingActive()); } } QRect ConcentricEllipseAssistant::boundingRect() const { if (!isAssistantComplete()) { return KisPaintingAssistant::boundingRect(); } if (m_ellipse.set(*handles()[0], *handles()[1], *handles()[2])) { return m_ellipse.boundingRect().adjusted(-2, -2, 2, 2).toAlignedRect(); } else { return QRect(); } } QPointF ConcentricEllipseAssistant::getEditorPosition() const { return (*handles()[0] + *handles()[1]) * 0.5; } bool ConcentricEllipseAssistant::isAssistantComplete() const { return handles().size() >= 3; } ConcentricEllipseAssistantFactory::ConcentricEllipseAssistantFactory() { } ConcentricEllipseAssistantFactory::~ConcentricEllipseAssistantFactory() { } QString ConcentricEllipseAssistantFactory::id() const { return "concentric ellipse"; } QString ConcentricEllipseAssistantFactory::name() const { return i18n("Concentric Ellipse"); } KisPaintingAssistant* ConcentricEllipseAssistantFactory::createPaintingAssistant() const { return new ConcentricEllipseAssistant; } diff --git a/plugins/assistants/Assistants/EllipseAssistant.cc b/plugins/assistants/Assistants/EllipseAssistant.cc index 8b3b16c64c..09d48fd972 100644 --- a/plugins/assistants/Assistants/EllipseAssistant.cc +++ b/plugins/assistants/Assistants/EllipseAssistant.cc @@ -1,181 +1,182 @@ /* * Copyright (c) 2008 Cyrille Berger * Copyright (c) 2010 Geoffry Song * Copyright (c) 2017 Scott Petrovic * * 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 of the License, or * (at your option) any later version. * * 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 "EllipseAssistant.h" #include #include "kis_debug.h" #include +#include #include #include #include #include #include EllipseAssistant::EllipseAssistant() : KisPaintingAssistant("ellipse", i18n("Ellipse assistant")) { } EllipseAssistant::EllipseAssistant(const EllipseAssistant &rhs, QMap &handleMap) : KisPaintingAssistant(rhs, handleMap) , e(rhs.e) { } KisPaintingAssistantSP EllipseAssistant::clone(QMap &handleMap) const { return KisPaintingAssistantSP(new EllipseAssistant(*this, handleMap)); } QPointF EllipseAssistant::project(const QPointF& pt) const { Q_ASSERT(isAssistantComplete()); e.set(*handles()[0], *handles()[1], *handles()[2]); return e.project(pt); } QPointF EllipseAssistant::adjustPosition(const QPointF& pt, const QPointF& /*strokeBegin*/) { return project(pt); } void EllipseAssistant::drawAssistant(QPainter& gc, const QRectF& updateRect, const KisCoordinatesConverter* converter, bool cached, KisCanvas2* canvas, bool assistantVisible, bool previewVisible) { gc.save(); gc.resetTransform(); QPoint mousePos; if (canvas){ //simplest, cheapest way to get the mouse-position// mousePos= canvas->canvasWidget()->mapFromGlobal(QCursor::pos()); } else { //...of course, you need to have access to a canvas-widget for that.// mousePos = QCursor::pos();//this'll give an offset// dbgFile<<"canvas does not exist in the ellipse assistant, you may have passed arguments incorrectly:"<documentToWidgetTransform(); if (isSnappingActive() && boundingRect().contains(initialTransform.inverted().map(mousePos), false) && previewVisible==true){ if (isAssistantComplete()){ if (e.set(*handles()[0], *handles()[1], *handles()[2])) { // valid ellipse gc.setTransform(initialTransform); gc.setTransform(e.getInverse(), true); QPainterPath path; //path.moveTo(QPointF(-e.semiMajor(), 0)); path.lineTo(QPointF(e.semiMajor(), 0)); //path.moveTo(QPointF(0, -e.semiMinor())); path.lineTo(QPointF(0, e.semiMinor())); // Draw the ellipse path.addEllipse(QPointF(0, 0), e.semiMajor(), e.semiMinor()); drawPreview(gc, path); } } } gc.restore(); KisPaintingAssistant::drawAssistant(gc, updateRect, converter, cached, canvas, assistantVisible, previewVisible); } void EllipseAssistant::drawCache(QPainter& gc, const KisCoordinatesConverter *converter, bool assistantVisible) { if (assistantVisible == false || handles().size() < 2){ return; } QTransform initialTransform = converter->documentToWidgetTransform(); if (handles().size() == 2) { // just draw the axis gc.setTransform(initialTransform); QPainterPath path; path.moveTo(*handles()[0]); path.lineTo(*handles()[1]); drawPath(gc, path, isSnappingActive()); return; } if (e.set(*handles()[0], *handles()[1], *handles()[2])) { // valid ellipse gc.setTransform(initialTransform); gc.setTransform(e.getInverse(), true); QPainterPath path; path.moveTo(QPointF(-e.semiMajor(), 0)); path.lineTo(QPointF(e.semiMajor(), 0)); path.moveTo(QPointF(0, -e.semiMinor())); path.lineTo(QPointF(0, e.semiMinor())); // Draw the ellipse path.addEllipse(QPointF(0, 0), e.semiMajor(), e.semiMinor()); drawPath(gc, path, isSnappingActive()); } } QRect EllipseAssistant::boundingRect() const { if (!isAssistantComplete()) { return KisPaintingAssistant::boundingRect(); } if (e.set(*handles()[0], *handles()[1], *handles()[2])) { return e.boundingRect().adjusted(-2, -2, 2, 2).toAlignedRect(); } else { return QRect(); } } QPointF EllipseAssistant::getEditorPosition() const { return (*handles()[0] + *handles()[1]) * 0.5; } bool EllipseAssistant::isAssistantComplete() const { return handles().size() >= 3; } EllipseAssistantFactory::EllipseAssistantFactory() { } EllipseAssistantFactory::~EllipseAssistantFactory() { } QString EllipseAssistantFactory::id() const { return "ellipse"; } QString EllipseAssistantFactory::name() const { return i18n("Ellipse"); } KisPaintingAssistant* EllipseAssistantFactory::createPaintingAssistant() const { return new EllipseAssistant; } diff --git a/plugins/assistants/Assistants/FisheyePointAssistant.cc b/plugins/assistants/Assistants/FisheyePointAssistant.cc index 8338f4b4ab..a5c0510289 100644 --- a/plugins/assistants/Assistants/FisheyePointAssistant.cc +++ b/plugins/assistants/Assistants/FisheyePointAssistant.cc @@ -1,238 +1,239 @@ /* * Copyright (c) 2008 Cyrille Berger * Copyright (c) 2010 Geoffry Song * Copyright (c) 2014 Wolthera van Hövell tot Westerflier * Copyright (c) 2017 Scott Petrovic * * 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 of the License, or * (at your option) any later version. * * 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 "FisheyePointAssistant.h" #include "kis_debug.h" #include #include +#include #include #include #include #include #include #include #include FisheyePointAssistant::FisheyePointAssistant() : KisPaintingAssistant("fisheye-point", i18n("Fish Eye Point assistant")) { } FisheyePointAssistant::FisheyePointAssistant(const FisheyePointAssistant &rhs, QMap &handleMap) : KisPaintingAssistant(rhs, handleMap) , e(rhs.e) , extraE(rhs.extraE) { } KisPaintingAssistantSP FisheyePointAssistant::clone(QMap &handleMap) const { return KisPaintingAssistantSP(new FisheyePointAssistant(*this, handleMap)); } QPointF FisheyePointAssistant::project(const QPointF& pt, const QPointF& strokeBegin) { const static QPointF nullPoint(std::numeric_limits::quiet_NaN(), std::numeric_limits::quiet_NaN()); Q_ASSERT(isAssistantComplete()); e.set(*handles()[0], *handles()[1], *handles()[2]); qreal dx = pt.x() - strokeBegin.x(), dy = pt.y() - strokeBegin.y(); if (dx * dx + dy * dy < 4.0) { // allow some movement before snapping return strokeBegin; } //set the extrapolation ellipse. if (e.set(*handles()[0], *handles()[1], *handles()[2])){ QLineF radius(*handles()[1], *handles()[0]); radius.setAngle(fmod(radius.angle()+180.0,360.0)); QLineF radius2(*handles()[0], *handles()[1]); radius2.setAngle(fmod(radius2.angle()+180.0,360.0)); if ( extraE.set(*handles()[0], *handles()[1],strokeBegin ) ) { return extraE.project(pt); } else if (extraE.set(radius.p1(), radius.p2(),strokeBegin)) { return extraE.project(pt); } else if (extraE.set(radius2.p1(), radius2.p2(),strokeBegin)){ return extraE.project(pt); } } return nullPoint; } QPointF FisheyePointAssistant::adjustPosition(const QPointF& pt, const QPointF& strokeBegin) { return project(pt, strokeBegin); } void FisheyePointAssistant::drawAssistant(QPainter& gc, const QRectF& updateRect, const KisCoordinatesConverter* converter, bool cached, KisCanvas2* canvas, bool assistantVisible, bool previewVisible) { gc.save(); gc.resetTransform(); QPointF mousePos(0,0); if (canvas){ //simplest, cheapest way to get the mouse-position// mousePos= canvas->canvasWidget()->mapFromGlobal(QCursor::pos()); } else { //...of course, you need to have access to a canvas-widget for that.// mousePos = QCursor::pos();//this'll give an offset// dbgFile<<"canvas does not exist in ruler, you may have passed arguments incorrectly:"<documentToWidgetTransform(); if (isSnappingActive() && previewVisible == true ) { if (isAssistantComplete()){ if (e.set(*handles()[0], *handles()[1], *handles()[2])) { if (extraE.set(*handles()[0], *handles()[1], initialTransform.inverted().map(mousePos))){ gc.setTransform(initialTransform); gc.setTransform(e.getInverse(), true); QPainterPath path; // Draw the ellipse path.addEllipse(QPointF(0, 0), extraE.semiMajor(), extraE.semiMinor()); drawPreview(gc, path); } QLineF radius(*handles()[1], *handles()[0]); radius.setAngle(fmod(radius.angle()+180.0,360.0)); if (extraE.set(radius.p1(), radius.p2(), initialTransform.inverted().map(mousePos))){ gc.setTransform(initialTransform); gc.setTransform(extraE.getInverse(), true); QPainterPath path; // Draw the ellipse path.addEllipse(QPointF(0, 0), extraE.semiMajor(), extraE.semiMinor()); drawPreview(gc, path); } QLineF radius2(*handles()[0], *handles()[1]); radius2.setAngle(fmod(radius2.angle()+180.0,360.0)); if (extraE.set(radius2.p1(), radius2.p2(), initialTransform.inverted().map(mousePos))){ gc.setTransform(initialTransform); gc.setTransform(extraE.getInverse(), true); QPainterPath path; // Draw the ellipse path.addEllipse(QPointF(0, 0), extraE.semiMajor(), extraE.semiMinor()); drawPreview(gc, path); } } } } gc.restore(); KisPaintingAssistant::drawAssistant(gc, updateRect, converter, cached, canvas, assistantVisible, previewVisible); } void FisheyePointAssistant::drawCache(QPainter& gc, const KisCoordinatesConverter *converter, bool assistantVisible) { if (assistantVisible == false){ return; } QTransform initialTransform = converter->documentToWidgetTransform(); if (handles().size() == 2) { // just draw the axis gc.setTransform(initialTransform); QPainterPath path; path.moveTo(*handles()[0]); path.lineTo(*handles()[1]); drawPath(gc, path, isSnappingActive()); return; } if (e.set(*handles()[0], *handles()[1], *handles()[2])) { // valid ellipse gc.setTransform(initialTransform); gc.setTransform(e.getInverse(), true); QPainterPath path; //path.moveTo(QPointF(-e.semiMajor(), -e.semiMinor())); path.lineTo(QPointF(e.semiMajor(), -e.semiMinor())); path.moveTo(QPointF(-e.semiMajor(), -e.semiMinor())); path.lineTo(QPointF(-e.semiMajor(), e.semiMinor())); //path.moveTo(QPointF(-e.semiMajor(), e.semiMinor())); path.lineTo(QPointF(e.semiMajor(), e.semiMinor())); path.moveTo(QPointF(e.semiMajor(), -e.semiMinor())); path.lineTo(QPointF(e.semiMajor(), e.semiMinor())); path.moveTo(QPointF(-(e.semiMajor()*3), -e.semiMinor())); path.lineTo(QPointF(-(e.semiMajor()*3), e.semiMinor())); path.moveTo(QPointF((e.semiMajor()*3), -e.semiMinor())); path.lineTo(QPointF((e.semiMajor()*3), e.semiMinor())); path.moveTo(QPointF(-e.semiMajor(), 0)); path.lineTo(QPointF(e.semiMajor(), 0)); //path.moveTo(QPointF(0, -e.semiMinor())); path.lineTo(QPointF(0, e.semiMinor())); // Draw the ellipse path.addEllipse(QPointF(0, 0), e.semiMajor(), e.semiMinor()); drawPath(gc, path, isSnappingActive()); } } QRect FisheyePointAssistant::boundingRect() const { if (!isAssistantComplete()) { return KisPaintingAssistant::boundingRect(); } if (e.set(*handles()[0], *handles()[1], *handles()[2])) { return e.boundingRect().adjusted(-(e.semiMajor()*2), -2, (e.semiMajor()*2), 2).toAlignedRect(); } else { return QRect(); } } QPointF FisheyePointAssistant::getEditorPosition() const { return (*handles()[0] + *handles()[1]) * 0.5; } bool FisheyePointAssistant::isAssistantComplete() const { return handles().size() >= 3; } FisheyePointAssistantFactory::FisheyePointAssistantFactory() { } FisheyePointAssistantFactory::~FisheyePointAssistantFactory() { } QString FisheyePointAssistantFactory::id() const { return "fisheye-point"; } QString FisheyePointAssistantFactory::name() const { return i18n("Fish Eye Point"); } KisPaintingAssistant* FisheyePointAssistantFactory::createPaintingAssistant() const { return new FisheyePointAssistant; } diff --git a/plugins/assistants/Assistants/InfiniteRulerAssistant.cc b/plugins/assistants/Assistants/InfiniteRulerAssistant.cc index 4fe1907137..03da329b38 100644 --- a/plugins/assistants/Assistants/InfiniteRulerAssistant.cc +++ b/plugins/assistants/Assistants/InfiniteRulerAssistant.cc @@ -1,171 +1,172 @@ /* * Copyright (c) 2008 Cyrille Berger * Copyright (c) 2010 Geoffry Song * Copyright (c) 2014 Wolthera van Hövell tot Westerflier * Copyright (c) 2017 Scott Petrovic * * 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 of the License, or * (at your option) any later version. * * 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 "InfiniteRulerAssistant.h" #include "kis_debug.h" #include #include +#include #include #include #include #include #include #include InfiniteRulerAssistant::InfiniteRulerAssistant() : KisPaintingAssistant("infinite ruler", i18n("Infinite Ruler assistant")) { } InfiniteRulerAssistant::InfiniteRulerAssistant(const InfiniteRulerAssistant &rhs, QMap &handleMap) : KisPaintingAssistant(rhs, handleMap) { } KisPaintingAssistantSP InfiniteRulerAssistant::clone(QMap &handleMap) const { return KisPaintingAssistantSP(new InfiniteRulerAssistant(*this, handleMap)); } QPointF InfiniteRulerAssistant::project(const QPointF& pt, const QPointF& strokeBegin) { Q_ASSERT(isAssistantComplete()); //code nicked from the perspective ruler. qreal dx = pt.x() - strokeBegin.x(), dy = pt.y() - strokeBegin.y(); if (dx * dx + dy * dy < 4.0) { // allow some movement before snapping return strokeBegin; } //dbgKrita<canvasWidget()->mapFromGlobal(QCursor::pos()); } else { //...of course, you need to have access to a canvas-widget for that.// mousePos = QCursor::pos();//this'll give an offset// dbgFile<<"canvas does not exist in ruler, you may have passed arguments incorrectly:"<documentToWidgetTransform(); QLineF snapLine= QLineF(initialTransform.map(*handles()[0]), initialTransform.map(*handles()[1])); QRect viewport= gc.viewport(); KisAlgebra2D::intersectLineRect(snapLine, viewport); QPainterPath path; path.moveTo(snapLine.p1()); path.lineTo(snapLine.p2()); drawPreview(gc, path);//and we draw the preview. } gc.restore(); KisPaintingAssistant::drawAssistant(gc, updateRect, converter, cached, canvas, assistantVisible, previewVisible); } void InfiniteRulerAssistant::drawCache(QPainter& gc, const KisCoordinatesConverter *converter, bool assistantVisible) { if (assistantVisible == false || !isAssistantComplete()){ return; } QTransform initialTransform = converter->documentToWidgetTransform(); // Draw the line QPointF p1 = *handles()[0]; QPointF p2 = *handles()[1]; gc.setTransform(initialTransform); QPainterPath path; path.moveTo(p1); path.lineTo(p2); drawPath(gc, path, isSnappingActive()); } QPointF InfiniteRulerAssistant::getEditorPosition() const { return (*handles()[0]); } bool InfiniteRulerAssistant::isAssistantComplete() const { return handles().size() >= 2; } InfiniteRulerAssistantFactory::InfiniteRulerAssistantFactory() { } InfiniteRulerAssistantFactory::~InfiniteRulerAssistantFactory() { } QString InfiniteRulerAssistantFactory::id() const { return "infinite ruler"; } QString InfiniteRulerAssistantFactory::name() const { return i18n("Infinite Ruler"); } KisPaintingAssistant* InfiniteRulerAssistantFactory::createPaintingAssistant() const { return new InfiniteRulerAssistant; } diff --git a/plugins/assistants/Assistants/ParallelRulerAssistant.cc b/plugins/assistants/Assistants/ParallelRulerAssistant.cc index 2deb3d3ab2..405a172e59 100644 --- a/plugins/assistants/Assistants/ParallelRulerAssistant.cc +++ b/plugins/assistants/Assistants/ParallelRulerAssistant.cc @@ -1,178 +1,179 @@ /* * Copyright (c) 2008 Cyrille Berger * Copyright (c) 2010 Geoffry Song * Copyright (c) 2014 Wolthera van Hövell tot Westerflier * Copyright (c) 2017 Scott Petrovic * * 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 of the License, or * (at your option) any later version. * * 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 "ParallelRulerAssistant.h" #include "kis_debug.h" #include #include +#include #include #include #include #include #include #include ParallelRulerAssistant::ParallelRulerAssistant() : KisPaintingAssistant("parallel ruler", i18n("Parallel Ruler assistant")) { } KisPaintingAssistantSP ParallelRulerAssistant::clone(QMap &handleMap) const { return KisPaintingAssistantSP(new ParallelRulerAssistant(*this, handleMap)); } ParallelRulerAssistant::ParallelRulerAssistant(const ParallelRulerAssistant &rhs, QMap &handleMap) : KisPaintingAssistant(rhs, handleMap) { } QPointF ParallelRulerAssistant::project(const QPointF& pt, const QPointF& strokeBegin) { Q_ASSERT(isAssistantComplete()); //code nicked from the perspective ruler. qreal dx = pt.x() - strokeBegin.x(); qreal dy = pt.y() - strokeBegin.y(); if (dx * dx + dy * dy < 4.0) { return strokeBegin; // allow some movement before snapping } //dbgKrita<canvasWidget()->mapFromGlobal(QCursor::pos()); } else { //...of course, you need to have access to a canvas-widget for that.// mousePos = QCursor::pos();//this'll give an offset// dbgFile<<"canvas does not exist in ruler, you may have passed arguments incorrectly:"<documentToWidgetTransform(); QLineF snapLine= QLineF(initialTransform.map(*handles()[0]), initialTransform.map(*handles()[1])); QPointF translation = (initialTransform.map(*handles()[0])-mousePos)*-1.0; snapLine= snapLine.translated(translation); QRect viewport= gc.viewport(); KisAlgebra2D::intersectLineRect(snapLine, viewport); QPainterPath path; path.moveTo(snapLine.p1()); path.lineTo(snapLine.p2()); drawPreview(gc, path);//and we draw the preview. } gc.restore(); KisPaintingAssistant::drawAssistant(gc, updateRect, converter, cached, canvas, assistantVisible, previewVisible); } void ParallelRulerAssistant::drawCache(QPainter& gc, const KisCoordinatesConverter *converter, bool assistantVisible) { if (assistantVisible == false || !isAssistantComplete()){ return; } QTransform initialTransform = converter->documentToWidgetTransform(); // Draw the line QPointF p1 = *handles()[0]; QPointF p2 = *handles()[1]; gc.setTransform(initialTransform); QPainterPath path; path.moveTo(p1); path.lineTo(p2); drawPath(gc, path, isSnappingActive()); } QPointF ParallelRulerAssistant::getEditorPosition() const { return (*handles()[0] + *handles()[1]) * 0.5; } bool ParallelRulerAssistant::isAssistantComplete() const { return handles().size() >= 2; } ParallelRulerAssistantFactory::ParallelRulerAssistantFactory() { } ParallelRulerAssistantFactory::~ParallelRulerAssistantFactory() { } QString ParallelRulerAssistantFactory::id() const { return "parallel ruler"; } QString ParallelRulerAssistantFactory::name() const { return i18n("Parallel Ruler"); } KisPaintingAssistant* ParallelRulerAssistantFactory::createPaintingAssistant() const { return new ParallelRulerAssistant; } diff --git a/plugins/assistants/Assistants/PerspectiveAssistant.cc b/plugins/assistants/Assistants/PerspectiveAssistant.cc index 46007b5e3c..bcb266997f 100644 --- a/plugins/assistants/Assistants/PerspectiveAssistant.cc +++ b/plugins/assistants/Assistants/PerspectiveAssistant.cc @@ -1,481 +1,482 @@ /* * Copyright (c) 2008 Cyrille Berger * Copyright (c) 2010 Geoffry Song * Copyright (c) 2017 Scott Petrovic * * 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 of the License, or * (at your option) any later version. * * 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 "PerspectiveAssistant.h" #include "kis_debug.h" #include #include +#include #include #include #include #include #include #include #include PerspectiveAssistant::PerspectiveAssistant(QObject *parent) : KisAbstractPerspectiveGrid(parent) , KisPaintingAssistant("perspective", i18n("Perspective assistant")) { } PerspectiveAssistant::PerspectiveAssistant(const PerspectiveAssistant &rhs, QMap &handleMap) : KisAbstractPerspectiveGrid(rhs.parent()) , KisPaintingAssistant(rhs, handleMap) , m_snapLine(rhs.m_snapLine) , m_cachedTransform(rhs.m_cachedTransform) , m_cachedPolygon(rhs.m_cachedPolygon) , m_cacheValid(rhs.m_cacheValid) { for (int i = 0; i < 4; ++i) { m_cachedPoints[i] = rhs.m_cachedPoints[i]; } } KisPaintingAssistantSP PerspectiveAssistant::clone(QMap &handleMap) const { return KisPaintingAssistantSP(new PerspectiveAssistant(*this, handleMap)); } // squared distance from a point to a line inline qreal distsqr(const QPointF& pt, const QLineF& line) { // distance = |(p2 - p1) x (p1 - pt)| / |p2 - p1| // magnitude of (p2 - p1) x (p1 - pt) const qreal cross = (line.dx() * (line.y1() - pt.y()) - line.dy() * (line.x1() - pt.x())); return cross * cross / (line.dx() * line.dx() + line.dy() * line.dy()); } QPointF PerspectiveAssistant::project(const QPointF& pt, const QPointF& strokeBegin) { const static QPointF nullPoint(std::numeric_limits::quiet_NaN(), std::numeric_limits::quiet_NaN()); Q_ASSERT(isAssistantComplete()); if (m_snapLine.isNull()) { QPolygonF poly; QTransform transform; if (!getTransform(poly, transform)) { return nullPoint; } if (!poly.containsPoint(strokeBegin, Qt::OddEvenFill)) { return nullPoint; // avoid problems with multiple assistants: only snap if starting in the grid } const qreal dx = pt.x() - strokeBegin.x(); const qreal dy = pt.y() - strokeBegin.y(); if (dx * dx + dy * dy < 4.0) { return strokeBegin; // allow some movement before snapping } // construct transformation bool invertible; const QTransform inverse = transform.inverted(&invertible); if (!invertible) { return nullPoint; // shouldn't happen } // figure out which direction to go const QPointF start = inverse.map(strokeBegin); const QLineF verticalLine = QLineF(strokeBegin, transform.map(start + QPointF(0, 1))); const QLineF horizontalLine = QLineF(strokeBegin, transform.map(start + QPointF(1, 0))); // determine whether the horizontal or vertical line is closer to the point m_snapLine = distsqr(pt, verticalLine) < distsqr(pt, horizontalLine) ? verticalLine : horizontalLine; } // snap to line const qreal dx = m_snapLine.dx(), dy = m_snapLine.dy(), dx2 = dx * dx, dy2 = dy * dy, invsqrlen = 1.0 / (dx2 + dy2); QPointF r(dx2 * pt.x() + dy2 * m_snapLine.x1() + dx * dy * (pt.y() - m_snapLine.y1()), dx2 * m_snapLine.y1() + dy2 * pt.y() + dx * dy * (pt.x() - m_snapLine.x1())); r *= invsqrlen; return r; } QPointF PerspectiveAssistant::adjustPosition(const QPointF& pt, const QPointF& strokeBegin) { return project(pt, strokeBegin); } void PerspectiveAssistant::endStroke() { m_snapLine = QLineF(); } bool PerspectiveAssistant::contains(const QPointF& pt) const { QPolygonF poly; if (!quad(poly)) return false; return poly.containsPoint(pt, Qt::OddEvenFill); } inline qreal lengthSquared(const QPointF& vector) { return vector.x() * vector.x() + vector.y() * vector.y(); } inline qreal localScale(const QTransform& transform, QPointF pt) { // const qreal epsilon = 1e-5, epsilonSquared = epsilon * epsilon; // qreal xSizeSquared = lengthSquared(transform.map(pt + QPointF(epsilon, 0.0)) - orig) / epsilonSquared; // qreal ySizeSquared = lengthSquared(transform.map(pt + QPointF(0.0, epsilon)) - orig) / epsilonSquared; // xSizeSquared /= lengthSquared(transform.map(QPointF(0.0, pt.y())) - transform.map(QPointF(1.0, pt.y()))); // ySizeSquared /= lengthSquared(transform.map(QPointF(pt.x(), 0.0)) - transform.map(QPointF(pt.x(), 1.0))); // when taking the limit epsilon->0: // xSizeSquared=((m23*y+m33)^2*(m23*y+m33+m13)^2)/(m23*y+m13*x+m33)^4 // ySizeSquared=((m23*y+m33)^2*(m23*y+m33+m13)^2)/(m23*y+m13*x+m33)^4 // xSize*ySize=(abs(m13*x+m33)*abs(m13*x+m33+m23)*abs(m23*y+m33)*abs(m23*y+m33+m13))/(m23*y+m13*x+m33)^4 const qreal x = transform.m13() * pt.x(), y = transform.m23() * pt.y(), a = x + transform.m33(), b = y + transform.m33(), c = x + y + transform.m33(), d = c * c; return fabs(a*(a + transform.m23())*b*(b + transform.m13()))/(d * d); } // returns the reciprocal of the maximum local scale at the points (0,0),(0,1),(1,0),(1,1) inline qreal inverseMaxLocalScale(const QTransform& transform) { const qreal a = fabs((transform.m33() + transform.m13()) * (transform.m33() + transform.m23())), b = fabs((transform.m33()) * (transform.m13() + transform.m33() + transform.m23())), d00 = transform.m33() * transform.m33(), d11 = (transform.m33() + transform.m23() + transform.m13())*(transform.m33() + transform.m23() + transform.m13()), s0011 = qMin(d00, d11) / a, d10 = (transform.m33() + transform.m13()) * (transform.m33() + transform.m13()), d01 = (transform.m33() + transform.m23()) * (transform.m33() + transform.m23()), s1001 = qMin(d10, d01) / b; return qMin(s0011, s1001); } qreal PerspectiveAssistant::distance(const QPointF& pt) const { QPolygonF poly; QTransform transform; if (!getTransform(poly, transform)) { return 1.0; } bool invertible; QTransform inverse = transform.inverted(&invertible); if (!invertible) { return 1.0; } if (inverse.m13() * pt.x() + inverse.m23() * pt.y() + inverse.m33() == 0.0) { return 0.0; // point at infinity } return localScale(transform, inverse.map(pt)) * inverseMaxLocalScale(transform); } // draw a vanishing point marker inline QPainterPath drawX(const QPointF& pt) { QPainterPath path; path.moveTo(QPointF(pt.x() - 5.0, pt.y() - 5.0)); path.lineTo(QPointF(pt.x() + 5.0, pt.y() + 5.0)); path.moveTo(QPointF(pt.x() - 5.0, pt.y() + 5.0)); path.lineTo(QPointF(pt.x() + 5.0, pt.y() - 5.0)); return path; } void PerspectiveAssistant::drawAssistant(QPainter& gc, const QRectF& updateRect, const KisCoordinatesConverter* converter, bool cached, KisCanvas2* canvas, bool assistantVisible, bool previewVisible) { gc.save(); gc.resetTransform(); QTransform initialTransform = converter->documentToWidgetTransform(); //QTransform reverseTransform = converter->widgetToDocument(); QPolygonF poly; QTransform transform; // unused, but computed for caching purposes if (getTransform(poly, transform) && assistantVisible==true) { // draw vanishing points QPointF intersection(0, 0); if (fmod(QLineF(poly[0], poly[1]).angle(), 180.0)>=fmod(QLineF(poly[2], poly[3]).angle(), 180.0)+2.0 || fmod(QLineF(poly[0], poly[1]).angle(), 180.0)<=fmod(QLineF(poly[2], poly[3]).angle(), 180.0)-2.0) { if (QLineF(poly[0], poly[1]).intersect(QLineF(poly[2], poly[3]), &intersection) != QLineF::NoIntersection) { drawPath(gc, drawX(initialTransform.map(intersection))); } } if (fmod(QLineF(poly[1], poly[2]).angle(), 180.0)>=fmod(QLineF(poly[3], poly[0]).angle(), 180.0)+2.0 || fmod(QLineF(poly[1], poly[2]).angle(), 180.0)<=fmod(QLineF(poly[3], poly[0]).angle(), 180.0)-2.0){ if (QLineF(poly[1], poly[2]).intersect(QLineF(poly[3], poly[0]), &intersection) != QLineF::NoIntersection) { drawPath(gc, drawX(initialTransform.map(intersection))); } } } if (isSnappingActive() && getTransform(poly, transform) && previewVisible==true){ //find vanishing point, find mouse, draw line between both. QPainterPath path2; QPointF intersection(0, 0);//this is the position of the vanishing point. QPointF mousePos(0,0); QLineF snapLine; QRect viewport= gc.viewport(); QRect bounds; if (canvas){ //simplest, cheapest way to get the mouse-position mousePos= canvas->canvasWidget()->mapFromGlobal(QCursor::pos()); } else { //...of course, you need to have access to a canvas-widget for that. mousePos = QCursor::pos(); // this'll give an offset dbgFile<<"canvas does not exist, you may have passed arguments incorrectly:"<=fmod(QLineF(poly[2], poly[3]).angle(), 180.0)+2.0 || fmod(QLineF(poly[0], poly[1]).angle(), 180.0)<=fmod(QLineF(poly[2], poly[3]).angle(), 180.0)-2.0) { if (QLineF(poly[0], poly[1]).intersect(QLineF(poly[2], poly[3]), &intersection) != QLineF::NoIntersection) { intersectTransformed = initialTransform.map(intersection); snapLine = QLineF(intersectTransformed, mousePos); KisAlgebra2D::intersectLineRect(snapLine, viewport); bounds= QRect(snapLine.p1().toPoint(), snapLine.p2().toPoint()); QPainterPath path; if (bounds.contains(intersectTransformed.toPoint())){ path2.moveTo(intersectTransformed); path2.lineTo(snapLine.p1()); } else { path2.moveTo(snapLine.p1()); path2.lineTo(snapLine.p2()); } } } if (fmod(QLineF(poly[1], poly[2]).angle(), 180.0)>=fmod(QLineF(poly[3], poly[0]).angle(), 180.0)+2.0 || fmod(QLineF(poly[1], poly[2]).angle(), 180.0)<=fmod(QLineF(poly[3], poly[0]).angle(), 180.0)-2.0){ if (QLineF(poly[1], poly[2]).intersect(QLineF(poly[3], poly[0]), &intersection) != QLineF::NoIntersection) { intersectTransformed = initialTransform.map(intersection); snapLine = QLineF(intersectTransformed, mousePos); KisAlgebra2D::intersectLineRect(snapLine, viewport); bounds= QRect(snapLine.p1().toPoint(), snapLine.p2().toPoint()); QPainterPath path; if (bounds.contains(intersectTransformed.toPoint())){ path2.moveTo(intersectTransformed); path2.lineTo(snapLine.p1()); } else { path2.moveTo(snapLine.p1()); path2.lineTo(snapLine.p2()); } } } drawPreview(gc, path2); } } gc.restore(); KisPaintingAssistant::drawAssistant(gc, updateRect, converter, cached,canvas, assistantVisible, previewVisible); } void PerspectiveAssistant::drawCache(QPainter& gc, const KisCoordinatesConverter *converter, bool assistantVisible) { if (assistantVisible == false) { return; } gc.setTransform(converter->documentToWidgetTransform()); QPolygonF poly; QTransform transform; if (!getTransform(poly, transform)) { // color red for an invalid transform, but not for an incomplete one if(isAssistantComplete()) { gc.setPen(QColor(255, 0, 0, 125)); gc.drawPolygon(poly); } else { QPainterPath path; path.addPolygon(poly); drawPath(gc, path, isSnappingActive()); } } else { gc.setPen(QColor(0, 0, 0, 125)); gc.setTransform(transform, true); QPainterPath path; for (int y = 0; y <= 8; ++y) { path.moveTo(QPointF(0.0, y * 0.125)); path.lineTo(QPointF(1.0, y * 0.125)); } for (int x = 0; x <= 8; ++x) { path.moveTo(QPointF(x * 0.125, 0.0)); path.lineTo(QPointF(x * 0.125, 1.0)); } drawPath(gc, path, isSnappingActive()); } } QPointF PerspectiveAssistant::getEditorPosition() const { QPointF centroid(0, 0); for (int i = 0; i < 4; ++i) { centroid += *handles()[i]; } return centroid * 0.25; } template int sign(T a) { return (a > 0) - (a < 0); } // perpendicular dot product inline qreal pdot(const QPointF& a, const QPointF& b) { return a.x() * b.y() - a.y() * b.x(); } bool PerspectiveAssistant::quad(QPolygonF& poly) const { for (int i = 0; i < handles().size(); ++i) { poly.push_back(*handles()[i]); } if (!isAssistantComplete()) { return false; } int sum = 0; int signs[4]; for (int i = 0; i < 4; ++i) { int j = (i == 3) ? 0 : (i + 1); int k = (j == 3) ? 0 : (j + 1); signs[i] = sign(pdot(poly[j] - poly[i], poly[k] - poly[j])); sum += signs[i]; } if (sum == 0) { // complex (crossed) for (int i = 0; i < 4; ++i) { int j = (i == 3) ? 0 : (i + 1); if (signs[i] * signs[j] == -1) { // opposite signs: uncross std::swap(poly[i], poly[j]); return true; } } // okay, maybe it's just a line return false; } else if (sum != 4 && sum != -4) { // concave, or a triangle if (sum == 2 || sum == -2) { // concave, let's return a triangle instead for (int i = 0; i < 4; ++i) { int j = (i == 3) ? 0 : (i + 1); if (signs[i] != sign(sum)) { // wrong sign: drop the inside node poly.remove(j); return false; } } } return false; } // convex return true; } bool PerspectiveAssistant::getTransform(QPolygonF& poly, QTransform& transform) const { if (m_cachedPolygon.size() != 0 && isAssistantComplete()) { for (int i = 0; i <= 4; ++i) { if (i == 4) { poly = m_cachedPolygon; transform = m_cachedTransform; return m_cacheValid; } if (m_cachedPoints[i] != *handles()[i]) break; } } m_cachedPolygon.clear(); m_cacheValid = false; if (!quad(poly)) { m_cachedPolygon = poly; return false; } if (!QTransform::squareToQuad(poly, transform)) { qWarning("Failed to create perspective mapping"); return false; } for (int i = 0; i < 4; ++i) { m_cachedPoints[i] = *handles()[i]; } m_cachedPolygon = poly; m_cachedTransform = transform; m_cacheValid = true; return true; } bool PerspectiveAssistant::isAssistantComplete() const { return handles().size() >= 4; // specify 4 corners to make assistant complete } PerspectiveAssistantFactory::PerspectiveAssistantFactory() { } PerspectiveAssistantFactory::~PerspectiveAssistantFactory() { } QString PerspectiveAssistantFactory::id() const { return "perspective"; } QString PerspectiveAssistantFactory::name() const { return i18n("Perspective"); } KisPaintingAssistant* PerspectiveAssistantFactory::createPaintingAssistant() const { return new PerspectiveAssistant; } diff --git a/plugins/assistants/Assistants/RulerAssistant.cc b/plugins/assistants/Assistants/RulerAssistant.cc index 48cc15e6a2..64f1772185 100644 --- a/plugins/assistants/Assistants/RulerAssistant.cc +++ b/plugins/assistants/Assistants/RulerAssistant.cc @@ -1,177 +1,178 @@ /* * Copyright (c) 2008 Cyrille Berger * Copyright (c) 2017 Scott Petrovic * * 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 of the License, or * (at your option) any later version. * * 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 "RulerAssistant.h" #include "kis_debug.h" #include #include +#include #include #include #include #include #include RulerAssistant::RulerAssistant() : KisPaintingAssistant("ruler", i18n("Ruler assistant")) { } KisPaintingAssistantSP RulerAssistant::clone(QMap &handleMap) const { return KisPaintingAssistantSP(new RulerAssistant(*this, handleMap)); } RulerAssistant::RulerAssistant(const RulerAssistant &rhs, QMap &handleMap) : KisPaintingAssistant(rhs, handleMap) { } QPointF RulerAssistant::project(const QPointF& pt) const { Q_ASSERT(isAssistantComplete()); QPointF pt1 = *handles()[0]; QPointF pt2 = *handles()[1]; QPointF a = pt - pt1; QPointF u = pt2 - pt1; qreal u_norm = sqrt(u.x() * u.x() + u.y() * u.y()); if(u_norm == 0) return pt; u /= u_norm; double t = a.x() * u.x() + a.y() * u.y(); if(t < 0.0) return pt1; if(t > u_norm) return pt2; return t * u + pt1; } QPointF RulerAssistant::adjustPosition(const QPointF& pt, const QPointF& /*strokeBegin*/) { return project(pt); } inline double angle(const QPointF& p1, const QPointF& p2) { return atan2(p2.y() - p1.y(), p2.x() - p1.x()); } inline double norm2(const QPointF& p) { return sqrt(p.x() * p.x() + p.y() * p.y()); } void RulerAssistant::drawAssistant(QPainter& gc, const QRectF& updateRect, const KisCoordinatesConverter* converter, bool cached, KisCanvas2* canvas, bool assistantVisible, bool previewVisible) { gc.save(); gc.resetTransform(); QPointF mousePos; if (canvas){ //simplest, cheapest way to get the mouse-position// mousePos= canvas->canvasWidget()->mapFromGlobal(QCursor::pos()); } else { //...of course, you need to have access to a canvas-widget for that.// mousePos = QCursor::pos();//this'll give an offset// dbgFile<<"canvas does not exist in ruler, you may have passed arguments incorrectly:"<documentToWidgetTransform(); // first we find the path that our point create. QPointF p1 = *handles()[0]; QPointF p2 = *handles()[1]; gc.setTransform(initialTransform); QPainterPath path; path.moveTo(p1); path.lineTo(p2); //then we use this path to check the bounding rectangle// if (isSnappingActive() && path.boundingRect().contains(initialTransform.inverted().map(mousePos)) && previewVisible==true){ drawPreview(gc, path);//and we draw the preview. } } gc.restore(); KisPaintingAssistant::drawAssistant(gc, updateRect, converter, cached, canvas, assistantVisible, previewVisible); } void RulerAssistant::drawCache(QPainter& gc, const KisCoordinatesConverter *converter, bool assistantVisible) { if (assistantVisible == false || !isAssistantComplete()){ return; } QTransform initialTransform = converter->documentToWidgetTransform(); // Draw the line QPointF p1 = *handles()[0]; QPointF p2 = *handles()[1]; gc.setTransform(initialTransform); QPainterPath path; path.moveTo(p1); path.lineTo(p2); drawPath(gc, path, isSnappingActive()); } QPointF RulerAssistant::getEditorPosition() const { return (*handles()[0] + *handles()[1]) * 0.5; } bool RulerAssistant::isAssistantComplete() const { return handles().size() >= 2; } RulerAssistantFactory::RulerAssistantFactory() { } RulerAssistantFactory::~RulerAssistantFactory() { } QString RulerAssistantFactory::id() const { return "ruler"; } QString RulerAssistantFactory::name() const { return i18n("Ruler"); } KisPaintingAssistant* RulerAssistantFactory::createPaintingAssistant() const { return new RulerAssistant; } diff --git a/plugins/assistants/Assistants/SplineAssistant.cc b/plugins/assistants/Assistants/SplineAssistant.cc index 20b19dda6e..dfbd87227e 100644 --- a/plugins/assistants/Assistants/SplineAssistant.cc +++ b/plugins/assistants/Assistants/SplineAssistant.cc @@ -1,239 +1,240 @@ /* * Copyright (c) 2008 Cyrille Berger * Copyright (c) 2010 Geoffry Song * Copyright (c) 2017 Scott Petrovic * * 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 of the License, or * (at your option) any later version. * * 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 "SplineAssistant.h" #include #include +#include #include #include #include #include #include "kis_debug.h" #include #include #include SplineAssistant::SplineAssistant() : KisPaintingAssistant("spline", i18n("Spline assistant")) { } SplineAssistant::SplineAssistant(const SplineAssistant &rhs, QMap &handleMap) : KisPaintingAssistant(rhs, handleMap) , m_canvas(rhs.m_canvas) { } KisPaintingAssistantSP SplineAssistant::clone(QMap &handleMap) const { return KisPaintingAssistantSP(new SplineAssistant(*this, handleMap)); } // parametric form of a cubic spline (B(t) = (1-t)^3 P0 + 3 (1-t)^2 t P1 + 3 (1-t) t^2 P2 + t^3 P3) inline QPointF B(qreal t, const QPointF& P0, const QPointF& P1, const QPointF& P2, const QPointF& P3) { const qreal tp = 1 - t; const qreal tp2 = tp * tp; const qreal t2 = t * t; return ( tp2 * tp) * P0 + (3 * tp2 * t ) * P1 + (3 * tp * t2) * P2 + ( t * t2) * P3; } // squared distance from a point on the spline to given point: we want to minimize this inline qreal D(qreal t, const QPointF& P0, const QPointF& P1, const QPointF& P2, const QPointF& P3, const QPointF& p) { const qreal tp = 1 - t, tp2 = tp * tp, t2 = t * t, a = tp2 * tp, b = 3 * tp2 * t, c = 3 * tp * t2, d = t * t2, x_dist = a*P0.x() + b*P1.x() + c*P2.x() + d*P3.x() - p.x(), y_dist = a*P0.y() + b*P1.y() + c*P2.y() + d*P3.y() - p.y(); return x_dist * x_dist + y_dist * y_dist; } QPointF SplineAssistant::project(const QPointF& pt) const { Q_ASSERT(isAssistantComplete()); // minimize d(t), but keep t in the same neighbourhood as before (unless starting a new stroke) // (this is a rather inefficient method) qreal min_t = std::numeric_limits::max(); qreal d_min_t = std::numeric_limits::max(); for (qreal t = 0; t <= 1; t += 1e-3) { qreal d_t = D(t, *handles()[0], *handles()[2], *handles()[3], *handles()[1], pt); if (d_t < d_min_t) { d_min_t = d_t; min_t = t; } } return B(min_t, *handles()[0], *handles()[2], *handles()[3], *handles()[1]); } QPointF SplineAssistant::adjustPosition(const QPointF& pt, const QPointF& /*strokeBegin*/) { return project(pt); } void SplineAssistant::drawAssistant(QPainter& gc, const QRectF& updateRect, const KisCoordinatesConverter* converter, bool cached, KisCanvas2* canvas, bool assistantVisible, bool previewVisible) { gc.save(); gc.resetTransform(); QPoint mousePos; if (canvas){ //simplest, cheapest way to get the mouse-position// mousePos= canvas->canvasWidget()->mapFromGlobal(QCursor::pos()); m_canvas = canvas; } else { //...of course, you need to have access to a canvas-widget for that.// mousePos = QCursor::pos();//this'll give an offset// dbgFile<<"canvas does not exist in spline, you may have passed arguments incorrectly:"< 1) { QTransform initialTransform = converter->documentToWidgetTransform(); // first we find the path that our point create. QPointF pts[4]; pts[0] = *handles()[0]; pts[1] = *handles()[1]; pts[2] = (handles().size() >= 3) ? (*handles()[2]) : (*handles()[0]); pts[3] = (handles().size() >= 4) ? (*handles()[3]) : (handles().size() >= 3) ? (*handles()[2]) : (*handles()[1]); gc.setTransform(initialTransform); // Draw the spline QPainterPath path; path.moveTo(pts[0]); path.cubicTo(pts[2], pts[3], pts[1]); //then we use this path to check the bounding rectangle// if (isSnappingActive() && path.boundingRect().contains(initialTransform.inverted().map(mousePos)) && previewVisible==true){ drawPreview(gc, path);//and we draw the preview. } } gc.restore(); // there is some odd rectangle that is getting rendered when there is only one point, so don't start rendering the line until after 2 // this issue only exists with this spline assistant...none of the others if (handles().size() > 2) { KisPaintingAssistant::drawAssistant(gc, updateRect, converter, cached, canvas, assistantVisible, previewVisible); } } void SplineAssistant::drawCache(QPainter& gc, const KisCoordinatesConverter *converter, bool assistantVisible) { if (assistantVisible == false || handles().size() < 2 ){ return; } QTransform initialTransform = converter->documentToWidgetTransform(); QPointF pts[4]; pts[0] = *handles()[0]; pts[1] = *handles()[1]; pts[2] = (handles().size() >= 3) ? (*handles()[2]) : (*handles()[0]); pts[3] = (handles().size() >= 4) ? (*handles()[3]) : (handles().size() >= 3) ? (*handles()[2]) : (*handles()[1]); gc.setTransform(initialTransform); { // Draw bezier handles control lines only if we are editing the assistant gc.save(); QColor assistantColor = effectiveAssistantColor(); QPen bezierlinePen(assistantColor); bezierlinePen.setStyle(Qt::DotLine); bezierlinePen.setWidth(1); if (m_canvas->paintingAssistantsDecoration()->isEditingAssistants()) { if (!isSnappingActive()) { QColor snappingColor = assistantColor; snappingColor.setAlpha(snappingColor.alpha() * 0.2); bezierlinePen.setColor(snappingColor); } gc.setPen(bezierlinePen); gc.drawLine(pts[0], pts[2]); if (isAssistantComplete()) { gc.drawLine(pts[1], pts[3]); } gc.setPen(QColor(0, 0, 0, 125)); } gc.restore(); } // Draw the spline QPainterPath path; path.moveTo(pts[0]); path.cubicTo(pts[2], pts[3], pts[1]); drawPath(gc, path, isSnappingActive()); } QPointF SplineAssistant::getEditorPosition() const { return B(0.5, *handles()[0], *handles()[2], *handles()[3], *handles()[1]); } bool SplineAssistant::isAssistantComplete() const { return handles().size() >= 4; // specify 4 corners to make assistant complete } SplineAssistantFactory::SplineAssistantFactory() { } SplineAssistantFactory::~SplineAssistantFactory() { } QString SplineAssistantFactory::id() const { return "spline"; } QString SplineAssistantFactory::name() const { return i18n("Spline"); } KisPaintingAssistant* SplineAssistantFactory::createPaintingAssistant() const { return new SplineAssistant; } diff --git a/plugins/assistants/Assistants/VanishingPointAssistant.cc b/plugins/assistants/Assistants/VanishingPointAssistant.cc index 3cae70dd14..dbc66be579 100644 --- a/plugins/assistants/Assistants/VanishingPointAssistant.cc +++ b/plugins/assistants/Assistants/VanishingPointAssistant.cc @@ -1,318 +1,319 @@ /* * Copyright (c) 2008 Cyrille Berger * Copyright (c) 2010 Geoffry Song * Copyright (c) 2014 Wolthera van Hövell tot Westerflier * Copyright (c) 2017 Scott Petrovic * * 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 of the License, or * (at your option) any later version. * * 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 "VanishingPointAssistant.h" #include "kis_debug.h" #include #include +#include #include #include #include #include #include #include #include VanishingPointAssistant::VanishingPointAssistant() : KisPaintingAssistant("vanishing point", i18n("Vanishing Point assistant")) { } VanishingPointAssistant::VanishingPointAssistant(const VanishingPointAssistant &rhs, QMap &handleMap) : KisPaintingAssistant(rhs, handleMap) , m_canvas(rhs.m_canvas) , m_referenceLineDensity(rhs.m_referenceLineDensity) { } KisPaintingAssistantSP VanishingPointAssistant::clone(QMap &handleMap) const { return KisPaintingAssistantSP(new VanishingPointAssistant(*this, handleMap)); } QPointF VanishingPointAssistant::project(const QPointF& pt, const QPointF& strokeBegin) { //Q_ASSERT(handles().size() == 1 || handles().size() == 5); //code nicked from the perspective ruler. qreal dx = pt.x() - strokeBegin.x(); qreal dy = pt.y() - strokeBegin.y(); if (dx * dx + dy * dy < 4.0) { // allow some movement before snapping return strokeBegin; } //dbgKrita<canvasWidget()->mapFromGlobal(QCursor::pos()); m_canvas = canvas; } else { //...of course, you need to have access to a canvas-widget for that.// mousePos = QCursor::pos();//this'll give an offset// dbgFile<<"canvas does not exist in ruler, you may have passed arguments incorrectly:"<paintingAssistantsDecoration()->isEditingAssistants() == false && isAssistantComplete()) { if (isSnappingActive() && previewVisible == true) { //don't draw if invalid. QTransform initialTransform = converter->documentToWidgetTransform(); QPointF startPoint = initialTransform.map(*handles()[0]); QLineF snapLine= QLineF(startPoint, mousePos); QRect viewport= gc.viewport(); KisAlgebra2D::intersectLineRect(snapLine, viewport); QRect bounds= QRect(snapLine.p1().toPoint(), snapLine.p2().toPoint()); QPainterPath path; if (bounds.contains(startPoint.toPoint())){ path.moveTo(startPoint); path.lineTo(snapLine.p1()); } else { path.moveTo(snapLine.p1()); path.lineTo(snapLine.p2()); } drawPreview(gc, path);//and we draw the preview. } } // editor specific controls display if (canvas->paintingAssistantsDecoration()->isEditingAssistants()) { // draws a circle around the vanishing point node while editing QTransform initialTransform = converter->documentToWidgetTransform(); QPointF p0 = initialTransform.map(*handles()[0]); // main vanishing point QPointF p1 = initialTransform.map(*sideHandles()[0]); QPointF p2 = initialTransform.map(*sideHandles()[1]); QPointF p3 = initialTransform.map(*sideHandles()[2]); QPointF p4 = initialTransform.map(*sideHandles()[3]); QRectF ellipse = QRectF(QPointF(p0.x() -15, p0.y() -15), QSizeF(30, 30)); QPainterPath pathCenter; pathCenter.addEllipse(ellipse); drawPath(gc, pathCenter, isSnappingActive()); QColor paintingColor = effectiveAssistantColor(); // draw the lines connecting the different nodes QPen penStyle(paintingColor, 2.0, Qt::SolidLine); if (!isSnappingActive()) { QColor snappingColor = paintingColor; snappingColor.setAlpha(snappingColor.alpha() * 0.2); penStyle.setColor(snappingColor); } gc.save(); gc.setPen(penStyle); gc.drawLine(p0, p1); gc.drawLine(p0, p3); gc.drawLine(p1, p2); gc.drawLine(p3, p4); gc.restore(); } // draw references guide for vanishing points at specified density // this is shown as part of the preview, so don't show if preview is off if ( (assistantVisible && canvas->paintingAssistantsDecoration()->outlineVisibility()) && this->isSnappingActive() ) { // cycle through degrees from 0 to 180. We are doing an infinite line, so we don't need to go 360 QTransform initialTransform = converter->documentToWidgetTransform(); QPointF p0 = initialTransform.map(*handles()[0]); // main vanishing point for (int currentAngle=0; currentAngle <= 180; currentAngle = currentAngle + m_referenceLineDensity ) { // determine the correct angle based on the iteration float xPos = cos(currentAngle * M_PI / 180); float yPos = sin(currentAngle * M_PI / 180); QPointF unitAngle; unitAngle.setX(p0.x() + xPos); unitAngle.setY(p0.y() + yPos); // find point QLineF snapLine= QLineF(p0, unitAngle); QRect viewport= gc.viewport(); KisAlgebra2D::intersectLineRect(snapLine, viewport); // make a line from VP center to edge of canvas with that angle QPainterPath path; path.moveTo(snapLine.p1()); path.lineTo(snapLine.p2()); drawPreview(gc, path);//and we draw the preview. } } gc.restore(); KisPaintingAssistant::drawAssistant(gc, updateRect, converter, cached, canvas, assistantVisible, previewVisible); } void VanishingPointAssistant::drawCache(QPainter& gc, const KisCoordinatesConverter *converter, bool assistantVisible) { if (!m_canvas || !isAssistantComplete()) { return; } if (assistantVisible == false || m_canvas->paintingAssistantsDecoration()->isEditingAssistants()) { return; } QTransform initialTransform = converter->documentToWidgetTransform(); QPointF p0 = initialTransform.map(*handles()[0]); // draws an "X" QPainterPath path; path.moveTo(QPointF(p0.x() - 10.0, p0.y() - 10.0)); path.lineTo(QPointF(p0.x() + 10.0, p0.y() + 10.0)); path.moveTo(QPointF(p0.x() - 10.0, p0.y() + 10.0)); path.lineTo(QPointF(p0.x() + 10.0, p0.y() - 10.0)); drawPath(gc, path, isSnappingActive()); } QPointF VanishingPointAssistant::getEditorPosition() const { return (*handles()[0]); } void VanishingPointAssistant::setReferenceLineDensity(float value) { // cannot have less than 1 degree value if (value < 1.0) { value = 1.0; } m_referenceLineDensity = value; } float VanishingPointAssistant::referenceLineDensity() { return m_referenceLineDensity; } bool VanishingPointAssistant::isAssistantComplete() const { return handles().size() > 0; // only need one point to be ready } void VanishingPointAssistant::saveCustomXml(QXmlStreamWriter* xml) { xml->writeStartElement("angleDensity"); xml->writeAttribute("value", KisDomUtils::toString( this->referenceLineDensity())); xml->writeEndElement(); } bool VanishingPointAssistant::loadCustomXml(QXmlStreamReader* xml) { if (xml && xml->name() == "angleDensity") { this->setReferenceLineDensity((float)KisDomUtils::toDouble(xml->attributes().value("value").toString())); } return true; } VanishingPointAssistantFactory::VanishingPointAssistantFactory() { } VanishingPointAssistantFactory::~VanishingPointAssistantFactory() { } QString VanishingPointAssistantFactory::id() const { return "vanishing point"; } QString VanishingPointAssistantFactory::name() const { return i18n("Vanishing Point"); } KisPaintingAssistant* VanishingPointAssistantFactory::createPaintingAssistant() const { return new VanishingPointAssistant; } diff --git a/plugins/assistants/Assistants/kis_assistant_tool.cc b/plugins/assistants/Assistants/kis_assistant_tool.cc index 4cff446818..37ac8eef3a 100644 --- a/plugins/assistants/Assistants/kis_assistant_tool.cc +++ b/plugins/assistants/Assistants/kis_assistant_tool.cc @@ -1,1075 +1,1076 @@ /* * Copyright (c) 2008 Cyrille Berger * Copyright (c) 2010 Geoffry Song * Copyright (c) 2017 Scott Petrovic * * 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 of the License, or * (at your option) any later version. * * 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 #include +#include #include #include #include #include #include #include #include #include #include "kis_dom_utils.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_global.h" #include "VanishingPointAssistant.h" #include "EditAssistantsCommand.h" #include #include KisAssistantTool::KisAssistantTool(KoCanvasBase * canvas) : KisTool(canvas, KisCursor::arrowCursor()), m_canvas(dynamic_cast(canvas)), m_assistantDrag(0), m_newAssistant(0), m_optionsWidget(0) { Q_ASSERT(m_canvas); setObjectName("tool_assistanttool"); } KisAssistantTool::~KisAssistantTool() { } void KisAssistantTool::activate(ToolActivation toolActivation, const QSet &shapes) { KisTool::activate(toolActivation, shapes); m_canvas->paintingAssistantsDecoration()->activateAssistantsEditor(); m_handles = m_canvas->paintingAssistantsDecoration()->handles(); m_handleDrag = 0; m_internalMode = MODE_CREATION; m_assistantHelperYOffset = 10; m_handleSize = 17; m_canvas->paintingAssistantsDecoration()->setHandleSize(m_handleSize); if (m_optionsWidget) { m_canvas->paintingAssistantsDecoration()->deselectAssistant(); updateToolOptionsUI(); } m_canvas->updateCanvas(); } void KisAssistantTool::deactivate() { m_canvas->paintingAssistantsDecoration()->deactivateAssistantsEditor(); m_canvas->updateCanvas(); KisTool::deactivate(); } void KisAssistantTool::beginPrimaryAction(KoPointerEvent *event) { setMode(KisTool::PAINT_MODE); m_origAssistantList = cloneAssistantList(m_canvas->paintingAssistantsDecoration()->assistants()); bool newAssistantAllowed = true; KisPaintingAssistantsDecorationSP canvasDecoration = m_canvas->paintingAssistantsDecoration(); if (m_newAssistant) { m_internalMode = MODE_CREATION; *m_newAssistant->handles().back() = canvasDecoration->snapToGuide(event, QPointF(), false); if (m_newAssistant->handles().size() == m_newAssistant->numHandles()) { addAssistant(); } else { m_newAssistant->addHandle(new KisPaintingAssistantHandle(canvasDecoration->snapToGuide(event, QPointF(), false)), HandleType::NORMAL); } m_canvas->updateCanvas(); return; } m_handleDrag = 0; double minDist = 81.0; QPointF mousePos = m_canvas->viewConverter()->documentToView(canvasDecoration->snapToGuide(event, QPointF(), false));//m_canvas->viewConverter()->documentToView(event->point); // syncs the assistant handles to the handles reference we store in this tool // they can get out of sync with the way the actions and paintevents occur // we probably need to stop storing a reference in m_handles and call the assistants directly m_handles = m_canvas->paintingAssistantsDecoration()->handles(); Q_FOREACH (KisPaintingAssistantSP assistant, m_canvas->paintingAssistantsDecoration()->assistants()) { // find out which handle on all assistants is closest to the mouse position // vanishing points have "side handles", so make sure to include that { QList allAssistantHandles; allAssistantHandles.append(assistant->handles()); allAssistantHandles.append(assistant->sideHandles()); Q_FOREACH (const KisPaintingAssistantHandleSP handle, allAssistantHandles) { double dist = KisPaintingAssistant::norm2(mousePos - m_canvas->viewConverter()->documentToView(*handle)); if (dist < minDist) { minDist = dist; m_handleDrag = handle; assistantSelected(assistant); // whatever handle is the closest contains the selected assistant } } } if(m_handleDrag && assistant->id() == "perspective") { // Look for the handle which was pressed if (m_handleDrag == assistant->topLeft()) { double dist = KisPaintingAssistant::norm2(mousePos - m_canvas->viewConverter()->documentToView(*m_handleDrag)); if (dist < minDist) { minDist = dist; } m_dragStart = QPointF(assistant->topRight().data()->x(),assistant->topRight().data()->y()); m_internalMode = MODE_DRAGGING_NODE; } else if (m_handleDrag == assistant->topRight()) { double dist = KisPaintingAssistant::norm2(mousePos - m_canvas->viewConverter()->documentToView(*m_handleDrag)); if (dist < minDist) { minDist = dist; } m_internalMode = MODE_DRAGGING_NODE; m_dragStart = QPointF(assistant->topLeft().data()->x(),assistant->topLeft().data()->y()); } else if (m_handleDrag == assistant->bottomLeft()) { double dist = KisPaintingAssistant::norm2(mousePos - m_canvas->viewConverter()->documentToView(*m_handleDrag)); if (dist < minDist) { minDist = dist; } m_internalMode = MODE_DRAGGING_NODE; m_dragStart = QPointF(assistant->bottomRight().data()->x(),assistant->bottomRight().data()->y()); } else if (m_handleDrag == assistant->bottomRight()) { double dist = KisPaintingAssistant::norm2(mousePos - m_canvas->viewConverter()->documentToView(*m_handleDrag)); if (dist < minDist) { minDist = dist; } m_internalMode = MODE_DRAGGING_NODE; m_dragStart = QPointF(assistant->bottomLeft().data()->x(),assistant->bottomLeft().data()->y()); } else if (m_handleDrag == assistant->leftMiddle()) { m_internalMode = MODE_DRAGGING_TRANSLATING_TWONODES; m_dragStart = QPointF((assistant->bottomLeft().data()->x()+assistant->topLeft().data()->x())*0.5, (assistant->bottomLeft().data()->y()+assistant->topLeft().data()->y())*0.5); m_selectedNode1 = new KisPaintingAssistantHandle(assistant->topLeft().data()->x(),assistant->topLeft().data()->y()); m_selectedNode2 = new KisPaintingAssistantHandle(assistant->bottomLeft().data()->x(),assistant->bottomLeft().data()->y()); m_newAssistant = toQShared(KisPaintingAssistantFactoryRegistry::instance()->get("perspective")->createPaintingAssistant()); m_newAssistant->addHandle(assistant->topLeft(), HandleType::NORMAL ); m_newAssistant->addHandle(m_selectedNode1, HandleType::NORMAL); m_newAssistant->addHandle(m_selectedNode2, HandleType::NORMAL); m_newAssistant->addHandle(assistant->bottomLeft(), HandleType::NORMAL); m_dragEnd = event->point; m_handleDrag = 0; m_canvas->updateCanvas(); // TODO update only the relevant part of the canvas return; } else if (m_handleDrag == assistant->rightMiddle()) { m_dragStart = QPointF((assistant->topRight().data()->x()+assistant->bottomRight().data()->x())*0.5, (assistant->topRight().data()->y()+assistant->bottomRight().data()->y())*0.5); m_internalMode = MODE_DRAGGING_TRANSLATING_TWONODES; m_selectedNode1 = new KisPaintingAssistantHandle(assistant->topRight().data()->x(),assistant->topRight().data()->y()); m_selectedNode2 = new KisPaintingAssistantHandle(assistant->bottomRight().data()->x(),assistant->bottomRight().data()->y()); m_newAssistant = toQShared(KisPaintingAssistantFactoryRegistry::instance()->get("perspective")->createPaintingAssistant()); m_newAssistant->addHandle(assistant->topRight(), HandleType::NORMAL); m_newAssistant->addHandle(m_selectedNode1, HandleType::NORMAL); m_newAssistant->addHandle(m_selectedNode2, HandleType::NORMAL); m_newAssistant->addHandle(assistant->bottomRight(), HandleType::NORMAL); m_dragEnd = event->point; m_handleDrag = 0; m_canvas->updateCanvas(); // TODO update only the relevant part of the canvas return; } else if (m_handleDrag == assistant->topMiddle()) { m_dragStart = QPointF((assistant->topLeft().data()->x()+assistant->topRight().data()->x())*0.5, (assistant->topLeft().data()->y()+assistant->topRight().data()->y())*0.5); m_internalMode = MODE_DRAGGING_TRANSLATING_TWONODES; m_selectedNode1 = new KisPaintingAssistantHandle(assistant->topLeft().data()->x(),assistant->topLeft().data()->y()); m_selectedNode2 = new KisPaintingAssistantHandle(assistant->topRight().data()->x(),assistant->topRight().data()->y()); m_newAssistant = toQShared(KisPaintingAssistantFactoryRegistry::instance()->get("perspective")->createPaintingAssistant()); m_newAssistant->addHandle(m_selectedNode1, HandleType::NORMAL); m_newAssistant->addHandle(m_selectedNode2, HandleType::NORMAL); m_newAssistant->addHandle(assistant->topRight(), HandleType::NORMAL); m_newAssistant->addHandle(assistant->topLeft(), HandleType::NORMAL); m_dragEnd = event->point; m_handleDrag = 0; m_canvas->updateCanvas(); // TODO update only the relevant part of the canvas return; } else if (m_handleDrag == assistant->bottomMiddle()) { m_dragStart = QPointF((assistant->bottomLeft().data()->x()+assistant->bottomRight().data()->x())*0.5, (assistant->bottomLeft().data()->y()+assistant->bottomRight().data()->y())*0.5); m_internalMode = MODE_DRAGGING_TRANSLATING_TWONODES; m_selectedNode1 = new KisPaintingAssistantHandle(assistant->bottomLeft().data()->x(),assistant->bottomLeft().data()->y()); m_selectedNode2 = new KisPaintingAssistantHandle(assistant->bottomRight().data()->x(),assistant->bottomRight().data()->y()); m_newAssistant = toQShared(KisPaintingAssistantFactoryRegistry::instance()->get("perspective")->createPaintingAssistant()); m_newAssistant->addHandle(assistant->bottomLeft(), HandleType::NORMAL); m_newAssistant->addHandle(assistant->bottomRight(), HandleType::NORMAL); m_newAssistant->addHandle(m_selectedNode2, HandleType::NORMAL); m_newAssistant->addHandle(m_selectedNode1, HandleType::NORMAL); m_dragEnd = event->point; m_handleDrag = 0; m_canvas->updateCanvas(); // TODO update only the relevant part of the canvas return; } m_snapIsRadial = false; } else if (m_handleDrag && assistant->handles().size()>1 && (assistant->id() == "ruler" || assistant->id() == "parallel ruler" || assistant->id() == "infinite ruler" || assistant->id() == "spline")){ if (m_handleDrag == assistant->handles()[0]) { m_dragStart = *assistant->handles()[1]; } else if (m_handleDrag == assistant->handles()[1]) { m_dragStart = *assistant->handles()[0]; } else if(assistant->handles().size()==4){ if (m_handleDrag == assistant->handles()[2]) { m_dragStart = *assistant->handles()[0]; } else if (m_handleDrag == assistant->handles()[3]) { m_dragStart = *assistant->handles()[1]; } } m_snapIsRadial = false; } else if (m_handleDrag && assistant->handles().size()>2 && (assistant->id() == "ellipse" || assistant->id() == "concentric ellipse" || assistant->id() == "fisheye-point")){ m_snapIsRadial = false; if (m_handleDrag == assistant->handles()[0]) { m_dragStart = *assistant->handles()[1]; } else if (m_handleDrag == assistant->handles()[1]) { m_dragStart = *assistant->handles()[0]; } else if (m_handleDrag == assistant->handles()[2]) { m_dragStart = assistant->getEditorPosition(); m_radius = QLineF(m_dragStart, *assistant->handles()[0]); m_snapIsRadial = true; } } else { m_dragStart = assistant->getEditorPosition(); m_snapIsRadial = false; } } if (m_handleDrag) { // TODO: Shift-press should now be handled using the alternate actions // if (event->modifiers() & Qt::ShiftModifier) { // m_handleDrag->uncache(); // m_handleDrag = m_handleDrag->split()[0]; // m_handles = m_canvas->view()->paintingAssistantsDecoration()->handles(); // } m_canvas->updateCanvas(); // TODO update only the relevant part of the canvas return; } m_assistantDrag.clear(); Q_FOREACH (KisPaintingAssistantSP assistant, m_canvas->paintingAssistantsDecoration()->assistants()) { AssistantEditorData editorShared; // shared position data between assistant tool and decoration const KisCoordinatesConverter *converter = m_canvas->coordinatesConverter(); // This code contains the click event behavior. QTransform initialTransform = converter->documentToWidgetTransform(); QPointF actionsPosition = initialTransform.map(assistant->viewportConstrainedEditorPosition(converter, editorShared.boundingSize)); // for UI editor widget controls with move, show, and delete -- disregard document transforms like rotating and mirroring. // otherwise the UI controls get awkward to use when they are at 45 degree angles or the order of controls gets flipped backwards QPointF uiMousePosition = initialTransform.map(canvasDecoration->snapToGuide(event, QPointF(), false)); QPointF iconMovePosition(actionsPosition + editorShared.moveIconPosition); QPointF iconSnapPosition(actionsPosition + editorShared.snapIconPosition); QPointF iconDeletePosition(actionsPosition + editorShared.deleteIconPosition); QRectF deleteRect(iconDeletePosition, QSizeF(editorShared.deleteIconSize, editorShared.deleteIconSize)); QRectF visibleRect(iconSnapPosition, QSizeF(editorShared.snapIconSize, editorShared.snapIconSize)); QRectF moveRect(iconMovePosition, QSizeF(editorShared.moveIconSize, editorShared.moveIconSize)); if (moveRect.contains(uiMousePosition)) { m_assistantDrag = assistant; m_cursorStart = event->point; m_currentAdjustment = QPointF(); m_internalMode = MODE_EDITING; assistantSelected(assistant); // whatever handle is the closest contains the selected assistant return; } if (deleteRect.contains(uiMousePosition)) { removeAssistant(assistant); if(m_canvas->paintingAssistantsDecoration()->assistants().isEmpty()) { m_internalMode = MODE_CREATION; } else m_internalMode = MODE_EDITING; m_canvas->updateCanvas(); return; } if (visibleRect.contains(uiMousePosition)) { newAssistantAllowed = false; assistant->setSnappingActive(!assistant->isSnappingActive()); // toggle assistant->uncache();//this updates the chache of the assistant, very important. assistantSelected(assistant); // whatever handle is the closest contains the selected assistant } } if (newAssistantAllowed==true){//don't make a new assistant when I'm just toogling visibility// QString key = m_options.availableAssistantsComboBox->model()->index( m_options.availableAssistantsComboBox->currentIndex(), 0 ).data(Qt::UserRole).toString(); m_newAssistant = toQShared(KisPaintingAssistantFactoryRegistry::instance()->get(key)->createPaintingAssistant()); m_internalMode = MODE_CREATION; m_newAssistant->addHandle(new KisPaintingAssistantHandle(canvasDecoration->snapToGuide(event, QPointF(), false)), HandleType::NORMAL); if (m_newAssistant->numHandles() <= 1) { addAssistant(); } else { m_newAssistant->addHandle(new KisPaintingAssistantHandle(canvasDecoration->snapToGuide(event, QPointF(), false)), HandleType::NORMAL); } } if (m_newAssistant) { m_newAssistant->setAssistantGlobalColorCache(m_canvas->paintingAssistantsDecoration()->globalAssistantsColor()); } m_canvas->updateCanvas(); } void KisAssistantTool::continuePrimaryAction(KoPointerEvent *event) { KisPaintingAssistantsDecorationSP canvasDecoration = m_canvas->paintingAssistantsDecoration(); if (m_handleDrag) { *m_handleDrag = event->point; //ported from the gradient tool... we need to think about this more in the future. if (event->modifiers() == Qt::ShiftModifier && m_snapIsRadial) { QLineF dragRadius = QLineF(m_dragStart, event->point); dragRadius.setLength(m_radius.length()); *m_handleDrag = dragRadius.p2(); } else if (event->modifiers() == Qt::ShiftModifier ) { QPointF move = snapToClosestAxis(event->point - m_dragStart); *m_handleDrag = m_dragStart + move; } else { *m_handleDrag = canvasDecoration->snapToGuide(event, QPointF(), false); } m_handleDrag->uncache(); m_handleCombine = 0; if (!(event->modifiers() & Qt::ShiftModifier)) { double minDist = 49.0; QPointF mousePos = m_canvas->viewConverter()->documentToView(event->point); Q_FOREACH (const KisPaintingAssistantHandleSP handle, m_handles) { if (handle == m_handleDrag) continue; double dist = KisPaintingAssistant::norm2(mousePos - m_canvas->viewConverter()->documentToView(*handle)); if (dist < minDist) { minDist = dist; m_handleCombine = handle; } } } m_canvas->updateCanvas(); } else if (m_assistantDrag) { QPointF newAdjustment = canvasDecoration->snapToGuide(event, QPointF(), false) - m_cursorStart; if (event->modifiers() == Qt::ShiftModifier ) { newAdjustment = snapToClosestAxis(newAdjustment); } Q_FOREACH (KisPaintingAssistantHandleSP handle, m_assistantDrag->handles()) { *handle += (newAdjustment - m_currentAdjustment); } if (m_assistantDrag->id()== "vanishing point"){ Q_FOREACH (KisPaintingAssistantHandleSP handle, m_assistantDrag->sideHandles()) { *handle += (newAdjustment - m_currentAdjustment); } } m_assistantDrag->uncache(); m_currentAdjustment = newAdjustment; m_canvas->updateCanvas(); } else { event->ignore(); } bool wasHiglightedNode = m_higlightedNode != 0; QPointF mousep = m_canvas->viewConverter()->documentToView(event->point); QList pAssistant= m_canvas->paintingAssistantsDecoration()->assistants(); Q_FOREACH (KisPaintingAssistantSP assistant, pAssistant) { if(assistant->id() == "perspective") { if ((m_higlightedNode = assistant->closestCornerHandleFromPoint(mousep))) { if (m_higlightedNode == m_selectedNode1 || m_higlightedNode == m_selectedNode2) { m_higlightedNode = 0; } else { m_canvas->updateCanvas(); // TODO update only the relevant part of the canvas break; } } } //this following bit sets the translations for the vanishing-point handles. if(m_handleDrag && assistant->id() == "vanishing point" && assistant->sideHandles().size()==4) { //for inner handles, the outer handle gets translated. if (m_handleDrag == assistant->sideHandles()[0]) { QLineF perspectiveline = QLineF(*assistant->handles()[0], *assistant->sideHandles()[0]); qreal length = QLineF(*assistant->sideHandles()[0], *assistant->sideHandles()[1]).length(); if (length < 2.0){ length = 2.0; } length += perspectiveline.length(); perspectiveline.setLength(length); *assistant->sideHandles()[1] = perspectiveline.p2(); } else if (m_handleDrag == assistant->sideHandles()[2]){ QLineF perspectiveline = QLineF(*assistant->handles()[0], *assistant->sideHandles()[2]); qreal length = QLineF(*assistant->sideHandles()[2], *assistant->sideHandles()[3]).length(); if (length<2.0){ length=2.0; } length += perspectiveline.length(); perspectiveline.setLength(length); *assistant->sideHandles()[3] = perspectiveline.p2(); } // for outer handles, only the vanishing point is translated, but only if there's an intersection. else if (m_handleDrag == assistant->sideHandles()[1]|| m_handleDrag == assistant->sideHandles()[3]){ QPointF vanishingpoint(0,0); QLineF perspectiveline = QLineF(*assistant->sideHandles()[0], *assistant->sideHandles()[1]); QLineF perspectiveline2 = QLineF(*assistant->sideHandles()[2], *assistant->sideHandles()[3]); if (QLineF(perspectiveline2).intersect(QLineF(perspectiveline), &vanishingpoint) != QLineF::NoIntersection){ *assistant->handles()[0] = vanishingpoint; } }// and for the vanishing point itself, only the outer handles get translated. else if (m_handleDrag == assistant->handles()[0]){ QLineF perspectiveline = QLineF(*assistant->handles()[0], *assistant->sideHandles()[0]); QLineF perspectiveline2 = QLineF(*assistant->handles()[0], *assistant->sideHandles()[2]); qreal length = QLineF(*assistant->sideHandles()[0], *assistant->sideHandles()[1]).length(); qreal length2 = QLineF(*assistant->sideHandles()[2], *assistant->sideHandles()[3]).length(); if (length < 2.0) { length = 2.0; } if (length2 < 2.0) { length2=2.0; } length += perspectiveline.length(); length2 += perspectiveline2.length(); perspectiveline.setLength(length); perspectiveline2.setLength(length2); *assistant->sideHandles()[1] = perspectiveline.p2(); *assistant->sideHandles()[3] = perspectiveline2.p2(); } } } if (wasHiglightedNode && !m_higlightedNode) { m_canvas->updateCanvas(); // TODO update only the relevant part of the canvas } } void KisAssistantTool::endPrimaryAction(KoPointerEvent *event) { setMode(KisTool::HOVER_MODE); if (m_handleDrag || m_assistantDrag) { if (m_handleDrag) { if (!(event->modifiers() & Qt::ShiftModifier) && m_handleCombine) { m_handleCombine->mergeWith(m_handleDrag); m_handleCombine->uncache(); m_handles = m_canvas->paintingAssistantsDecoration()->handles(); } m_handleDrag = m_handleCombine = 0; } else { m_assistantDrag.clear(); } dbgUI << "creating undo command..."; KUndo2Command *command = new EditAssistantsCommand(m_canvas, m_origAssistantList, cloneAssistantList(m_canvas->paintingAssistantsDecoration()->assistants())); m_canvas->viewManager()->undoAdapter()->addCommand(command); dbgUI << "done"; } else if(m_internalMode == MODE_DRAGGING_TRANSLATING_TWONODES) { addAssistant(); m_internalMode = MODE_CREATION; } else { event->ignore(); } m_canvas->updateCanvas(); // TODO update only the relevant part of the canvas } QList KisAssistantTool::cloneAssistantList(const QList &list) const { QMap handleMap; QList clonedList; dbgUI << "cloning assistants..."; for (auto i = list.begin(); i != list.end(); ++i) { clonedList << (*i)->clone(handleMap); } dbgUI << "done"; return clonedList; } void KisAssistantTool::addAssistant() { m_canvas->paintingAssistantsDecoration()->addAssistant(m_newAssistant); KisAbstractPerspectiveGrid* grid = dynamic_cast(m_newAssistant.data()); if (grid) { m_canvas->viewManager()->canvasResourceProvider()->addPerspectiveGrid(grid); } QList assistants = m_canvas->paintingAssistantsDecoration()->assistants(); KUndo2Command *addAssistantCmd = new EditAssistantsCommand(m_canvas, m_origAssistantList, cloneAssistantList(assistants), EditAssistantsCommand::ADD, assistants.indexOf(m_newAssistant)); m_canvas->viewManager()->undoAdapter()->addCommand(addAssistantCmd); m_handles = m_canvas->paintingAssistantsDecoration()->handles(); m_canvas->paintingAssistantsDecoration()->setSelectedAssistant(m_newAssistant); updateToolOptionsUI(); // vanishing point assistant will get an extra option m_newAssistant.clear(); } void KisAssistantTool::removeAssistant(KisPaintingAssistantSP assistant) { QList assistants = m_canvas->paintingAssistantsDecoration()->assistants(); KisAbstractPerspectiveGrid* grid = dynamic_cast(assistant.data()); if (grid) { m_canvas->viewManager()->canvasResourceProvider()->removePerspectiveGrid(grid); } m_canvas->paintingAssistantsDecoration()->removeAssistant(assistant); KUndo2Command *removeAssistantCmd = new EditAssistantsCommand(m_canvas, m_origAssistantList, cloneAssistantList(m_canvas->paintingAssistantsDecoration()->assistants()), EditAssistantsCommand::REMOVE, assistants.indexOf(assistant)); m_canvas->viewManager()->undoAdapter()->addCommand(removeAssistantCmd); m_handles = m_canvas->paintingAssistantsDecoration()->handles(); m_canvas->paintingAssistantsDecoration()->deselectAssistant(); updateToolOptionsUI(); } void KisAssistantTool::assistantSelected(KisPaintingAssistantSP assistant) { m_canvas->paintingAssistantsDecoration()->setSelectedAssistant(assistant); updateToolOptionsUI(); } void KisAssistantTool::updateToolOptionsUI() { KisPaintingAssistantSP m_selectedAssistant = m_canvas->paintingAssistantsDecoration()->selectedAssistant(); bool hasActiveAssistant = m_selectedAssistant ? true : false; if (m_selectedAssistant) { bool isVanishingPointAssistant = m_selectedAssistant->id() == "vanishing point"; m_options.vanishingPointAngleSpinbox->setVisible(isVanishingPointAssistant); if (isVanishingPointAssistant) { QSharedPointer assis = qSharedPointerCast(m_selectedAssistant); m_options.vanishingPointAngleSpinbox->setValue(assis->referenceLineDensity()); } // load custom color settings from assistant (this happens when changing assistant m_options.useCustomAssistantColor->setChecked(m_selectedAssistant->useCustomColor()); m_options.customAssistantColorButton->setColor(m_selectedAssistant->assistantCustomColor()); float opacity = (float)m_selectedAssistant->assistantCustomColor().alpha()/255.0 * 100.0 ; m_options.customColorOpacitySlider->setValue(opacity); } else { m_options.vanishingPointAngleSpinbox->setVisible(false); // } // show/hide elements if an assistant is selected or not m_options.assistantsGlobalOpacitySlider->setVisible(hasActiveAssistant); m_options.assistantsColor->setVisible(hasActiveAssistant); m_options.globalColorLabel->setVisible(hasActiveAssistant); m_options.useCustomAssistantColor->setVisible(hasActiveAssistant); // hide custom color options if use custom color is not selected bool showCustomColorSettings = m_options.useCustomAssistantColor->isChecked() && hasActiveAssistant; m_options.customColorOpacitySlider->setVisible(showCustomColorSettings); m_options.customAssistantColorButton->setVisible(showCustomColorSettings); // disable global color settings if we are using the custom color m_options.assistantsGlobalOpacitySlider->setEnabled(!showCustomColorSettings); m_options.assistantsColor->setEnabled(!showCustomColorSettings); m_options.globalColorLabel->setEnabled(!showCustomColorSettings); } void KisAssistantTool::slotChangeVanishingPointAngle(double value) { if ( m_canvas->paintingAssistantsDecoration()->assistants().length() == 0) { return; } // get the selected assistant and change the angle value KisPaintingAssistantSP m_selectedAssistant = m_canvas->paintingAssistantsDecoration()->selectedAssistant(); if (m_selectedAssistant) { bool isVanishingPointAssistant = m_selectedAssistant->id() == "vanishing point"; if (isVanishingPointAssistant) { QSharedPointer assis = qSharedPointerCast(m_selectedAssistant); assis->setReferenceLineDensity((float)value); } } m_canvas->canvasWidget()->update(); } void KisAssistantTool::mouseMoveEvent(KoPointerEvent *event) { if (m_newAssistant && m_internalMode == MODE_CREATION) { *m_newAssistant->handles().back() = event->point; } else if (m_newAssistant && m_internalMode == MODE_DRAGGING_TRANSLATING_TWONODES) { QPointF translate = event->point - m_dragEnd; m_dragEnd = event->point; m_selectedNode1.data()->operator = (QPointF(m_selectedNode1.data()->x(),m_selectedNode1.data()->y()) + translate); m_selectedNode2.data()->operator = (QPointF(m_selectedNode2.data()->x(),m_selectedNode2.data()->y()) + translate); } m_canvas->updateCanvas(); } void KisAssistantTool::paint(QPainter& _gc, const KoViewConverter &_converter) { QRectF canvasSize = QRectF(QPointF(0, 0), QSizeF(m_canvas->image()->size())); // show special display while a new assistant is in the process of being created if (m_newAssistant) { QColor assistantColor = m_newAssistant->effectiveAssistantColor(); assistantColor.setAlpha(80); m_newAssistant->drawAssistant(_gc, canvasSize, m_canvas->coordinatesConverter(), false, m_canvas, true, false); Q_FOREACH (const KisPaintingAssistantHandleSP handle, m_newAssistant->handles()) { QPainterPath path; path.addEllipse(QRectF(_converter.documentToView(*handle) - QPointF(m_handleSize * 0.5, m_handleSize * 0.5), QSizeF(m_handleSize, m_handleSize))); _gc.save(); _gc.setPen(Qt::NoPen); _gc.setBrush(assistantColor); _gc.drawPath(path); _gc.restore(); } } Q_FOREACH (KisPaintingAssistantSP assistant, m_canvas->paintingAssistantsDecoration()->assistants()) { QColor assistantColor = assistant->effectiveAssistantColor(); assistantColor.setAlpha(80); Q_FOREACH (const KisPaintingAssistantHandleSP handle, m_handles) { QRectF ellipse(_converter.documentToView(*handle) - QPointF(m_handleSize * 0.5, m_handleSize * 0.5), QSizeF(m_handleSize, m_handleSize)); // render handles differently if it is the one being dragged. if (handle == m_handleDrag || handle == m_handleCombine) { QPen stroke(assistantColor, 4); _gc.save(); _gc.setPen(stroke); _gc.setBrush(Qt::NoBrush); _gc.drawEllipse(ellipse); _gc.restore(); } } } } void KisAssistantTool::removeAllAssistants() { m_canvas->viewManager()->canvasResourceProvider()->clearPerspectiveGrids(); m_canvas->paintingAssistantsDecoration()->removeAll(); m_handles = m_canvas->paintingAssistantsDecoration()->handles(); m_canvas->updateCanvas(); m_canvas->paintingAssistantsDecoration()->deselectAssistant(); updateToolOptionsUI(); } void KisAssistantTool::loadAssistants() { KoFileDialog dialog(m_canvas->viewManager()->mainWindow(), KoFileDialog::OpenFile, "OpenAssistant"); dialog.setCaption(i18n("Select an Assistant")); dialog.setDefaultDir(QStandardPaths::writableLocation(QStandardPaths::PicturesLocation)); dialog.setMimeTypeFilters(QStringList() << "application/x-krita-assistant", "application/x-krita-assistant"); QString filename = dialog.filename(); if (filename.isEmpty()) return; if (!QFileInfo(filename).exists()) return; QFile file(filename); file.open(QIODevice::ReadOnly); QByteArray data = file.readAll(); QXmlStreamReader xml(data); QMap handleMap; KisPaintingAssistantSP assistant; bool errors = false; while (!xml.atEnd()) { switch (xml.readNext()) { case QXmlStreamReader::StartElement: if (xml.name() == "handle") { if (assistant && !xml.attributes().value("ref").isEmpty()) { KisPaintingAssistantHandleSP handle = handleMap.value(xml.attributes().value("ref").toString().toInt()); if (handle) { assistant->addHandle(handle, HandleType::NORMAL); } else { errors = true; } } else { QString strId = xml.attributes().value("id").toString(), strX = xml.attributes().value("x").toString(), strY = xml.attributes().value("y").toString(); if (!strId.isEmpty() && !strX.isEmpty() && !strY.isEmpty()) { int id = strId.toInt(); double x = strX.toDouble(), y = strY.toDouble(); if (!handleMap.contains(id)) { handleMap.insert(id, new KisPaintingAssistantHandle(x, y)); } else { errors = true; } } else { errors = true; } } } else if (xml.name() == "assistant") { const KisPaintingAssistantFactory* factory = KisPaintingAssistantFactoryRegistry::instance()->get(xml.attributes().value("type").toString()); if (factory) { if (assistant) { errors = true; assistant.clear(); } assistant = toQShared(factory->createPaintingAssistant()); } else { errors = true; } if (assistant) { // load custom shared assistant properties if (xml.attributes().hasAttribute("useCustomColor")) { QStringRef useCustomColor = xml.attributes().value("useCustomColor"); bool usingColor = false; if (useCustomColor.toString() == "1") { usingColor = true; } assistant->setUseCustomColor(usingColor); } if ( xml.attributes().hasAttribute("useCustomColor")) { QStringRef customColor = xml.attributes().value("customColor"); assistant->setAssistantCustomColor( KisDomUtils::qStringToQColor(customColor.toString()) ); } } } if (assistant) { assistant->loadCustomXml(&xml); } break; case QXmlStreamReader::EndElement: if (xml.name() == "assistant") { if (assistant) { if (assistant->handles().size() == assistant->numHandles()) { if (assistant->id() == "vanishing point"){ //ideally we'd save and load side-handles as well, but this is all I've got// QPointF pos = *assistant->handles()[0]; assistant->addHandle(new KisPaintingAssistantHandle(pos+QPointF(-70,0)), HandleType::SIDE); assistant->addHandle(new KisPaintingAssistantHandle(pos+QPointF(-140,0)), HandleType::SIDE); assistant->addHandle(new KisPaintingAssistantHandle(pos+QPointF(70,0)), HandleType::SIDE); assistant->addHandle(new KisPaintingAssistantHandle(pos+QPointF(140,0)), HandleType::SIDE); } m_canvas->paintingAssistantsDecoration()->addAssistant(assistant); KisAbstractPerspectiveGrid* grid = dynamic_cast(assistant.data()); if (grid) { m_canvas->viewManager()->canvasResourceProvider()->addPerspectiveGrid(grid); } } else { errors = true; } assistant.clear(); } } break; default: break; } } if (assistant) { errors = true; assistant.clear(); } if (xml.hasError()) { QMessageBox::warning(0, i18nc("@title:window", "Krita"), xml.errorString()); } if (errors) { QMessageBox::warning(0, i18nc("@title:window", "Krita"), i18n("Errors were encountered. Not all assistants were successfully loaded.")); } m_handles = m_canvas->paintingAssistantsDecoration()->handles(); m_canvas->updateCanvas(); } void KisAssistantTool::saveAssistants() { if (m_handles.isEmpty()) return; QByteArray data; QXmlStreamWriter xml(&data); xml.writeStartDocument(); xml.writeStartElement("paintingassistant"); xml.writeAttribute("color", KisDomUtils::qColorToQString( m_canvas->paintingAssistantsDecoration()->globalAssistantsColor())); // global color if no custom color used xml.writeStartElement("handles"); QMap handleMap; Q_FOREACH (const KisPaintingAssistantHandleSP handle, m_handles) { int id = handleMap.size(); handleMap.insert(handle, id); xml.writeStartElement("handle"); //xml.writeAttribute("type", handle->handleType()); xml.writeAttribute("id", QString::number(id)); xml.writeAttribute("x", QString::number(double(handle->x()), 'f', 3)); xml.writeAttribute("y", QString::number(double(handle->y()), 'f', 3)); xml.writeEndElement(); } xml.writeEndElement(); xml.writeStartElement("assistants"); Q_FOREACH (const KisPaintingAssistantSP assistant, m_canvas->paintingAssistantsDecoration()->assistants()) { xml.writeStartElement("assistant"); xml.writeAttribute("type", assistant->id()); xml.writeAttribute("useCustomColor", QString::number(assistant->useCustomColor())); xml.writeAttribute("customColor", KisDomUtils::qColorToQString(assistant->assistantCustomColor())); // custom assistant properties like angle density on vanishing point assistant->saveCustomXml(&xml); // handle information xml.writeStartElement("handles"); Q_FOREACH (const KisPaintingAssistantHandleSP handle, assistant->handles()) { xml.writeStartElement("handle"); xml.writeAttribute("ref", QString::number(handleMap.value(handle))); xml.writeEndElement(); } xml.writeEndElement(); xml.writeEndElement(); } xml.writeEndElement(); xml.writeEndElement(); xml.writeEndDocument(); KoFileDialog dialog(m_canvas->viewManager()->mainWindow(), KoFileDialog::SaveFile, "OpenAssistant"); dialog.setCaption(i18n("Save Assistant")); dialog.setDefaultDir(QStandardPaths::writableLocation(QStandardPaths::PicturesLocation)); dialog.setMimeTypeFilters(QStringList() << "application/x-krita-assistant", "application/x-krita-assistant"); QString filename = dialog.filename(); if (filename.isEmpty()) return; QFile file(filename); file.open(QIODevice::WriteOnly); file.write(data); } QWidget *KisAssistantTool::createOptionWidget() { if (!m_optionsWidget) { m_optionsWidget = new QWidget; m_options.setupUi(m_optionsWidget); // See https://bugs.kde.org/show_bug.cgi?id=316896 QWidget *specialSpacer = new QWidget(m_optionsWidget); specialSpacer->setObjectName("SpecialSpacer"); specialSpacer->setFixedSize(0, 0); m_optionsWidget->layout()->addWidget(specialSpacer); m_options.loadAssistantButton->setIcon(KisIconUtils::loadIcon("document-open")); m_options.saveAssistantButton->setIcon(KisIconUtils::loadIcon("document-save")); m_options.deleteAllAssistantsButton->setIcon(KisIconUtils::loadIcon("edit-delete")); QList assistants; Q_FOREACH (const QString& key, KisPaintingAssistantFactoryRegistry::instance()->keys()) { QString name = KisPaintingAssistantFactoryRegistry::instance()->get(key)->name(); assistants << KoID(key, name); } std::sort(assistants.begin(), assistants.end(), KoID::compareNames); Q_FOREACH(const KoID &id, assistants) { m_options.availableAssistantsComboBox->addItem(id.name(), id.id()); } connect(m_options.saveAssistantButton, SIGNAL(clicked()), SLOT(saveAssistants())); connect(m_options.loadAssistantButton, SIGNAL(clicked()), SLOT(loadAssistants())); connect(m_options.deleteAllAssistantsButton, SIGNAL(clicked()), SLOT(removeAllAssistants())); connect(m_options.assistantsColor, SIGNAL(changed(QColor)), SLOT(slotGlobalAssistantsColorChanged(QColor))); connect(m_options.assistantsGlobalOpacitySlider, SIGNAL(valueChanged(int)), SLOT(slotGlobalAssistantOpacityChanged())); connect(m_options.vanishingPointAngleSpinbox, SIGNAL(valueChanged(double)), this, SLOT(slotChangeVanishingPointAngle(double))); //ENTER_FUNCTION() << ppVar(m_canvas) << ppVar(m_canvas && m_canvas->paintingAssistantsDecoration()); // initialize UI elements with existing data if possible if (m_canvas && m_canvas->paintingAssistantsDecoration()) { const QColor color = m_canvas->paintingAssistantsDecoration()->globalAssistantsColor(); QColor opaqueColor = color; opaqueColor.setAlpha(255); //ENTER_FUNCTION() << ppVar(opaqueColor); m_options.assistantsColor->setColor(opaqueColor); m_options.customAssistantColorButton->setColor(opaqueColor); m_options.assistantsGlobalOpacitySlider->setValue(color.alphaF() * 100.0); } else { m_options.assistantsColor->setColor(QColor(176, 176, 176, 255)); // grey default for all assistants m_options.assistantsGlobalOpacitySlider->setValue(100); // 100% } m_options.assistantsGlobalOpacitySlider->setPrefix(i18n("Opacity: ")); m_options.assistantsGlobalOpacitySlider->setSuffix(" %"); // custom color of selected assistant m_options.customColorOpacitySlider->setValue(100); // 100% m_options.customColorOpacitySlider->setPrefix(i18n("Opacity: ")); m_options.customColorOpacitySlider->setSuffix(" %"); connect(m_options.useCustomAssistantColor, SIGNAL(clicked(bool)), this, SLOT(slotUpdateCustomColor())); connect(m_options.customAssistantColorButton, SIGNAL(changed(QColor)), this, SLOT(slotUpdateCustomColor())); connect(m_options.customColorOpacitySlider, SIGNAL(valueChanged(int)), SLOT(slotCustomOpacityChanged())); m_options.vanishingPointAngleSpinbox->setPrefix(i18n("Density: ")); m_options.vanishingPointAngleSpinbox->setSuffix(QChar(Qt::Key_degree)); m_options.vanishingPointAngleSpinbox->setRange(1.0, 180.0); m_options.vanishingPointAngleSpinbox->setSingleStep(0.5); m_options.vanishingPointAngleSpinbox->setVisible(false); } updateToolOptionsUI(); return m_optionsWidget; } void KisAssistantTool::slotGlobalAssistantsColorChanged(const QColor& setColor) { // color and alpha are stored separately, so we need to merge the values before sending it on int oldAlpha = m_canvas->paintingAssistantsDecoration()->globalAssistantsColor().alpha(); QColor newColor = setColor; newColor.setAlpha(oldAlpha); m_canvas->paintingAssistantsDecoration()->setGlobalAssistantsColor(newColor); m_canvas->paintingAssistantsDecoration()->uncache(); m_canvas->canvasWidget()->update(); } void KisAssistantTool::slotGlobalAssistantOpacityChanged() { QColor newColor = m_canvas->paintingAssistantsDecoration()->globalAssistantsColor(); qreal newOpacity = m_options.assistantsGlobalOpacitySlider->value() * 0.01 * 255.0; newColor.setAlpha(int(newOpacity)); m_canvas->paintingAssistantsDecoration()->setGlobalAssistantsColor(newColor); m_canvas->paintingAssistantsDecoration()->uncache(); m_canvas->canvasWidget()->update(); } void KisAssistantTool::slotUpdateCustomColor() { // get the selected assistant and change the angle value KisPaintingAssistantSP m_selectedAssistant = m_canvas->paintingAssistantsDecoration()->selectedAssistant(); if (m_selectedAssistant) { m_selectedAssistant->setUseCustomColor(m_options.useCustomAssistantColor->isChecked()); // changing color doesn't keep alpha, so update that before we send it on QColor newColor = m_options.customAssistantColorButton->color(); newColor.setAlpha(m_selectedAssistant->assistantCustomColor().alpha()); m_selectedAssistant->setAssistantCustomColor(newColor); m_selectedAssistant->uncache(); } updateToolOptionsUI(); m_canvas->canvasWidget()->update(); } void KisAssistantTool::slotCustomOpacityChanged() { KisPaintingAssistantSP m_selectedAssistant = m_canvas->paintingAssistantsDecoration()->selectedAssistant(); if (m_selectedAssistant) { QColor newColor = m_selectedAssistant->assistantCustomColor(); qreal newOpacity = m_options.customColorOpacitySlider->value() * 0.01 * 255.0; newColor.setAlpha(int(newOpacity)); m_selectedAssistant->setAssistantCustomColor(newColor); m_selectedAssistant->uncache(); } // this forces the canvas to refresh to see the changes immediately m_canvas->paintingAssistantsDecoration()->uncache(); m_canvas->canvasWidget()->update(); } diff --git a/plugins/dockers/histogram/histogramdockerwidget.cpp b/plugins/dockers/histogram/histogramdockerwidget.cpp index 5c8e29d929..83aa432f6e 100644 --- a/plugins/dockers/histogram/histogramdockerwidget.cpp +++ b/plugins/dockers/histogram/histogramdockerwidget.cpp @@ -1,199 +1,200 @@ /* * Copyright (c) 2016 Eugene Ingerman * * 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 of the License, or * (at your option) any later version. * * 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 "histogramdockerwidget.h" #include #include #include #include #include #include +#include #include #include "KoChannelInfo.h" #include "kis_paint_device.h" #include "KoColorSpace.h" #include "kis_iterator_ng.h" #include "kis_canvas2.h" HistogramDockerWidget::HistogramDockerWidget(QWidget *parent, const char *name, Qt::WindowFlags f) : QLabel(parent, f), m_paintDevice(nullptr), m_smoothHistogram(true) { setObjectName(name); } HistogramDockerWidget::~HistogramDockerWidget() { } void HistogramDockerWidget::setPaintDevice(KisCanvas2* canvas) { if (canvas) { m_paintDevice = canvas->image()->projection(); m_bounds = canvas->image()->bounds(); } else { m_paintDevice.clear(); m_bounds = QRect(); m_histogramData.clear(); } } void HistogramDockerWidget::updateHistogram() { if (!m_paintDevice.isNull()) { KisPaintDeviceSP m_devClone = new KisPaintDevice(m_paintDevice->colorSpace()); m_devClone->makeCloneFrom(m_paintDevice, m_bounds); HistogramComputationThread *workerThread = new HistogramComputationThread(m_devClone, m_bounds); connect(workerThread, &HistogramComputationThread::resultReady, this, &HistogramDockerWidget::receiveNewHistogram); connect(workerThread, &HistogramComputationThread::finished, workerThread, &QObject::deleteLater); workerThread->start(); } else { m_histogramData.clear(); update(); } } void HistogramDockerWidget::receiveNewHistogram(HistVector *histogramData) { m_histogramData = *histogramData; update(); } void HistogramDockerWidget::paintEvent(QPaintEvent *event) { if (m_paintDevice && !m_histogramData.empty()) { int nBins = m_histogramData.at(0).size(); const KoColorSpace* cs = m_paintDevice->colorSpace(); QLabel::paintEvent(event); QPainter painter(this); painter.fillRect(0, 0, this->width(), this->height(), this->palette().dark().color()); painter.setPen(this->palette().light().color()); const int NGRID = 4; for (int i = 0; i <= NGRID; ++i) { painter.drawLine(this->width()*i / NGRID, 0., this->width()*i / NGRID, this->height()); painter.drawLine(0., this->height()*i / NGRID, this->width(), this->height()*i / NGRID); } unsigned int nChannels = cs->channelCount(); QList channels = cs->channels(); unsigned int highest = 0; //find the most populous bin in the histogram to scale it properly for (int chan = 0; chan < channels.size(); chan++) { if (channels.at(chan)->channelType() != KoChannelInfo::ALPHA) { std::vector histogramTemp = m_histogramData.at(chan); //use 98th percentile, rather than max for better visual appearance int nthPercentile = 2 * histogramTemp.size() / 100; //unsigned int max = *std::max_element(m_histogramData.at(chan).begin(),m_histogramData.at(chan).end()); std::nth_element(histogramTemp.begin(), histogramTemp.begin() + nthPercentile, histogramTemp.end(), std::greater()); unsigned int max = *(histogramTemp.begin() + nthPercentile); highest = std::max(max, highest); } } painter.setWindow(QRect(-1, 0, nBins + 1, highest)); painter.setCompositionMode(QPainter::CompositionMode_Plus); for (int chan = 0; chan < (int)nChannels; chan++) { if (channels.at(chan)->channelType() != KoChannelInfo::ALPHA) { QColor color = channels.at(chan)->color(); //special handling of grayscale color spaces. can't use color returned above. if(cs->colorChannelCount()==1){ color = QColor(Qt::gray); } QColor fill_color = color; fill_color.setAlphaF(.25); painter.setBrush(fill_color); QPen pen = QPen(color); pen.setWidth(0); painter.setPen(pen); if (m_smoothHistogram) { QPainterPath path; path.moveTo(QPointF(-1, highest)); for (qint32 i = 0; i < nBins; ++i) { float v = std::max((float)highest - m_histogramData[chan][i], 0.f); path.lineTo(QPointF(i, v)); } path.lineTo(QPointF(nBins + 1, highest)); path.closeSubpath(); painter.drawPath(path); } else { pen.setWidth(1); painter.setPen(pen); for (qint32 i = 0; i < nBins; ++i) { float v = std::max((float)highest - m_histogramData[chan][i], 0.f); painter.drawLine(QPointF(i, highest), QPointF(i, v)); } } } } } } void HistogramComputationThread::run() { const KoColorSpace *cs = m_dev->colorSpace(); quint32 channelCount = m_dev->channelCount(); quint32 pixelSize = m_dev->pixelSize(); quint32 imageSize = m_bounds.width() * m_bounds.height(); quint32 nSkip = 1 + (imageSize >> 20); //for speed use about 1M pixels for computing histograms //allocate space for the histogram data bins.resize((int)channelCount); for (auto &bin : bins) { bin.resize(std::numeric_limits::max() + 1); } QRect bounds = m_dev->exactBounds(); if (bounds.isEmpty()) return; quint32 toSkip = nSkip; KisSequentialConstIterator it(m_dev, m_dev->exactBounds()); int numConseqPixels = it.nConseqPixels(); while (it.nextPixels(numConseqPixels)) { numConseqPixels = it.nConseqPixels(); const quint8* pixel = it.rawDataConst(); for (int k = 0; k < numConseqPixels; ++k) { if (--toSkip == 0) { for (int chan = 0; chan < (int)channelCount; ++chan) { bins[chan][cs->scaleToU8(pixel, chan)]++; } toSkip = nSkip; } pixel += pixelSize; } } emit resultReady(&bins); } diff --git a/plugins/flake/pathshapes/enhancedpath/EnhancedPathShape.cpp b/plugins/flake/pathshapes/enhancedpath/EnhancedPathShape.cpp index 3c246c9c43..96d1892994 100644 --- a/plugins/flake/pathshapes/enhancedpath/EnhancedPathShape.cpp +++ b/plugins/flake/pathshapes/enhancedpath/EnhancedPathShape.cpp @@ -1,744 +1,746 @@ /* This file is part of the KDE project * Copyright (C) 2007,2010,2011 Jan Hambrecht * Copyright (C) 2009-2010 Thomas Zander * Copyright (C) 2010 Carlos Licea * Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). * Contact: Suresh Chande suresh.chande@nokia.com * Copyright (C) 2009-2010 Thorsten Zachmann * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include #include "EnhancedPathShape.h" #include "EnhancedPathCommand.h" #include "EnhancedPathParameter.h" #include "EnhancedPathHandle.h" #include "EnhancedPathFormula.h" +#include + #include #include #include #include #include #include #include EnhancedPathShape::EnhancedPathShape(const QRect &viewBox) : m_viewBox(viewBox) , m_viewBoxOffset(0.0, 0.0) , m_mirrorVertically(false) , m_mirrorHorizontally(false) , m_pathStretchPointX(-1) , m_pathStretchPointY(-1) , m_cacheResults(false) { } EnhancedPathShape::EnhancedPathShape(const EnhancedPathShape &rhs) : KoParameterShape(new KoParameterShapePrivate(*rhs.d_func(), this)), m_viewBox(rhs.m_viewBox), m_viewBound(rhs.m_viewBound), m_viewMatrix(rhs.m_viewMatrix), m_mirrorMatrix(rhs.m_mirrorMatrix), m_viewBoxOffset(rhs.m_viewBoxOffset), m_textArea(rhs.m_textArea), m_commands(rhs.m_commands), m_enhancedHandles(rhs.m_enhancedHandles), m_formulae(rhs.m_formulae), m_modifiers(rhs.m_modifiers), m_parameters(rhs.m_parameters), m_mirrorVertically(rhs.m_mirrorVertically), m_mirrorHorizontally(rhs.m_mirrorHorizontally), m_pathStretchPointX(rhs.m_pathStretchPointX), m_pathStretchPointY(rhs.m_pathStretchPointY), m_resultCache(rhs.m_resultCache), m_cacheResults(rhs.m_cacheResults) { } EnhancedPathShape::~EnhancedPathShape() { reset(); } KoShape *EnhancedPathShape::cloneShape() const { return new EnhancedPathShape(*this); } void EnhancedPathShape::reset() { qDeleteAll(m_commands); m_commands.clear(); qDeleteAll(m_enhancedHandles); m_enhancedHandles.clear(); setHandles(QList()); qDeleteAll(m_formulae); m_formulae.clear(); qDeleteAll(m_parameters); m_parameters.clear(); m_modifiers.clear(); m_viewMatrix.reset(); m_viewBoxOffset = QPointF(); clear(); m_textArea.clear(); } void EnhancedPathShape::moveHandleAction(int handleId, const QPointF &point, Qt::KeyboardModifiers modifiers) { Q_UNUSED(modifiers); EnhancedPathHandle *handle = m_enhancedHandles[ handleId ]; if (handle) { handle->changePosition(shapeToViewbox(point)); } } void EnhancedPathShape::updatePath(const QSizeF &size) { Q_D(KoParameterShape); if (isParametricShape()) { clear(); enableResultCache(true); foreach (EnhancedPathCommand *cmd, m_commands) { cmd->execute(); } enableResultCache(false); qreal stretchPointsScale = 1; bool isStretched = useStretchPoints(size, stretchPointsScale); m_viewBound = outline().boundingRect(); m_mirrorMatrix.reset(); m_mirrorMatrix.translate(m_viewBound.center().x(), m_viewBound.center().y()); m_mirrorMatrix.scale(m_mirrorHorizontally ? -1 : 1, m_mirrorVertically ? -1 : 1); m_mirrorMatrix.translate(-m_viewBound.center().x(), -m_viewBound.center().y()); QTransform matrix(1.0, 0.0, 0.0, 1.0, m_viewBoxOffset.x(), m_viewBoxOffset.y()); // if stretch points are set than stretch the path manually if (isStretched) { //if the path was stretched manually the stretch matrix is not more valid //and it has to be recalculated so that stretching in x and y direction is the same matrix.scale(stretchPointsScale, stretchPointsScale); matrix = m_mirrorMatrix * matrix; } else { matrix = m_mirrorMatrix * m_viewMatrix * matrix; } foreach (KoSubpath *subpath, d->subpaths) { foreach (KoPathPoint *point, *subpath) { point->map(matrix); } } const int handleCount = m_enhancedHandles.count(); QList handles; for (int i = 0; i < handleCount; ++i) { handles.append(matrix.map(m_enhancedHandles[i]->position())); } setHandles(handles); normalize(); } } void EnhancedPathShape::setSize(const QSizeF &newSize) { // handle offset KoParameterShape::setSize(newSize); // calculate scaling factors from viewbox size to shape size qreal xScale = m_viewBound.width() == 0 ? 1 : newSize.width() / m_viewBound.width(); qreal yScale = m_viewBound.height() == 0 ? 1 : newSize.height() / m_viewBound.height(); // create view matrix, take mirroring into account m_viewMatrix.reset(); m_viewMatrix.scale(xScale, yScale); updatePath(newSize); } QPointF EnhancedPathShape::normalize() { QPointF offset = KoParameterShape::normalize(); m_viewBoxOffset -= offset; return offset; } QPointF EnhancedPathShape::shapeToViewbox(const QPointF &point) const { return (m_mirrorMatrix * m_viewMatrix).inverted().map(point - m_viewBoxOffset); } void EnhancedPathShape::evaluateHandles() { const int handleCount = m_enhancedHandles.count(); QList handles; for (int i = 0; i < handleCount; ++i) { handles.append(m_enhancedHandles[i]->position()); } setHandles(handles); } QRect EnhancedPathShape::viewBox() const { return m_viewBox; } qreal EnhancedPathShape::evaluateReference(const QString &reference) { if (reference.isEmpty()) { return 0.0; } const char c = reference[0].toLatin1(); qreal res = 0.0; switch (c) { // referenced modifier case '$': { bool success = false; int modifierIndex = reference.mid(1).toInt(&success); res = m_modifiers.value(modifierIndex); break; } // referenced formula case '?': { QString fname = reference.mid(1); if (m_cacheResults && m_resultCache.contains(fname)) { res = m_resultCache.value(fname); } else { FormulaStore::const_iterator formulaIt = m_formulae.constFind(fname); if (formulaIt != m_formulae.constEnd()) { EnhancedPathFormula *formula = formulaIt.value(); if (formula) { res = formula->evaluate(); if (m_cacheResults) { m_resultCache.insert(fname, res); } } } } break; } // maybe an identifier ? default: EnhancedPathNamedParameter p(reference, this); res = p.evaluate(); break; } return res; } qreal EnhancedPathShape::evaluateConstantOrReference(const QString &val) { bool ok = true; qreal res = val.toDouble(&ok); if (ok) { return res; } return evaluateReference(val); } void EnhancedPathShape::modifyReference(const QString &reference, qreal value) { if (reference.isEmpty()) { return; } const char c = reference[0].toLatin1(); if (c == '$') { bool success = false; int modifierIndex = reference.mid(1).toInt(&success); if (modifierIndex >= 0 && modifierIndex < m_modifiers.count()) { m_modifiers[modifierIndex] = value; } } } EnhancedPathParameter *EnhancedPathShape::parameter(const QString &text) { Q_ASSERT(! text.isEmpty()); ParameterStore::const_iterator parameterIt = m_parameters.constFind(text); if (parameterIt != m_parameters.constEnd()) { return parameterIt.value(); } else { EnhancedPathParameter *parameter = 0; const char c = text[0].toLatin1(); if (c == '$' || c == '?') { parameter = new EnhancedPathReferenceParameter(text, this); } else { bool success = false; qreal constant = text.toDouble(&success); if (success) { parameter = new EnhancedPathConstantParameter(constant, this); } else { Identifier identifier = EnhancedPathNamedParameter::identifierFromString(text); if (identifier != IdentifierUnknown) { parameter = new EnhancedPathNamedParameter(identifier, this); } } } if (parameter) { m_parameters[text] = parameter; } return parameter; } } void EnhancedPathShape::addFormula(const QString &name, const QString &formula) { if (name.isEmpty() || formula.isEmpty()) { return; } m_formulae[name] = new EnhancedPathFormula(formula, this); } void EnhancedPathShape::addHandle(const QMap &handle) { if (handle.isEmpty()) { return; } if (!handle.contains("draw:handle-position")) { return; } QVariant position = handle.value("draw:handle-position"); QStringList tokens = position.toString().simplified().split(' '); if (tokens.count() < 2) { return; } EnhancedPathHandle *newHandle = new EnhancedPathHandle(this); newHandle->setPosition(parameter(tokens[0]), parameter(tokens[1])); // check if we have a polar handle if (handle.contains("draw:handle-polar")) { QVariant polar = handle.value("draw:handle-polar"); QStringList tokens = polar.toString().simplified().split(' '); if (tokens.count() == 2) { newHandle->setPolarCenter(parameter(tokens[0]), parameter(tokens[1])); QVariant minRadius = handle.value("draw:handle-radius-range-minimum"); QVariant maxRadius = handle.value("draw:handle-radius-range-maximum"); if (minRadius.isValid() && maxRadius.isValid()) { newHandle->setRadiusRange(parameter(minRadius.toString()), parameter(maxRadius.toString())); } } } else { QVariant minX = handle.value("draw:handle-range-x-minimum"); QVariant maxX = handle.value("draw:handle-range-x-maximum"); if (minX.isValid() && maxX.isValid()) { newHandle->setRangeX(parameter(minX.toString()), parameter(maxX.toString())); } QVariant minY = handle.value("draw:handle-range-y-minimum"); QVariant maxY = handle.value("draw:handle-range-y-maximum"); if (minY.isValid() && maxY.isValid()) { newHandle->setRangeY(parameter(minY.toString()), parameter(maxY.toString())); } } m_enhancedHandles.append(newHandle); evaluateHandles(); } void EnhancedPathShape::addModifiers(const QString &modifiers) { if (modifiers.isEmpty()) { return; } QStringList tokens = modifiers.simplified().split(' '); int tokenCount = tokens.count(); for (int i = 0; i < tokenCount; ++i) { m_modifiers.append(tokens[i].toDouble()); } } void EnhancedPathShape::addCommand(const QString &command) { addCommand(command, true); } void EnhancedPathShape::addCommand(const QString &command, bool triggerUpdate) { QString commandStr = command.simplified(); if (commandStr.isEmpty()) { return; } // the first character is the command EnhancedPathCommand *cmd = new EnhancedPathCommand(commandStr[0], this); // strip command char commandStr = commandStr.mid(1).simplified(); // now parse the command parameters if (!commandStr.isEmpty()) { QStringList tokens = commandStr.split(' '); for (int i = 0; i < tokens.count(); ++i) { cmd->addParameter(parameter(tokens[i])); } } m_commands.append(cmd); if (triggerUpdate) { updatePath(size()); } } bool EnhancedPathShape::useStretchPoints(const QSizeF &size, qreal &scale) { Q_D(KoParameterShape); bool retval = false; if (m_pathStretchPointX != -1 && m_pathStretchPointY != -1) { qreal scaleX = size.width(); qreal scaleY = size.height(); if (qreal(m_viewBox.width()) / m_viewBox.height() < qreal(scaleX) / scaleY) { qreal deltaX = (scaleX * m_viewBox.height()) / scaleY - m_viewBox.width(); foreach (KoSubpath *subpath, d->subpaths) { foreach (KoPathPoint *currPoint, *subpath) { if (currPoint->point().x() >= m_pathStretchPointX && currPoint->controlPoint1().x() >= m_pathStretchPointX && currPoint->controlPoint2().x() >= m_pathStretchPointX) { currPoint->setPoint(QPointF(currPoint->point().x() + deltaX, currPoint->point().y())); currPoint->setControlPoint1(QPointF(currPoint->controlPoint1().x() + deltaX, currPoint->controlPoint1().y())); currPoint->setControlPoint2(QPointF(currPoint->controlPoint2().x() + deltaX, currPoint->controlPoint2().y())); retval = true; } } } scale = scaleY / m_viewBox.height(); } else if (qreal(m_viewBox.width()) / m_viewBox.height() > qreal(scaleX) / scaleY) { qreal deltaY = (m_viewBox.width() * scaleY) / scaleX - m_viewBox.height(); foreach (KoSubpath *subpath, d->subpaths) { foreach (KoPathPoint *currPoint, *subpath) { if (currPoint->point().y() >= m_pathStretchPointY && currPoint->controlPoint1().y() >= m_pathStretchPointY && currPoint->controlPoint2().y() >= m_pathStretchPointY) { currPoint->setPoint(QPointF(currPoint->point().x(), currPoint->point().y() + deltaY)); currPoint->setControlPoint1(QPointF(currPoint->controlPoint1().x(), currPoint->controlPoint1().y() + deltaY)); currPoint->setControlPoint2(QPointF(currPoint->controlPoint2().x(), currPoint->controlPoint2().y() + deltaY)); retval = true; } } } scale = scaleX / m_viewBox.width(); } notifyPointsChanged(); } return retval; } void EnhancedPathShape::saveOdf(KoShapeSavingContext &context) const { if (isParametricShape()) { context.xmlWriter().startElement("draw:custom-shape"); const QSizeF currentSize = outline().boundingRect().size(); // save the right position so that when loading we fit the viewbox // to the right position without getting any wrong scaling // -> calculate the right position from the current 0 position / viewbound ratio // this is e.g. the case when there is a callout that goes into negative viewbound coordinates QPointF topLeft = m_viewBound.topLeft(); QPointF diff; if (qAbs(topLeft.x()) > 1E-5) { diff.setX(topLeft.x()*currentSize.width() / m_viewBound.width()); } if (qAbs(topLeft.y()) > 1E-5) { diff.setY(topLeft.y()*currentSize.height() / m_viewBound.height()); } if (diff.isNull()) { saveOdfAttributes(context, OdfAllAttributes & ~OdfSize); } else { //FIXME: this needs to be fixed for shapes that are transformed by rotation or skewing QTransform offset(context.shapeOffset(this)); QTransform newOffset(offset); newOffset.translate(-diff.x(), -diff.y()); context.addShapeOffset(this, newOffset); saveOdfAttributes(context, OdfAllAttributes & ~OdfSize); if (offset.isIdentity()) { context.removeShapeOffset(this); } else { context.addShapeOffset(this, offset); } } // save the right size so that when loading we fit the viewbox // to the right size without getting any wrong scaling // -> calculate the right size from the current size/viewbound ratio context.xmlWriter().addAttribute("svg:width", currentSize.width() == 0 ? 0 : m_viewBox.width()*currentSize.width() / m_viewBound.width()); context.xmlWriter().addAttribute("svg:height", currentSize.height() == 0 ? 0 : m_viewBox.height()*currentSize.height() / m_viewBound.height()); saveText(context); context.xmlWriter().startElement("draw:enhanced-geometry"); context.xmlWriter().addAttribute("svg:viewBox", QString("%1 %2 %3 %4").arg(m_viewBox.x()).arg(m_viewBox.y()).arg(m_viewBox.width()).arg(m_viewBox.height())); if (m_pathStretchPointX != -1) { context.xmlWriter().addAttribute("draw:path-stretchpoint-x", m_pathStretchPointX); } if (m_pathStretchPointY != -1) { context.xmlWriter().addAttribute("draw:path-stretchpoint-y", m_pathStretchPointY); } if (m_mirrorHorizontally) { context.xmlWriter().addAttribute("draw:mirror-horizontal", "true"); } if (m_mirrorVertically) { context.xmlWriter().addAttribute("draw:mirror-vertical", "true"); } QString modifiers; foreach (qreal modifier, m_modifiers) { modifiers += QString::number(modifier) + ' '; } context.xmlWriter().addAttribute("draw:modifiers", modifiers.trimmed()); if (m_textArea.size() >= 4) { context.xmlWriter().addAttribute("draw:text-areas", m_textArea.join(" ")); } QString path; foreach (EnhancedPathCommand *c, m_commands) { path += c->toString() + ' '; } context.xmlWriter().addAttribute("draw:enhanced-path", path.trimmed()); FormulaStore::const_iterator i = m_formulae.constBegin(); for (; i != m_formulae.constEnd(); ++i) { context.xmlWriter().startElement("draw:equation"); context.xmlWriter().addAttribute("draw:name", i.key()); context.xmlWriter().addAttribute("draw:formula", i.value()->toString()); context.xmlWriter().endElement(); // draw:equation } foreach (EnhancedPathHandle *handle, m_enhancedHandles) { handle->saveOdf(context); } context.xmlWriter().endElement(); // draw:enhanced-geometry saveOdfCommonChildElements(context); context.xmlWriter().endElement(); // draw:custom-shape } else { KoPathShape::saveOdf(context); } } bool EnhancedPathShape::loadOdf(const KoXmlElement &element, KoShapeLoadingContext &context) { reset(); const KoXmlElement enhancedGeometry(KoXml::namedItemNS(element, KoXmlNS::draw, "enhanced-geometry")); if (!enhancedGeometry.isNull()) { setPathStretchPointX(enhancedGeometry.attributeNS(KoXmlNS::draw, "path-stretchpoint-x", "-1").toDouble()); setPathStretchPointY(enhancedGeometry.attributeNS(KoXmlNS::draw, "path-stretchpoint-y", "-1").toDouble()); // load the modifiers QString modifiers = enhancedGeometry.attributeNS(KoXmlNS::draw, "modifiers", ""); if (!modifiers.isEmpty()) { addModifiers(modifiers); } m_textArea = enhancedGeometry.attributeNS(KoXmlNS::draw, "text-areas", "").split(' '); if (m_textArea.size() >= 4) { setResizeBehavior(TextFollowsPreferredTextRect); } KoXmlElement grandChild; forEachElement(grandChild, enhancedGeometry) { if (grandChild.namespaceURI() != KoXmlNS::draw) { continue; } if (grandChild.localName() == "equation") { QString name = grandChild.attributeNS(KoXmlNS::draw, "name"); QString formula = grandChild.attributeNS(KoXmlNS::draw, "formula"); addFormula(name, formula); } else if (grandChild.localName() == "handle") { EnhancedPathHandle *handle = new EnhancedPathHandle(this); if (handle->loadOdf(grandChild, context)) { m_enhancedHandles.append(handle); evaluateHandles(); } else { delete handle; } } } setMirrorHorizontally(enhancedGeometry.attributeNS(KoXmlNS::draw, "mirror-horizontal") == "true"); setMirrorVertically(enhancedGeometry.attributeNS(KoXmlNS::draw, "mirror-vertical") == "true"); // load the enhanced path data QString path = enhancedGeometry.attributeNS(KoXmlNS::draw, "enhanced-path", ""); #ifndef NWORKAROUND_ODF_BUGS KoOdfWorkaround::fixEnhancedPath(path, enhancedGeometry, context); #endif // load the viewbox m_viewBox = loadOdfViewbox(enhancedGeometry); if (!path.isEmpty()) { parsePathData(path); } if (m_viewBox.isEmpty()) { // if there is no view box defined make it is big as the path. m_viewBox = m_viewBound.toAlignedRect(); } } QSizeF size; size.setWidth(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "width", QString()))); size.setHeight(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "height", QString()))); // the viewbox is to be fitted into the size of the shape, so before setting // the size we just loaded // we set the viewbox to be the basis to calculate // the viewbox matrix from m_viewBound = m_viewBox; setSize(size); QPointF pos; pos.setX(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "x", QString()))); pos.setY(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "y", QString()))); setPosition(pos - m_viewMatrix.map(QPointF(0, 0)) - m_viewBoxOffset); loadOdfAttributes(element, context, OdfMandatories | OdfTransformation | OdfAdditionalAttributes | OdfCommonChildElements); loadText(element, context); return true; } void EnhancedPathShape::parsePathData(const QString &data) { if (data.isEmpty()) { return; } int start = -1; bool separator = true; for (int i = 0; i < data.length(); ++i) { QChar ch = data.at(i); ushort uni_ch = ch.unicode(); if (separator && (uni_ch == 'M' || uni_ch == 'L' || uni_ch == 'C' || uni_ch == 'Z' || uni_ch == 'N' || uni_ch == 'F' || uni_ch == 'S' || uni_ch == 'T' || uni_ch == 'U' || uni_ch == 'A' || uni_ch == 'B' || uni_ch == 'W' || uni_ch == 'V' || uni_ch == 'X' || uni_ch == 'Y' || uni_ch == 'Q')) { if (start != -1) { // process last chars addCommand(data.mid(start, i - start), false); } start = i; } separator = ch.isSpace(); } if (start < data.length()) { addCommand(data.mid(start)); } if (start != -1) { updatePath(size()); } } void EnhancedPathShape::setMirrorHorizontally(bool mirrorHorizontally) { if (m_mirrorHorizontally != mirrorHorizontally) { m_mirrorHorizontally = mirrorHorizontally; updatePath(size()); } } void EnhancedPathShape::setMirrorVertically(bool mirrorVertically) { if (m_mirrorVertically != mirrorVertically) { m_mirrorVertically = mirrorVertically; updatePath(size()); } } void EnhancedPathShape::shapeChanged(ChangeType type, KoShape *shape) { KoParameterShape::shapeChanged(type, shape); if (!shape || shape == this) { if (type == ParentChanged || type == ParameterChanged) { updateTextArea(); } } } void EnhancedPathShape::updateTextArea() { if (m_textArea.size() >= 4) { QRectF r = m_viewBox; r.setLeft(evaluateConstantOrReference(m_textArea[0])); r.setTop(evaluateConstantOrReference(m_textArea[1])); r.setRight(evaluateConstantOrReference(m_textArea[2])); r.setBottom(evaluateConstantOrReference(m_textArea[3])); r = m_viewMatrix.mapRect(r).translated(m_viewBoxOffset); setPreferredTextRect(r); } } void EnhancedPathShape::enableResultCache(bool enable) { m_resultCache.clear(); m_cacheResults = enable; } void EnhancedPathShape::setPathStretchPointX(qreal pathStretchPointX) { if (m_pathStretchPointX != pathStretchPointX) { m_pathStretchPointX = pathStretchPointX; } } void EnhancedPathShape::setPathStretchPointY(qreal pathStretchPointY) { if (m_pathStretchPointY != pathStretchPointY) { m_pathStretchPointY = pathStretchPointY; } } diff --git a/plugins/flake/textshape/TextShape.cpp b/plugins/flake/textshape/TextShape.cpp index c06c799109..5b2fd123a9 100644 --- a/plugins/flake/textshape/TextShape.cpp +++ b/plugins/flake/textshape/TextShape.cpp @@ -1,471 +1,472 @@ /* This file is part of the KDE project * Copyright (C) 2006-2010 Thomas Zander * Copyright (C) 2008-2010 Thorsten Zachmann * Copyright (C) 2008 Pierre Stirnweiss \pierre.stirnweiss_calligra@gadz.org> * Copyright (C) 2010 KO GmbH * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "TextShape.h" #include "ShrinkToFitShapeContainer.h" #include #include "SimpleRootAreaProvider.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include +#include #include #include #include #include #include "kis_painting_tweaks.h" TextShape::TextShape(KoInlineTextObjectManager *inlineTextObjectManager, KoTextRangeManager *textRangeManager) : KoShapeContainer(new KoTextShapeContainerModel()) , KoFrameShape(KoXmlNS::draw, "text-box") , m_pageProvider(0) , m_imageCollection(0) , m_clip(true) { setShapeId(TextShape_SHAPEID); m_textShapeData = new KoTextShapeData(); setUserData(m_textShapeData); SimpleRootAreaProvider *provider = new SimpleRootAreaProvider(m_textShapeData, this); KoTextDocument(m_textShapeData->document()).setInlineTextObjectManager(inlineTextObjectManager); KoTextDocument(m_textShapeData->document()).setTextRangeManager(textRangeManager); m_layout = new KoTextDocumentLayout(m_textShapeData->document(), provider); m_textShapeData->document()->setDocumentLayout(m_layout); /// FIXME: collision detection was dropped in Krita /// due to performance reasons // setCollisionDetection(true); QObject::connect(m_layout, SIGNAL(layoutIsDirty()), m_layout, SLOT(scheduleLayout())); } TextShape::TextShape(const TextShape &rhs) : KoShapeContainer(new KoShapeContainerPrivate(*reinterpret_cast(rhs.d_ptr), this)), KoFrameShape(rhs), m_textShapeData(dynamic_cast(rhs.m_textShapeData->clone())), m_pageProvider(0), m_imageCollection(0), m_clip(rhs.m_clip) { reinterpret_cast(rhs.d_ptr)->model = new KoTextShapeContainerModel(); setShapeId(TextShape_SHAPEID); setUserData(m_textShapeData); SimpleRootAreaProvider *provider = new SimpleRootAreaProvider(m_textShapeData, this); m_layout = new KoTextDocumentLayout(m_textShapeData->document(), provider); m_textShapeData->document()->setDocumentLayout(m_layout); /// FIXME: collision detection was dropped in Krita /// due to performance reasons // setCollisionDetection(true); QObject::connect(m_layout, SIGNAL(layoutIsDirty()), m_layout, SLOT(scheduleLayout())); updateDocumentData(); m_layout->scheduleLayout(); } TextShape::~TextShape() { } KoShape *TextShape::cloneShape() const { return new TextShape(*this); } void TextShape::paintComponent(QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintContext) { painter.save(); applyConversion(painter, converter); KoBorder *border = this->border(); if (border) { paintBorder(painter, converter); } else if (paintContext.showTextShapeOutlines) { // No need to paint the outlines if there is a real border. if (qAbs(rotation()) > 1) { painter.setRenderHint(QPainter::Antialiasing); } QPen pen(QColor(210, 210, 210)); // use cosmetic pen QPointF onePixel = converter.viewToDocument(QPointF(1.0, 1.0)); QRectF rect(QPointF(0.0, 0.0), size() - QSizeF(onePixel.x(), onePixel.y())); painter.setPen(pen); painter.drawRect(rect); } painter.restore(); if (m_textShapeData->isDirty()) { // not layouted yet. return; } QTextDocument *doc = m_textShapeData->document(); Q_ASSERT(doc); KoTextDocumentLayout *lay = qobject_cast(doc->documentLayout()); Q_ASSERT(lay); lay->showInlineObjectVisualization(paintContext.showInlineObjectVisualization); applyConversion(painter, converter); if (background()) { QPainterPath p; p.addRect(QRectF(QPointF(), size())); background()->paint(painter, converter, paintContext, p); } // this enables to use the same shapes on different pages showing different page numbers if (m_pageProvider) { KoTextPage *page = m_pageProvider->page(this); if (page) { // this is used to not trigger repaints if layout during the painting is done m_paintRegion = KisPaintingTweaks::safeClipRegion(painter); if (!m_textShapeData->rootArea()->page() || page->pageNumber() != m_textShapeData->rootArea()->page()->pageNumber()) { m_textShapeData->rootArea()->setPage(page); // takes over ownership of the page } else { delete page; } } } KoTextDocumentLayout::PaintContext pc; QAbstractTextDocumentLayout::Selection selection; KoTextEditor *textEditor = KoTextDocument(m_textShapeData->document()).textEditor(); selection.cursor = *(textEditor->cursor()); QPalette palette = pc.textContext.palette; selection.format.setBackground(palette.brush(QPalette::Highlight)); selection.format.setForeground(palette.brush(QPalette::HighlightedText)); pc.textContext.selections.append(selection); pc.textContext.selections += KoTextDocument(doc).selections(); pc.viewConverter = &converter; pc.imageCollection = m_imageCollection; pc.showFormattingCharacters = paintContext.showFormattingCharacters; pc.showTableBorders = paintContext.showTableBorders; pc.showSectionBounds = paintContext.showSectionBounds; pc.showSpellChecking = paintContext.showSpellChecking; pc.showSelections = paintContext.showSelections; // When clipping the painter we need to make sure not to cutoff cosmetic pens which // may used to draw e.g. table-borders for user convenience when on screen (but not // on e.g. printing). Such cosmetic pens are special cause they will always have the // same pen-width (1 pixel) independent of zoom-factor or painter transformations and // are not taken into account in any border-calculations. QRectF clipRect = outlineRect(); qreal cosmeticPenX = 1 * 72. / painter.device()->logicalDpiX(); qreal cosmeticPenY = 1 * 72. / painter.device()->logicalDpiY(); painter.setClipRect(clipRect.adjusted(-cosmeticPenX, -cosmeticPenY, cosmeticPenX, cosmeticPenY), Qt::IntersectClip); painter.save(); painter.translate(0, -m_textShapeData->documentOffset()); m_textShapeData->rootArea()->paint(&painter, pc); // only need to draw ourselves painter.restore(); m_paintRegion = QRegion(); } QPointF TextShape::convertScreenPos(const QPointF &point) const { QPointF p = absoluteTransformation(0).inverted().map(point); return p + QPointF(0.0, m_textShapeData->documentOffset()); } QPainterPath TextShape::outline() const { QPainterPath path; path.addRect(QRectF(QPointF(0, 0), size())); return path; } QRectF TextShape::outlineRect() const { if (m_textShapeData->rootArea()) { QRectF rect = m_textShapeData->rootArea()->boundingRect(); rect.moveTop(rect.top() - m_textShapeData->rootArea()->top()); if (m_clip) { rect.setHeight(size().height()); } return rect | QRectF(QPointF(0, 0), size()); } return QRectF(QPointF(0, 0), size()); } void TextShape::shapeChanged(ChangeType type, KoShape *shape) { Q_UNUSED(shape); KoShapeContainer::shapeChanged(type, shape); if (type == PositionChanged || type == SizeChanged /* || type == CollisionDetected*/) { m_textShapeData->setDirty(); } } void TextShape::saveOdf(KoShapeSavingContext &context) const { KoXmlWriter &writer = context.xmlWriter(); QString textHeight = additionalAttribute("fo:min-height"); const_cast(this)->removeAdditionalAttribute("fo:min-height"); writer.startElement("draw:frame"); // if the TextShape is wrapped in a shrink to fit container we need to save the geometry of the container as // the geometry of the shape might have been changed. if (ShrinkToFitShapeContainer *stf = dynamic_cast(this->parent())) { stf->saveOdfAttributes(context, OdfSize | OdfPosition | OdfTransformation); saveOdfAttributes(context, OdfAdditionalAttributes | OdfMandatories | OdfCommonChildElements); } else { saveOdfAttributes(context, OdfAllAttributes); } writer.startElement("draw:text-box"); if (!textHeight.isEmpty()) { writer.addAttribute("fo:min-height", textHeight); } KoTextDocumentLayout *lay = qobject_cast(m_textShapeData->document()->documentLayout()); int index = -1; if (lay) { int i = 0; foreach (KoShape *shape, lay->shapes()) { if (shape == this) { index = i; } else if (index >= 0) { writer.addAttribute("draw:chain-next-name", shape->name()); break; } ++i; } } const bool saveMyText = index == 0; // only save the text once. m_textShapeData->saveOdf(context, 0, 0, saveMyText ? -1 : 0); writer.endElement(); // draw:text-box saveOdfCommonChildElements(context); writer.endElement(); // draw:frame } QString TextShape::saveStyle(KoGenStyle &style, KoShapeSavingContext &context) const { Qt::Alignment vAlign(m_textShapeData->verticalAlignment()); QString verticalAlign = "top"; if (vAlign == Qt::AlignBottom) { verticalAlign = "bottom"; } else if (vAlign == Qt::AlignVCenter) { verticalAlign = "middle"; } style.addProperty("draw:textarea-vertical-align", verticalAlign); KoTextShapeData::ResizeMethod resize = m_textShapeData->resizeMethod(); if (resize == KoTextShapeData::AutoGrowWidth || resize == KoTextShapeData::AutoGrowWidthAndHeight) { style.addProperty("draw:auto-grow-width", "true"); } if (resize != KoTextShapeData::AutoGrowHeight && resize != KoTextShapeData::AutoGrowWidthAndHeight) { style.addProperty("draw:auto-grow-height", "false"); } if (resize == KoTextShapeData::ShrinkToFitResize) { style.addProperty("draw:fit-to-size", "true"); } m_textShapeData->saveStyle(style, context); return KoShape::saveStyle(style, context); } void TextShape::loadStyle(const KoXmlElement &element, KoShapeLoadingContext &context) { KoShape::loadStyle(element, context); KoStyleStack &styleStack = context.odfLoadingContext().styleStack(); styleStack.setTypeProperties("graphic"); QString verticalAlign(styleStack.property(KoXmlNS::draw, "textarea-vertical-align")); Qt::Alignment alignment(Qt::AlignTop); if (verticalAlign == "bottom") { alignment = Qt::AlignBottom; } else if (verticalAlign == "justify") { // not yet supported alignment = Qt::AlignVCenter; } else if (verticalAlign == "middle") { alignment = Qt::AlignVCenter; } m_textShapeData->setVerticalAlignment(alignment); const QString fitToSize = styleStack.property(KoXmlNS::draw, "fit-to-size"); KoTextShapeData::ResizeMethod resize = KoTextShapeData::NoResize; if (fitToSize == "true" || fitToSize == "shrink-to-fit") { // second is buggy value from impress resize = KoTextShapeData::ShrinkToFitResize; } else { // An explicit svg:width or svg:height defined do change the default value (means those value // used if not explicit defined otherwise) for auto-grow-height and auto-grow-height. So // they are mutable exclusive. // It is not clear (means we did not test and took care of it) what happens if both are // defined and are in conflict with each other or how the fit-to-size is related to this. QString autoGrowWidth = styleStack.property(KoXmlNS::draw, "auto-grow-width"); if (autoGrowWidth.isEmpty()) { autoGrowWidth = element.hasAttributeNS(KoXmlNS::svg, "width") ? "false" : "true"; } QString autoGrowHeight = styleStack.property(KoXmlNS::draw, "auto-grow-height"); if (autoGrowHeight.isEmpty()) { autoGrowHeight = element.hasAttributeNS(KoXmlNS::svg, "height") ? "false" : "true"; } if (autoGrowWidth == "true") { resize = autoGrowHeight == "true" ? KoTextShapeData::AutoGrowWidthAndHeight : KoTextShapeData::AutoGrowWidth; } else if (autoGrowHeight == "true") { resize = KoTextShapeData::AutoGrowHeight; } } m_textShapeData->setResizeMethod(resize); } bool TextShape::loadOdf(const KoXmlElement &element, KoShapeLoadingContext &context) { m_textShapeData->document()->setUndoRedoEnabled(false); loadOdfAttributes(element, context, OdfAllAttributes); // this cannot be done in loadStyle as that fills the style stack incorrectly and therefore // it results in wrong data being loaded. m_textShapeData->loadStyle(element, context); #ifndef NWORKAROUND_ODF_BUGS KoTextShapeData::ResizeMethod method = m_textShapeData->resizeMethod(); if (KoOdfWorkaround::fixAutoGrow(method, context)) { KoTextDocumentLayout *lay = qobject_cast(m_textShapeData->document()->documentLayout()); Q_ASSERT(lay); if (lay) { SimpleRootAreaProvider *provider = dynamic_cast(lay->provider()); if (provider) { provider->m_fixAutogrow = true; } } } #endif bool answer = loadOdfFrame(element, context); m_textShapeData->document()->setUndoRedoEnabled(true); return answer; } bool TextShape::loadOdfFrame(const KoXmlElement &element, KoShapeLoadingContext &context) { // If the loadOdfFrame from the base class for draw:text-box fails, check // for table:table, because that is a legal child of draw:frame in ODF 1.2. if (!KoFrameShape::loadOdfFrame(element, context)) { const KoXmlElement &possibleTableElement(KoXml::namedItemNS(element, KoXmlNS::table, "table")); if (possibleTableElement.isNull()) { return false; } else { return loadOdfFrameElement(possibleTableElement, context); } } return true; } bool TextShape::loadOdfFrameElement(const KoXmlElement &element, KoShapeLoadingContext &context) { bool ok = m_textShapeData->loadOdf(element, context, 0, this); if (ok) { ShrinkToFitShapeContainer::tryWrapShape(this, element, context); } return ok; } void TextShape::update() const { KoShapeContainer::update(); } void TextShape::updateAbsolute(const QRectF &shape) const { // this is done to avoid updates which are called during the paint event and not needed. //if (!m_paintRegion.contains(shape.toRect())) { //KoShape::update(shape); //} // FIXME: just a hack to remove outdated call from the deprecated shape KoShape::updateAbsolute(shape); } void TextShape::waitUntilReady(const KoViewConverter &, bool asynchronous) const { Q_UNUSED(asynchronous); KoTextDocumentLayout *lay = qobject_cast(m_textShapeData->document()->documentLayout()); Q_ASSERT(lay); if (m_textShapeData->isDirty()) { // Do a simple layout-call which will make sure to relayout till things are done. If more // layouts are scheduled then we don't need to wait for them here but can just continue. lay->layout(); } } KoImageCollection *TextShape::imageCollection() { return m_imageCollection; } void TextShape::updateDocumentData() { if (m_layout) { KoTextDocument document(m_textShapeData->document()); m_layout->setStyleManager(document.styleManager()); m_layout->setInlineTextObjectManager(document.inlineTextObjectManager()); m_layout->setTextRangeManager(document.textRangeManager()); m_layout->setChangeTracker(document.changeTracker()); } } diff --git a/plugins/paintops/curvebrush/curve_brush.cpp b/plugins/paintops/curvebrush/curve_brush.cpp index 347cc317e4..82bc86be23 100644 --- a/plugins/paintops/curvebrush/curve_brush.cpp +++ b/plugins/paintops/curvebrush/curve_brush.cpp @@ -1,179 +1,181 @@ /* * Copyright (c) 2008,2010 Lukáš Tvrdý * * 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 "curve_brush.h" +#include + #include #include #include #include #include #include "kis_curve_line_option.h" #if defined(_WIN32) || defined(_WIN64) #define srand48 srand inline double drand48() { return double(rand()) / RAND_MAX; } #endif CurveBrush::CurveBrush() : m_painter(0), m_branch(0) { srand48(time(0)); m_pens.reserve(1024); } CurveBrush::~CurveBrush() { delete m_painter; } QPointF CurveBrush::getLinearBezier(const QPointF &p1, const QPointF &p2, qreal u) { qreal rx = (1.0 - u) * p1.x() + u * p2.x(); qreal ry = (1.0 - u) * p1.y() + u * p2.y(); return QPointF(rx, ry); } QPointF CurveBrush::getQuadraticBezier(const QPointF &p0, const QPointF &p1, const QPointF &p2, qreal u) { qreal rx = pow((1.0 - u), 2) * p0.x() + 2 * u * (1.0 - u) * p1.x() + pow(u, 2) * p2.x(); qreal ry = pow((1.0 - u), 2) * p0.y() + 2 * u * (1.0 - u) * p1.y() + pow(u, 2) * p2.y(); return QPointF(rx, ry); } QPointF CurveBrush::getCubicBezier(const QPointF &p0, const QPointF &p1, const QPointF &p2, const QPointF &p3, qreal u) { qreal rx = pow((1.0 - u), 3) * p0.x() + 3.0 * u * pow((1.0 - u), 2) * p1.x() + 3.0 * pow(u, 2) * (1.0 - u) * p2.x() + pow(u, 3) * p3.x(); qreal ry = pow((1.0 - u), 3) * p0.y() + 3.0 * u * pow((1.0 - u), 2) * p1.y() + 3.0 * pow(u, 2) * (1.0 - u) * p2.y() + pow(u, 3) * p3.y(); return QPointF(rx, ry); } void CurveBrush::putPixel(QPointF pos, KoColor &color) { int ipx = int (pos.x()); int ipy = int (pos.y()); qreal fx = pos.x() - ipx; qreal fy = pos.y() - ipy; qreal btl = (1 - fx) * (1 - fy); qreal btr = (fx) * (1 - fy); qreal bbl = (1 - fx) * (fy); qreal bbr = (fx) * (fy); color.setOpacity(btl); m_writeAccessor->moveTo(ipx , ipy); if (cs->opacityU8(m_writeAccessor->rawData()) < color.opacityU8()) { memcpy(m_writeAccessor->rawData(), color.data(), m_pixelSize); } color.setOpacity(btr); m_writeAccessor->moveTo(ipx + 1, ipy); if (cs->opacityU8(m_writeAccessor->rawData()) < color.opacityU8()) { memcpy(m_writeAccessor->rawData(), color.data(), m_pixelSize); } color.setOpacity(bbl); m_writeAccessor->moveTo(ipx, ipy + 1); if (cs->opacityU8(m_writeAccessor->rawData()) < color.opacityU8()) { memcpy(m_writeAccessor->rawData(), color.data(), m_pixelSize); } color.setOpacity(bbr); m_writeAccessor->moveTo(ipx + 1, ipy + 1); if (cs->opacityU8(m_writeAccessor->rawData()) < color.opacityU8()) { memcpy(m_writeAccessor->rawData(), color.data(), m_pixelSize); } } void CurveBrush::strokePens(QPointF pi1, QPointF pi2, KisPainter &/*painter*/) { if (m_pens.isEmpty()) { m_pens.append(Pen(pi1, 0.0, 1.0)); } qreal dx = pi2.x() - pi1.x(); qreal dy = pi2.y() - pi1.y(); for (int i = 0; i < m_pens.length(); i++) { Pen &pen = m_pens[i]; QPointF endPoint(dx, dy); QPainterPath path; path.moveTo(0, 0); path.lineTo(dx, dy); QTransform transform; transform.reset(); transform.translate(pen.pos.x(), pen.pos.y()); transform.scale(pen.scale, pen.scale); transform.rotate(pen.rotation); path = transform.map(path); //m_painter->drawPainterPath(path, QPen(Qt::white, 1.0)); endPoint = transform.map(endPoint); m_painter->drawThickLine(pen.pos, endPoint, 1.0, 1.0); pen.pos = endPoint; } qreal branchThreshold = 0.5; if ((m_branch * drand48() > branchThreshold) && (m_pens.length() < 1024)) { int index = floor(drand48() * (m_pens.length() - 1)); m_newPen.pos = m_pens.at(index).pos; m_newPen.rotation = drand48() * M_PI / 32; //atan(dy/dx) + (drand48() - 0.5) * M_PI/32; m_newPen.scale = drand48() * m_pens.at(index).scale; m_pens.append(m_newPen); dbgKrita << m_pens.length(); m_branch = 0; } else { m_branch++; } } diff --git a/plugins/paintops/curvebrush/kis_curve_paintop.cpp b/plugins/paintops/curvebrush/kis_curve_paintop.cpp index 295ee55d0b..ed68e3fe83 100644 --- a/plugins/paintops/curvebrush/kis_curve_paintop.cpp +++ b/plugins/paintops/curvebrush/kis_curve_paintop.cpp @@ -1,133 +1,134 @@ /* * Copyright (c) 2008-2011 Lukáš Tvrdý * * 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_curve_paintop.h" #include +#include #include #include #include #include "kis_global.h" #include "kis_paint_device.h" #include "kis_painter.h" #include "kis_types.h" #include "kis_spacing_information.h" #include KisCurvePaintOp::KisCurvePaintOp(const KisPaintOpSettingsSP settings, KisPainter * painter, KisNodeSP node, KisImageSP image) : KisPaintOp(painter) , m_painter(0) { Q_ASSERT(settings); Q_UNUSED(image); Q_UNUSED(node); m_curveProperties.readOptionSetting(settings); m_opacityOption.readOptionSetting(settings); m_lineWidthOption.readOptionSetting(settings); m_curvesOpacityOption.readOptionSetting(settings); } KisCurvePaintOp::~KisCurvePaintOp() { delete m_painter; } KisSpacingInformation KisCurvePaintOp::paintAt(const KisPaintInformation& info) { return updateSpacingImpl(info); } KisSpacingInformation KisCurvePaintOp::updateSpacingImpl(const KisPaintInformation &info) const { Q_UNUSED(info); return KisSpacingInformation(1.0); } void KisCurvePaintOp::paintLine(const KisPaintInformation &pi1, const KisPaintInformation &pi2, KisDistanceInformation *currentDistance) { Q_UNUSED(currentDistance); if (!painter()) return; if (!m_dab) { m_dab = source()->createCompositionSourceDevice(); } else { m_dab->clear(); } paintLine(m_dab, pi1, pi2); QRect rc = m_dab->extent(); quint8 origOpacity = m_opacityOption.apply(painter(), pi2); painter()->bitBlt(rc.topLeft(), m_dab, rc); painter()->renderMirrorMask(rc, m_dab); painter()->setOpacity(origOpacity); } void KisCurvePaintOp::paintLine(KisPaintDeviceSP dab, const KisPaintInformation &pi1, const KisPaintInformation &pi2) { if (!m_painter) { m_painter = new KisPainter(dab); m_painter->setPaintColor(painter()->paintColor()); } int maxPoints = m_curveProperties.curve_stroke_history_size; m_points.append(pi2.pos()); while (m_points.length() > maxPoints) { m_points.removeFirst(); } const qreal additionalScale = KisLodTransform::lodToScale(painter()->device()); const qreal lineWidth = additionalScale * m_lineWidthOption.apply(pi2, m_curveProperties.curve_line_width); QPen pen(QBrush(Qt::white), lineWidth); QPainterPath path; if (m_curveProperties.curve_paint_connection_line) { path.moveTo(pi1.pos()); path.lineTo(pi2.pos()); m_painter->drawPainterPath(path, pen); path = QPainterPath(); } if (m_points.length() >= maxPoints) { // alpha * 0.2; path.moveTo(m_points.first()); if (m_curveProperties.curve_smoothing) { path.quadTo(m_points.at(maxPoints / 2), m_points.last()); } else { // control point is at 1/3 of the history, 2/3 of the history and endpoint at 3/3 int step = maxPoints / 3; path.cubicTo(m_points.at(step), m_points.at(step + step), m_points.last()); } qreal curveOpacity = m_curvesOpacityOption.apply(pi2, m_curveProperties.curve_curves_opacity); m_painter->setOpacity(qRound(255.0 * curveOpacity)); m_painter->drawPainterPath(path, pen); m_painter->setOpacity(255); // full } } diff --git a/plugins/paintops/experiment/kis_experiment_paintop.cpp b/plugins/paintops/experiment/kis_experiment_paintop.cpp index 54764d8233..7c429b4ae1 100644 --- a/plugins/paintops/experiment/kis_experiment_paintop.cpp +++ b/plugins/paintops/experiment/kis_experiment_paintop.cpp @@ -1,355 +1,357 @@ /* * Copyright (c) 2010-2011 Lukáš Tvrdý * Copyright (c) 2012 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_experiment_paintop.h" #include "kis_experiment_paintop_settings.h" #include +#include + #include #include #include #include #include #include #include KisExperimentPaintOp::KisExperimentPaintOp(const KisPaintOpSettingsSP settings, KisPainter *painter, KisNodeSP node, KisImageSP image) : KisPaintOp(painter) { Q_UNUSED(image); Q_UNUSED(node); m_firstRun = true; m_experimentOption.readOptionSetting(settings); m_displaceEnabled = m_experimentOption.isDisplacementEnabled; m_displaceCoeff = (m_experimentOption.displacement * 0.01 * 14) + 1; // 1..15 [7 default according alchemy] m_speedEnabled = m_experimentOption.isSpeedEnabled; m_speedMultiplier = (m_experimentOption.speed * 0.01 * 35); // 0..35 [15 default according alchemy] m_smoothingEnabled = m_experimentOption.isSmoothingEnabled; m_smoothingThreshold = m_experimentOption.smoothing; m_useMirroring = painter->hasMirroring(); m_windingFill = m_experimentOption.windingFill; m_hardEdge = m_experimentOption.hardEdge; //Sets the brush to pattern or foregroundColor if (m_experimentOption.fillType == ExperimentFillType::Pattern) { m_fillStyle = KisPainter::FillStylePattern; } else { m_fillStyle = KisPainter::FillStyleForegroundColor; } // Mirror options set with appropriate color, pattern, and fillStyle if (m_useMirroring) { m_originalDevice = source()->createCompositionSourceDevice(); m_originalPainter = new KisPainter(m_originalDevice); m_originalPainter->setCompositeOp(COMPOSITE_COPY); m_originalPainter->setPaintColor(painter->paintColor()); m_originalPainter->setPattern(painter->pattern()); m_originalPainter->setFillStyle(m_fillStyle); } else { m_originalPainter = 0; } } KisExperimentPaintOp::~KisExperimentPaintOp() { delete m_originalPainter; } void KisExperimentPaintOp::paintRegion(const QRegion &changedRegion) { if (m_windingFill) { m_path.setFillRule(Qt::WindingFill); } if (m_useMirroring) { m_originalPainter->setAntiAliasPolygonFill(!m_hardEdge); Q_FOREACH (const QRect & rect, changedRegion.rects()) { m_originalPainter->fillPainterPath(m_path, rect); painter()->renderDabWithMirroringNonIncremental(rect, m_originalDevice); } } else { //Sets options when mirror is not selected painter()->setFillStyle(m_fillStyle); painter()->setCompositeOp(COMPOSITE_COPY); painter()->setAntiAliasPolygonFill(!m_hardEdge); Q_FOREACH (const QRect & rect, changedRegion.rects()) { painter()->fillPainterPath(m_path, rect); } } } QPointF KisExperimentPaintOp::speedCorrectedPosition(const KisPaintInformation& pi1, const KisPaintInformation& pi2) { const qreal fadeFactor = 0.6; QPointF diff = pi2.pos() - pi1.pos(); qreal realLength = sqrt(diff.x() * diff.x() + diff.y() * diff.y()); if (realLength < 0.1) return pi2.pos(); qreal coeff = 0.5 * realLength * m_speedMultiplier; m_savedSpeedCoeff = fadeFactor * m_savedSpeedCoeff + (1 - fadeFactor) * coeff; QPointF newPoint = pi1.pos() + diff * m_savedSpeedCoeff / realLength; m_savedSpeedPoint = fadeFactor * m_savedSpeedPoint + (1 - fadeFactor) * newPoint; return m_savedSpeedPoint; } void KisExperimentPaintOp::paintLine(const KisPaintInformation &pi1, const KisPaintInformation &pi2, KisDistanceInformation *currentDistance) { Q_UNUSED(currentDistance); if (!painter()) return; if (m_firstRun) { m_firstRun = false; m_path.moveTo(pi1.pos()); m_path.lineTo(pi2.pos()); m_center = pi1.pos(); m_savedUpdateDistance = 0; m_lastPaintTime = 0; m_savedSpeedCoeff = 0; m_savedSpeedPoint = m_center; m_savedSmoothingDistance = 0; m_savedSmoothingPoint = m_center; } else { const QPointF pos1 = pi1.pos(); QPointF pos2 = pi2.pos(); if (m_speedEnabled) { pos2 = speedCorrectedPosition(pi1, pi2); } int length = (pos2 - pos1).manhattanLength(); m_savedUpdateDistance += length; if (m_smoothingEnabled) { m_savedSmoothingDistance += length; if (m_savedSmoothingDistance > m_smoothingThreshold) { QPointF pt = (m_savedSmoothingPoint + pos2) * 0.5; // for updates approximate curve with two lines m_savedPoints << m_path.currentPosition(); m_savedPoints << m_savedSmoothingPoint; m_savedPoints << m_savedSmoothingPoint; m_savedPoints << pt; m_path.quadTo(m_savedSmoothingPoint, pt); m_savedSmoothingPoint = pos2; m_savedSmoothingDistance = 0; } } else { m_path.lineTo(pos2); m_savedPoints << pos1; m_savedPoints << pos2; } if (m_displaceEnabled) { if (m_path.elementCount() % 16 == 0) { QRectF bounds = m_path.boundingRect(); m_path = applyDisplace(m_path, m_displaceCoeff - length); bounds |= m_path.boundingRect(); qreal threshold = simplifyThreshold(bounds); m_path = KritaUtils::trySimplifyPath(m_path, threshold); } else { m_path = applyDisplace(m_path, m_displaceCoeff - length); } } /** * Refresh rate at least 25fps */ const int timeThreshold = 40; const int elapsedTime = pi2.currentTime() - m_lastPaintTime; QRect pathBounds = m_path.boundingRect().toRect(); int distanceMetric = qMax(pathBounds.width(), pathBounds.height()); if (elapsedTime > timeThreshold || (!m_displaceEnabled && m_savedUpdateDistance > distanceMetric / 8)) { if (m_displaceEnabled) { /** * Rendering the path with diff'ed rects is up to two * times more efficient for really huge shapes (tested * on 2000+ px shapes), however for smaller ones doing * paths arithmetics eats too much time. That's why we * choose the method on the base of the size of the * shape. */ const int pathSizeThreshold = 128; QRegion changedRegion; if (distanceMetric < pathSizeThreshold) { QRectF changedRect = m_path.boundingRect().toRect() | m_lastPaintedPath.boundingRect().toRect(); changedRect.adjust(-1, -1, 1, 1); changedRegion = changedRect.toRect(); } else { QPainterPath diff1 = m_path - m_lastPaintedPath; QPainterPath diff2 = m_lastPaintedPath - m_path; changedRegion = KritaUtils::splitPath(diff1 | diff2); } paintRegion(changedRegion); m_lastPaintedPath = m_path; } else if (!m_savedPoints.isEmpty()) { QRegion changedRegion = KritaUtils::splitTriangles(m_center, m_savedPoints); paintRegion(changedRegion); } m_savedPoints.clear(); m_savedUpdateDistance = 0; m_lastPaintTime = pi2.currentTime(); } } } KisSpacingInformation KisExperimentPaintOp::paintAt(const KisPaintInformation& info) { return updateSpacingImpl(info); } KisSpacingInformation KisExperimentPaintOp::updateSpacingImpl(const KisPaintInformation &info) const { Q_UNUSED(info); return KisSpacingInformation(1.0); } bool tryMergePoints(QPainterPath &path, const QPointF &startPoint, const QPointF &endPoint, qreal &distance, qreal distanceThreshold, bool lastSegment) { qreal length = (endPoint - startPoint).manhattanLength(); if (lastSegment || length > distanceThreshold) { if (distance != 0) { path.lineTo(startPoint); } distance = 0; return false; } distance += length; if (distance > distanceThreshold) { path.lineTo(endPoint); distance = 0; } return true; } qreal KisExperimentPaintOp::simplifyThreshold(const QRectF &bounds) { qreal maxDimension = qMax(bounds.width(), bounds.height()); return qMax(0.01 * maxDimension, 1.0); } QPointF KisExperimentPaintOp::getAngle(const QPointF& p1, const QPointF& p2, qreal distance) { QPointF diff = p1 - p2; qreal realLength = sqrt(diff.x() * diff.x() + diff.y() * diff.y()); return realLength > 0.5 ? p1 + diff * distance / realLength : p1; } QPainterPath KisExperimentPaintOp::applyDisplace(const QPainterPath& path, int speed) { QPointF lastPoint = path.currentPosition(); QPainterPath newPath; int count = path.elementCount(); int curveElementCounter = 0; QPointF ctrl1; QPointF ctrl2; QPointF endPoint; for (int i = 0; i < count; i++) { QPainterPath::Element e = path.elementAt(i); switch (e.type) { case QPainterPath::MoveToElement: { newPath.moveTo(getAngle(QPointF(e.x, e.y), lastPoint, speed)); break; } case QPainterPath::LineToElement: { newPath.lineTo(getAngle(QPointF(e.x, e.y), lastPoint, speed)); break; } case QPainterPath::CurveToElement: { curveElementCounter = 0; endPoint = getAngle(QPointF(e.x, e.y), lastPoint, speed); break; } case QPainterPath::CurveToDataElement: { curveElementCounter++; if (curveElementCounter == 1) { ctrl1 = getAngle(QPointF(e.x, e.y), lastPoint, speed); } else if (curveElementCounter == 2) { ctrl2 = getAngle(QPointF(e.x, e.y), lastPoint, speed); newPath.cubicTo(ctrl1, ctrl2, endPoint); } break; } } }// for return newPath; } diff --git a/plugins/paintops/experiment/kis_experiment_paintop.h b/plugins/paintops/experiment/kis_experiment_paintop.h index 19dec9b9d8..27f4fe3616 100644 --- a/plugins/paintops/experiment/kis_experiment_paintop.h +++ b/plugins/paintops/experiment/kis_experiment_paintop.h @@ -1,94 +1,96 @@ /* * Copyright (c) 2010-2011 Lukáš Tvrdý * * 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_EXPERIMENT_PAINTOP_H_ #define KIS_EXPERIMENT_PAINTOP_H_ +#include + #include #include #include #include "kis_experiment_paintop_settings.h" #include "kis_experimentop_option.h" #include class QPointF; class KisPainter; class KisExperimentPaintOp : public KisPaintOp { public: KisExperimentPaintOp(const KisPaintOpSettingsSP settings, KisPainter *painter, KisNodeSP node, KisImageSP image); ~KisExperimentPaintOp() override; void paintLine(const KisPaintInformation& pi1, const KisPaintInformation& pi2, KisDistanceInformation *currentDistance) override; protected: KisSpacingInformation paintAt(const KisPaintInformation& info) override; KisSpacingInformation updateSpacingImpl(const KisPaintInformation &info) const override; private: void paintRegion(const QRegion &changedRegion); QPointF speedCorrectedPosition(const KisPaintInformation& pi1, const KisPaintInformation& pi2); static qreal simplifyThreshold(const QRectF &bounds); static QPointF getAngle(const QPointF& p1, const QPointF& p2, qreal distance); static QPainterPath applyDisplace(const QPainterPath& path, int speed); bool m_displaceEnabled {false}; int m_displaceCoeff {0}; QPainterPath m_lastPaintedPath; bool m_windingFill {false}; bool m_hardEdge {false}; bool m_speedEnabled {false}; int m_speedMultiplier {1}; qreal m_savedSpeedCoeff {1.0}; QPointF m_savedSpeedPoint; bool m_smoothingEnabled {false}; int m_smoothingThreshold {1}; QPointF m_savedSmoothingPoint; int m_savedSmoothingDistance {1}; int m_savedUpdateDistance {1}; QVector m_savedPoints; int m_lastPaintTime {0}; bool m_firstRun {true}; QPointF m_center; QPainterPath m_path; ExperimentOption m_experimentOption; bool m_useMirroring {false}; KisPainter *m_originalPainter {0}; KisPaintDeviceSP m_originalDevice; KisPainter::FillStyle m_fillStyle {KisPainter::FillStyleNone}; }; #endif // KIS_EXPERIMENT_PAINTOP_H_ diff --git a/plugins/tools/defaulttool/defaulttool/DefaultTool.cpp b/plugins/tools/defaulttool/defaulttool/DefaultTool.cpp index de2787fc11..363025ed60 100644 --- a/plugins/tools/defaulttool/defaulttool/DefaultTool.cpp +++ b/plugins/tools/defaulttool/defaulttool/DefaultTool.cpp @@ -1,1735 +1,1736 @@ /* This file is part of the KDE project Copyright (C) 2006-2008 Thorsten Zachmann Copyright (C) 2006-2010 Thomas Zander Copyright (C) 2008-2009 Jan Hambrecht Copyright (C) 2008 C. Boemann This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "DefaultTool.h" #include "DefaultToolGeometryWidget.h" #include "DefaultToolTabbedWidget.h" #include "SelectionDecorator.h" #include "ShapeMoveStrategy.h" #include "ShapeRotateStrategy.h" #include "ShapeShearStrategy.h" #include "ShapeResizeStrategy.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_action_registry.h" #include "kis_node.h" #include "kis_node_manager.h" #include "KisViewManager.h" #include "kis_canvas2.h" #include "kis_canvas_resource_provider.h" #include #include "kis_document_aware_spin_box_unit_manager.h" #include +#include #include #include #include #include #include #include #include #include #include #include "kis_assert.h" #include "kis_global.h" #include "kis_debug.h" #include #define HANDLE_DISTANCE 10 #define HANDLE_DISTANCE_SQ (HANDLE_DISTANCE * HANDLE_DISTANCE) #define INNER_HANDLE_DISTANCE_SQ 16 namespace { static const QString EditFillGradientFactoryId = "edit_fill_gradient"; static const QString EditStrokeGradientFactoryId = "edit_stroke_gradient"; enum TransformActionType { TransformRotate90CW, TransformRotate90CCW, TransformRotate180, TransformMirrorX, TransformMirrorY, TransformReset }; enum BooleanOp { BooleanUnion, BooleanIntersection, BooleanSubtraction }; } class NopInteractionStrategy : public KoInteractionStrategy { public: explicit NopInteractionStrategy(KoToolBase *parent) : KoInteractionStrategy(parent) { } KUndo2Command *createCommand() override { return 0; } void handleMouseMove(const QPointF & /*mouseLocation*/, Qt::KeyboardModifiers /*modifiers*/) override {} void finishInteraction(Qt::KeyboardModifiers /*modifiers*/) override {} void paint(QPainter &painter, const KoViewConverter &converter) override { Q_UNUSED(painter); Q_UNUSED(converter); } }; class SelectionInteractionStrategy : public KoShapeRubberSelectStrategy { public: explicit SelectionInteractionStrategy(KoToolBase *parent, const QPointF &clicked, bool useSnapToGrid) : KoShapeRubberSelectStrategy(parent, clicked, useSnapToGrid) { } void paint(QPainter &painter, const KoViewConverter &converter) override { KoShapeRubberSelectStrategy::paint(painter, converter); } void finishInteraction(Qt::KeyboardModifiers modifiers = 0) override { Q_UNUSED(modifiers); DefaultTool *defaultTool = dynamic_cast(tool()); KIS_SAFE_ASSERT_RECOVER_RETURN(defaultTool); KoSelection * selection = defaultTool->koSelection(); const bool useContainedMode = currentMode() == CoveringSelection; QList shapes = defaultTool->shapeManager()-> shapesAt(selectedRectangle(), true, useContainedMode); Q_FOREACH (KoShape * shape, shapes) { if (!shape->isSelectable()) continue; selection->select(shape); } defaultTool->repaintDecorations(); defaultTool->canvas()->updateCanvas(selectedRectangle()); } }; #include #include "KoShapeGradientHandles.h" #include "ShapeGradientEditStrategy.h" class DefaultTool::MoveGradientHandleInteractionFactory : public KoInteractionStrategyFactory { public: MoveGradientHandleInteractionFactory(KoFlake::FillVariant fillVariant, int priority, const QString &id, DefaultTool *_q) : KoInteractionStrategyFactory(priority, id), q(_q), m_fillVariant(fillVariant) { } KoInteractionStrategy* createStrategy(KoPointerEvent *ev) override { m_currentHandle = handleAt(ev->point); if (m_currentHandle.type != KoShapeGradientHandles::Handle::None) { KoShape *shape = onlyEditableShape(); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(shape, 0); return new ShapeGradientEditStrategy(q, m_fillVariant, shape, m_currentHandle.type, ev->point); } return 0; } bool hoverEvent(KoPointerEvent *ev) override { m_currentHandle = handleAt(ev->point); return false; } bool paintOnHover(QPainter &painter, const KoViewConverter &converter) override { Q_UNUSED(painter); Q_UNUSED(converter); return false; } bool tryUseCustomCursor() override { if (m_currentHandle.type != KoShapeGradientHandles::Handle::None) { q->useCursor(Qt::OpenHandCursor); } return m_currentHandle.type != KoShapeGradientHandles::Handle::None; } private: KoShape* onlyEditableShape() const { KoSelection *selection = q->koSelection(); QList shapes = selection->selectedEditableShapes(); KoShape *shape = 0; if (shapes.size() == 1) { shape = shapes.first(); } return shape; } KoShapeGradientHandles::Handle handleAt(const QPointF &pos) { KoShapeGradientHandles::Handle result; KoShape *shape = onlyEditableShape(); if (shape) { KoFlake::SelectionHandle globalHandle = q->handleAt(pos); const qreal distanceThresholdSq = globalHandle == KoFlake::NoHandle ? HANDLE_DISTANCE_SQ : 0.25 * HANDLE_DISTANCE_SQ; const KoViewConverter *converter = q->canvas()->viewConverter(); const QPointF viewPoint = converter->documentToView(pos); qreal minDistanceSq = std::numeric_limits::max(); KoShapeGradientHandles sh(m_fillVariant, shape); Q_FOREACH (const KoShapeGradientHandles::Handle &handle, sh.handles()) { const QPointF handlePoint = converter->documentToView(handle.pos); const qreal distanceSq = kisSquareDistance(viewPoint, handlePoint); if (distanceSq < distanceThresholdSq && distanceSq < minDistanceSq) { result = handle; minDistanceSq = distanceSq; } } } return result; } private: DefaultTool *q; KoFlake::FillVariant m_fillVariant; KoShapeGradientHandles::Handle m_currentHandle; }; class SelectionHandler : public KoToolSelection { public: SelectionHandler(DefaultTool *parent) : KoToolSelection(parent) , m_selection(parent->koSelection()) { } bool hasSelection() override { if (m_selection) { return m_selection->count(); } return false; } private: QPointer m_selection; }; DefaultTool::DefaultTool(KoCanvasBase *canvas, bool connectToSelectedShapesProxy) : KoInteractionTool(canvas) , m_lastHandle(KoFlake::NoHandle) , m_hotPosition(KoFlake::TopLeft) , m_mouseWasInsideHandles(false) , m_decorator(0) , m_selectionHandler(new SelectionHandler(this)) , m_tabbedOptionWidget(0) { setupActions(); QPixmap rotatePixmap, shearPixmap; rotatePixmap.load(":/cursor_rotate.png"); Q_ASSERT(!rotatePixmap.isNull()); shearPixmap.load(":/cursor_shear.png"); Q_ASSERT(!shearPixmap.isNull()); m_rotateCursors[0] = QCursor(rotatePixmap.transformed(QTransform().rotate(45))); m_rotateCursors[1] = QCursor(rotatePixmap.transformed(QTransform().rotate(90))); m_rotateCursors[2] = QCursor(rotatePixmap.transformed(QTransform().rotate(135))); m_rotateCursors[3] = QCursor(rotatePixmap.transformed(QTransform().rotate(180))); m_rotateCursors[4] = QCursor(rotatePixmap.transformed(QTransform().rotate(225))); m_rotateCursors[5] = QCursor(rotatePixmap.transformed(QTransform().rotate(270))); m_rotateCursors[6] = QCursor(rotatePixmap.transformed(QTransform().rotate(315))); m_rotateCursors[7] = QCursor(rotatePixmap); /* m_rotateCursors[0] = QCursor(Qt::RotateNCursor); m_rotateCursors[1] = QCursor(Qt::RotateNECursor); m_rotateCursors[2] = QCursor(Qt::RotateECursor); m_rotateCursors[3] = QCursor(Qt::RotateSECursor); m_rotateCursors[4] = QCursor(Qt::RotateSCursor); m_rotateCursors[5] = QCursor(Qt::RotateSWCursor); m_rotateCursors[6] = QCursor(Qt::RotateWCursor); m_rotateCursors[7] = QCursor(Qt::RotateNWCursor); */ m_shearCursors[0] = QCursor(shearPixmap); m_shearCursors[1] = QCursor(shearPixmap.transformed(QTransform().rotate(45))); m_shearCursors[2] = QCursor(shearPixmap.transformed(QTransform().rotate(90))); m_shearCursors[3] = QCursor(shearPixmap.transformed(QTransform().rotate(135))); m_shearCursors[4] = QCursor(shearPixmap.transformed(QTransform().rotate(180))); m_shearCursors[5] = QCursor(shearPixmap.transformed(QTransform().rotate(225))); m_shearCursors[6] = QCursor(shearPixmap.transformed(QTransform().rotate(270))); m_shearCursors[7] = QCursor(shearPixmap.transformed(QTransform().rotate(315))); m_sizeCursors[0] = Qt::SizeVerCursor; m_sizeCursors[1] = Qt::SizeBDiagCursor; m_sizeCursors[2] = Qt::SizeHorCursor; m_sizeCursors[3] = Qt::SizeFDiagCursor; m_sizeCursors[4] = Qt::SizeVerCursor; m_sizeCursors[5] = Qt::SizeBDiagCursor; m_sizeCursors[6] = Qt::SizeHorCursor; m_sizeCursors[7] = Qt::SizeFDiagCursor; if (connectToSelectedShapesProxy) { connect(canvas->selectedShapesProxy(), SIGNAL(selectionChanged()), this, SLOT(updateActions())); } } DefaultTool::~DefaultTool() { } void DefaultTool::slotActivateEditFillGradient(bool value) { if (value) { addInteractionFactory( new MoveGradientHandleInteractionFactory(KoFlake::Fill, 1, EditFillGradientFactoryId, this)); } else { removeInteractionFactory(EditFillGradientFactoryId); } repaintDecorations(); } void DefaultTool::slotActivateEditStrokeGradient(bool value) { if (value) { addInteractionFactory( new MoveGradientHandleInteractionFactory(KoFlake::StrokeFill, 0, EditStrokeGradientFactoryId, this)); } else { removeInteractionFactory(EditStrokeGradientFactoryId); } repaintDecorations(); } bool DefaultTool::wantsAutoScroll() const { return true; } void DefaultTool::addMappedAction(QSignalMapper *mapper, const QString &actionId, int commandType) { QAction *a =action(actionId); connect(a, SIGNAL(triggered()), mapper, SLOT(map())); mapper->setMapping(a, commandType); } void DefaultTool::setupActions() { m_alignSignalsMapper = new QSignalMapper(this); addMappedAction(m_alignSignalsMapper, "object_align_horizontal_left", KoShapeAlignCommand::HorizontalLeftAlignment); addMappedAction(m_alignSignalsMapper, "object_align_horizontal_center", KoShapeAlignCommand::HorizontalCenterAlignment); addMappedAction(m_alignSignalsMapper, "object_align_horizontal_right", KoShapeAlignCommand::HorizontalRightAlignment); addMappedAction(m_alignSignalsMapper, "object_align_vertical_top", KoShapeAlignCommand::VerticalTopAlignment); addMappedAction(m_alignSignalsMapper, "object_align_vertical_center", KoShapeAlignCommand::VerticalCenterAlignment); addMappedAction(m_alignSignalsMapper, "object_align_vertical_bottom", KoShapeAlignCommand::VerticalBottomAlignment); m_distributeSignalsMapper = new QSignalMapper(this); addMappedAction(m_distributeSignalsMapper, "object_distribute_horizontal_left", KoShapeDistributeCommand::HorizontalLeftDistribution); addMappedAction(m_distributeSignalsMapper, "object_distribute_horizontal_center", KoShapeDistributeCommand::HorizontalCenterDistribution); addMappedAction(m_distributeSignalsMapper, "object_distribute_horizontal_right", KoShapeDistributeCommand::HorizontalRightDistribution); addMappedAction(m_distributeSignalsMapper, "object_distribute_horizontal_gaps", KoShapeDistributeCommand::HorizontalGapsDistribution); addMappedAction(m_distributeSignalsMapper, "object_distribute_vertical_top", KoShapeDistributeCommand::VerticalTopDistribution); addMappedAction(m_distributeSignalsMapper, "object_distribute_vertical_center", KoShapeDistributeCommand::VerticalCenterDistribution); addMappedAction(m_distributeSignalsMapper, "object_distribute_vertical_bottom", KoShapeDistributeCommand::VerticalBottomDistribution); addMappedAction(m_distributeSignalsMapper, "object_distribute_vertical_gaps", KoShapeDistributeCommand::VerticalGapsDistribution); m_transformSignalsMapper = new QSignalMapper(this); addMappedAction(m_transformSignalsMapper, "object_transform_rotate_90_cw", TransformRotate90CW); addMappedAction(m_transformSignalsMapper, "object_transform_rotate_90_ccw", TransformRotate90CCW); addMappedAction(m_transformSignalsMapper, "object_transform_rotate_180", TransformRotate180); addMappedAction(m_transformSignalsMapper, "object_transform_mirror_horizontally", TransformMirrorX); addMappedAction(m_transformSignalsMapper, "object_transform_mirror_vertically", TransformMirrorY); addMappedAction(m_transformSignalsMapper, "object_transform_reset", TransformReset); m_booleanSignalsMapper = new QSignalMapper(this); addMappedAction(m_booleanSignalsMapper, "object_unite", BooleanUnion); addMappedAction(m_booleanSignalsMapper, "object_intersect", BooleanIntersection); addMappedAction(m_booleanSignalsMapper, "object_subtract", BooleanSubtraction); m_contextMenu.reset(new QMenu()); } qreal DefaultTool::rotationOfHandle(KoFlake::SelectionHandle handle, bool useEdgeRotation) { QPointF selectionCenter = koSelection()->absolutePosition(); QPointF direction; switch (handle) { case KoFlake::TopMiddleHandle: if (useEdgeRotation) { direction = koSelection()->absolutePosition(KoFlake::TopRight) - koSelection()->absolutePosition(KoFlake::TopLeft); } else { QPointF handlePosition = koSelection()->absolutePosition(KoFlake::TopLeft); handlePosition += 0.5 * (koSelection()->absolutePosition(KoFlake::TopRight) - handlePosition); direction = handlePosition - selectionCenter; } break; case KoFlake::TopRightHandle: direction = (QVector2D(koSelection()->absolutePosition(KoFlake::TopRight) - koSelection()->absolutePosition(KoFlake::TopLeft)).normalized() + QVector2D(koSelection()->absolutePosition(KoFlake::TopRight) - koSelection()->absolutePosition(KoFlake::BottomRight)).normalized()).toPointF(); break; case KoFlake::RightMiddleHandle: if (useEdgeRotation) { direction = koSelection()->absolutePosition(KoFlake::BottomRight) - koSelection()->absolutePosition(KoFlake::TopRight); } else { QPointF handlePosition = koSelection()->absolutePosition(KoFlake::TopRight); handlePosition += 0.5 * (koSelection()->absolutePosition(KoFlake::BottomRight) - handlePosition); direction = handlePosition - selectionCenter; } break; case KoFlake::BottomRightHandle: direction = (QVector2D(koSelection()->absolutePosition(KoFlake::BottomRight) - koSelection()->absolutePosition(KoFlake::BottomLeft)).normalized() + QVector2D(koSelection()->absolutePosition(KoFlake::BottomRight) - koSelection()->absolutePosition(KoFlake::TopRight)).normalized()).toPointF(); break; case KoFlake::BottomMiddleHandle: if (useEdgeRotation) { direction = koSelection()->absolutePosition(KoFlake::BottomLeft) - koSelection()->absolutePosition(KoFlake::BottomRight); } else { QPointF handlePosition = koSelection()->absolutePosition(KoFlake::BottomLeft); handlePosition += 0.5 * (koSelection()->absolutePosition(KoFlake::BottomRight) - handlePosition); direction = handlePosition - selectionCenter; } break; case KoFlake::BottomLeftHandle: direction = koSelection()->absolutePosition(KoFlake::BottomLeft) - selectionCenter; direction = (QVector2D(koSelection()->absolutePosition(KoFlake::BottomLeft) - koSelection()->absolutePosition(KoFlake::BottomRight)).normalized() + QVector2D(koSelection()->absolutePosition(KoFlake::BottomLeft) - koSelection()->absolutePosition(KoFlake::TopLeft)).normalized()).toPointF(); break; case KoFlake::LeftMiddleHandle: if (useEdgeRotation) { direction = koSelection()->absolutePosition(KoFlake::TopLeft) - koSelection()->absolutePosition(KoFlake::BottomLeft); } else { QPointF handlePosition = koSelection()->absolutePosition(KoFlake::TopLeft); handlePosition += 0.5 * (koSelection()->absolutePosition(KoFlake::BottomLeft) - handlePosition); direction = handlePosition - selectionCenter; } break; case KoFlake::TopLeftHandle: direction = koSelection()->absolutePosition(KoFlake::TopLeft) - selectionCenter; direction = (QVector2D(koSelection()->absolutePosition(KoFlake::TopLeft) - koSelection()->absolutePosition(KoFlake::TopRight)).normalized() + QVector2D(koSelection()->absolutePosition(KoFlake::TopLeft) - koSelection()->absolutePosition(KoFlake::BottomLeft)).normalized()).toPointF(); break; case KoFlake::NoHandle: return 0.0; break; } qreal rotation = atan2(direction.y(), direction.x()) * 180.0 / M_PI; switch (handle) { case KoFlake::TopMiddleHandle: if (useEdgeRotation) { rotation -= 0.0; } else { rotation -= 270.0; } break; case KoFlake::TopRightHandle: rotation -= 315.0; break; case KoFlake::RightMiddleHandle: if (useEdgeRotation) { rotation -= 90.0; } else { rotation -= 0.0; } break; case KoFlake::BottomRightHandle: rotation -= 45.0; break; case KoFlake::BottomMiddleHandle: if (useEdgeRotation) { rotation -= 180.0; } else { rotation -= 90.0; } break; case KoFlake::BottomLeftHandle: rotation -= 135.0; break; case KoFlake::LeftMiddleHandle: if (useEdgeRotation) { rotation -= 270.0; } else { rotation -= 180.0; } break; case KoFlake::TopLeftHandle: rotation -= 225.0; break; default: ; } if (rotation < 0.0) { rotation += 360.0; } return rotation; } void DefaultTool::updateCursor() { if (tryUseCustomCursor()) return; QCursor cursor = Qt::ArrowCursor; QString statusText; KoSelection *selection = koSelection(); if (selection && selection->count() > 0) { // has a selection bool editable = !selection->selectedEditableShapes().isEmpty(); if (!m_mouseWasInsideHandles) { m_angle = rotationOfHandle(m_lastHandle, true); int rotOctant = 8 + int(8.5 + m_angle / 45); bool rotateHandle = false; bool shearHandle = false; switch (m_lastHandle) { case KoFlake::TopMiddleHandle: cursor = m_shearCursors[(0 + rotOctant) % 8]; shearHandle = true; break; case KoFlake::TopRightHandle: cursor = m_rotateCursors[(1 + rotOctant) % 8]; rotateHandle = true; break; case KoFlake::RightMiddleHandle: cursor = m_shearCursors[(2 + rotOctant) % 8]; shearHandle = true; break; case KoFlake::BottomRightHandle: cursor = m_rotateCursors[(3 + rotOctant) % 8]; rotateHandle = true; break; case KoFlake::BottomMiddleHandle: cursor = m_shearCursors[(4 + rotOctant) % 8]; shearHandle = true; break; case KoFlake::BottomLeftHandle: cursor = m_rotateCursors[(5 + rotOctant) % 8]; rotateHandle = true; break; case KoFlake::LeftMiddleHandle: cursor = m_shearCursors[(6 + rotOctant) % 8]; shearHandle = true; break; case KoFlake::TopLeftHandle: cursor = m_rotateCursors[(7 + rotOctant) % 8]; rotateHandle = true; break; case KoFlake::NoHandle: cursor = Qt::ArrowCursor; break; } if (rotateHandle) { statusText = i18n("Left click rotates around center, right click around highlighted position."); } if (shearHandle) { statusText = i18n("Click and drag to shear selection."); } } else { statusText = i18n("Click and drag to resize selection."); m_angle = rotationOfHandle(m_lastHandle, false); int rotOctant = 8 + int(8.5 + m_angle / 45); bool cornerHandle = false; switch (m_lastHandle) { case KoFlake::TopMiddleHandle: cursor = m_sizeCursors[(0 + rotOctant) % 8]; break; case KoFlake::TopRightHandle: cursor = m_sizeCursors[(1 + rotOctant) % 8]; cornerHandle = true; break; case KoFlake::RightMiddleHandle: cursor = m_sizeCursors[(2 + rotOctant) % 8]; break; case KoFlake::BottomRightHandle: cursor = m_sizeCursors[(3 + rotOctant) % 8]; cornerHandle = true; break; case KoFlake::BottomMiddleHandle: cursor = m_sizeCursors[(4 + rotOctant) % 8]; break; case KoFlake::BottomLeftHandle: cursor = m_sizeCursors[(5 + rotOctant) % 8]; cornerHandle = true; break; case KoFlake::LeftMiddleHandle: cursor = m_sizeCursors[(6 + rotOctant) % 8]; break; case KoFlake::TopLeftHandle: cursor = m_sizeCursors[(7 + rotOctant) % 8]; cornerHandle = true; break; case KoFlake::NoHandle: cursor = Qt::SizeAllCursor; statusText = i18n("Click and drag to move selection."); break; } if (cornerHandle) { statusText = i18n("Click and drag to resize selection. Middle click to set highlighted position."); } } if (!editable) { cursor = Qt::ArrowCursor; } } else { // there used to be guides... :'''( } useCursor(cursor); if (currentStrategy() == 0) { emit statusTextChanged(statusText); } } void DefaultTool::paint(QPainter &painter, const KoViewConverter &converter) { KoSelection *selection = koSelection(); if (selection) { this->m_decorator = new SelectionDecorator(canvas()->resourceManager()); { /** * Selection masks don't render the outline of the shapes, so we should * do that explicitly when rendering them via selection */ KisCanvas2 *kisCanvas = static_cast(canvas()); KisNodeSP node = kisCanvas->viewManager()->nodeManager()->activeNode(); const bool isSelectionMask = node && node->inherits("KisSelectionMask"); m_decorator->setForceShapeOutlines(isSelectionMask); } m_decorator->setSelection(selection); m_decorator->setHandleRadius(handleRadius()); m_decorator->setShowFillGradientHandles(hasInteractioFactory(EditFillGradientFactoryId)); m_decorator->setShowStrokeFillGradientHandles(hasInteractioFactory(EditStrokeGradientFactoryId)); m_decorator->paint(painter, converter); } KoInteractionTool::paint(painter, converter); painter.save(); KoShape::applyConversion(painter, converter); canvas()->snapGuide()->paint(painter, converter); painter.restore(); } bool DefaultTool::isValidForCurrentLayer() const { // if the currently active node has a shape manager, then it is // probably our client :) KisCanvas2 *kisCanvas = static_cast(canvas()); return bool(kisCanvas->localShapeManager()); } KoShapeManager *DefaultTool::shapeManager() const { return canvas()->shapeManager(); } void DefaultTool::mousePressEvent(KoPointerEvent *event) { // this tool only works on a vector layer right now, so give a warning if another layer type is trying to use it if (!isValidForCurrentLayer()) { KisCanvas2 *kiscanvas = static_cast(canvas()); kiscanvas->viewManager()->showFloatingMessage( i18n("This tool only works on vector layers. You probably want the move tool."), QIcon(), 2000, KisFloatingMessage::Medium, Qt::AlignCenter); return; } KoInteractionTool::mousePressEvent(event); updateCursor(); } void DefaultTool::mouseMoveEvent(KoPointerEvent *event) { KoInteractionTool::mouseMoveEvent(event); if (currentStrategy() == 0 && koSelection() && koSelection()->count() > 0) { QRectF bound = handlesSize(); if (bound.contains(event->point)) { bool inside; KoFlake::SelectionHandle newDirection = handleAt(event->point, &inside); if (inside != m_mouseWasInsideHandles || m_lastHandle != newDirection) { m_lastHandle = newDirection; m_mouseWasInsideHandles = inside; //repaintDecorations(); } } else { /*if (m_lastHandle != KoFlake::NoHandle) repaintDecorations(); */ m_lastHandle = KoFlake::NoHandle; m_mouseWasInsideHandles = false; // there used to be guides... :'''( } } else { // there used to be guides... :'''( } updateCursor(); } QRectF DefaultTool::handlesSize() { KoSelection *selection = koSelection(); if (!selection || !selection->count()) return QRectF(); recalcSelectionBox(selection); QRectF bound = m_selectionOutline.boundingRect(); // expansion Border if (!canvas() || !canvas()->viewConverter()) { return bound; } QPointF border = canvas()->viewConverter()->viewToDocument(QPointF(HANDLE_DISTANCE, HANDLE_DISTANCE)); bound.adjust(-border.x(), -border.y(), border.x(), border.y()); return bound; } void DefaultTool::mouseReleaseEvent(KoPointerEvent *event) { KoInteractionTool::mouseReleaseEvent(event); updateCursor(); // This makes sure the decorations that are shown are refreshed. especally the "T" icon canvas()->updateCanvas(QRectF(0,0,canvas()->canvasWidget()->width(), canvas()->canvasWidget()->height())); } void DefaultTool::mouseDoubleClickEvent(KoPointerEvent *event) { KoSelection *selection = koSelection(); KoShape *shape = shapeManager()->shapeAt(event->point, KoFlake::ShapeOnTop); if (shape && selection && !selection->isSelected(shape)) { if (!(event->modifiers() & Qt::ShiftModifier)) { selection->deselectAll(); } selection->select(shape); } explicitUserStrokeEndRequest(); } bool DefaultTool::moveSelection(int direction, Qt::KeyboardModifiers modifiers) { bool result = false; qreal x = 0.0, y = 0.0; if (direction == Qt::Key_Left) { x = -5; } else if (direction == Qt::Key_Right) { x = 5; } else if (direction == Qt::Key_Up) { y = -5; } else if (direction == Qt::Key_Down) { y = 5; } if (x != 0.0 || y != 0.0) { // actually move if ((modifiers & Qt::ShiftModifier) != 0) { x *= 10; y *= 10; } else if ((modifiers & Qt::AltModifier) != 0) { // more precise x /= 5; y /= 5; } QList shapes = koSelection()->selectedEditableShapes(); if (!shapes.isEmpty()) { canvas()->addCommand(new KoShapeMoveCommand(shapes, QPointF(x, y))); result = true; } } return result; } void DefaultTool::keyPressEvent(QKeyEvent *event) { KoInteractionTool::keyPressEvent(event); if (currentStrategy() == 0) { switch (event->key()) { case Qt::Key_Left: case Qt::Key_Right: case Qt::Key_Up: case Qt::Key_Down: if (moveSelection(event->key(), event->modifiers())) { event->accept(); } break; case Qt::Key_1: case Qt::Key_2: case Qt::Key_3: case Qt::Key_4: case Qt::Key_5: canvas()->resourceManager()->setResource(HotPosition, event->key() - Qt::Key_1); event->accept(); break; default: return; } } } void DefaultTool::repaintDecorations() { if (koSelection() && koSelection()->count() > 0) { canvas()->updateCanvas(handlesSize()); } } void DefaultTool::copy() const { // all the selected shapes, not only editable! QList shapes = koSelection()->selectedShapes(); if (!shapes.isEmpty()) { KoDrag drag; drag.setSvg(shapes); drag.addToClipboard(); } } void DefaultTool::deleteSelection() { QList shapes; foreach (KoShape *s, koSelection()->selectedShapes()) { if (s->isGeometryProtected()) { continue; } shapes << s; } if (!shapes.empty()) { canvas()->addCommand(canvas()->shapeController()->removeShapes(shapes)); } } bool DefaultTool::paste() { // we no longer have to do anything as tool Proxy will do it for us return false; } KoSelection *DefaultTool::koSelection() const { Q_ASSERT(canvas()); Q_ASSERT(canvas()->selectedShapesProxy()); return canvas()->selectedShapesProxy()->selection(); } KoFlake::SelectionHandle DefaultTool::handleAt(const QPointF &point, bool *innerHandleMeaning) { // check for handles in this order; meaning that when handles overlap the one on top is chosen static const KoFlake::SelectionHandle handleOrder[] = { KoFlake::BottomRightHandle, KoFlake::TopLeftHandle, KoFlake::BottomLeftHandle, KoFlake::TopRightHandle, KoFlake::BottomMiddleHandle, KoFlake::RightMiddleHandle, KoFlake::LeftMiddleHandle, KoFlake::TopMiddleHandle, KoFlake::NoHandle }; const KoViewConverter *converter = canvas()->viewConverter(); KoSelection *selection = koSelection(); if (!selection || !selection->count() || !converter) { return KoFlake::NoHandle; } recalcSelectionBox(selection); if (innerHandleMeaning) { QPainterPath path; path.addPolygon(m_selectionOutline); *innerHandleMeaning = path.contains(point) || path.intersects(handlePaintRect(point)); } const QPointF viewPoint = converter->documentToView(point); for (int i = 0; i < KoFlake::NoHandle; ++i) { KoFlake::SelectionHandle handle = handleOrder[i]; const QPointF handlePoint = converter->documentToView(m_selectionBox[handle]); const qreal distanceSq = kisSquareDistance(viewPoint, handlePoint); // if just inside the outline if (distanceSq < HANDLE_DISTANCE_SQ) { if (innerHandleMeaning) { if (distanceSq < INNER_HANDLE_DISTANCE_SQ) { *innerHandleMeaning = true; } } return handle; } } return KoFlake::NoHandle; } void DefaultTool::recalcSelectionBox(KoSelection *selection) { KIS_ASSERT_RECOVER_RETURN(selection->count()); QTransform matrix = selection->absoluteTransformation(0); m_selectionOutline = matrix.map(QPolygonF(selection->outlineRect())); m_angle = 0.0; QPolygonF outline = m_selectionOutline; //shorter name in the following :) m_selectionBox[KoFlake::TopMiddleHandle] = (outline.value(0) + outline.value(1)) / 2; m_selectionBox[KoFlake::TopRightHandle] = outline.value(1); m_selectionBox[KoFlake::RightMiddleHandle] = (outline.value(1) + outline.value(2)) / 2; m_selectionBox[KoFlake::BottomRightHandle] = outline.value(2); m_selectionBox[KoFlake::BottomMiddleHandle] = (outline.value(2) + outline.value(3)) / 2; m_selectionBox[KoFlake::BottomLeftHandle] = outline.value(3); m_selectionBox[KoFlake::LeftMiddleHandle] = (outline.value(3) + outline.value(0)) / 2; m_selectionBox[KoFlake::TopLeftHandle] = outline.value(0); if (selection->count() == 1) { #if 0 // TODO detect mirroring KoShape *s = koSelection()->firstSelectedShape(); if (s->scaleX() < 0) { // vertically mirrored: swap left / right std::swap(m_selectionBox[KoFlake::TopLeftHandle], m_selectionBox[KoFlake::TopRightHandle]); std::swap(m_selectionBox[KoFlake::LeftMiddleHandle], m_selectionBox[KoFlake::RightMiddleHandle]); std::swap(m_selectionBox[KoFlake::BottomLeftHandle], m_selectionBox[KoFlake::BottomRightHandle]); } if (s->scaleY() < 0) { // vertically mirrored: swap top / bottom std::swap(m_selectionBox[KoFlake::TopLeftHandle], m_selectionBox[KoFlake::BottomLeftHandle]); std::swap(m_selectionBox[KoFlake::TopMiddleHandle], m_selectionBox[KoFlake::BottomMiddleHandle]); std::swap(m_selectionBox[KoFlake::TopRightHandle], m_selectionBox[KoFlake::BottomRightHandle]); } #endif } } void DefaultTool::activate(ToolActivation activation, const QSet &shapes) { KoToolBase::activate(activation, shapes); QAction *actionBringToFront = action("object_order_front"); connect(actionBringToFront, SIGNAL(triggered()), this, SLOT(selectionBringToFront()), Qt::UniqueConnection); QAction *actionRaise = action("object_order_raise"); connect(actionRaise, SIGNAL(triggered()), this, SLOT(selectionMoveUp()), Qt::UniqueConnection); QAction *actionLower = action("object_order_lower"); connect(actionLower, SIGNAL(triggered()), this, SLOT(selectionMoveDown())); QAction *actionSendToBack = action("object_order_back"); connect(actionSendToBack, SIGNAL(triggered()), this, SLOT(selectionSendToBack()), Qt::UniqueConnection); QAction *actionGroupBottom = action("object_group"); connect(actionGroupBottom, SIGNAL(triggered()), this, SLOT(selectionGroup()), Qt::UniqueConnection); QAction *actionUngroupBottom = action("object_ungroup"); connect(actionUngroupBottom, SIGNAL(triggered()), this, SLOT(selectionUngroup()), Qt::UniqueConnection); QAction *actionSplit = action("object_split"); connect(actionSplit, SIGNAL(triggered()), this, SLOT(selectionSplitShapes()), Qt::UniqueConnection); connect(m_alignSignalsMapper, SIGNAL(mapped(int)), SLOT(selectionAlign(int))); connect(m_distributeSignalsMapper, SIGNAL(mapped(int)), SLOT(selectionDistribute(int))); connect(m_transformSignalsMapper, SIGNAL(mapped(int)), SLOT(selectionTransform(int))); connect(m_booleanSignalsMapper, SIGNAL(mapped(int)), SLOT(selectionBooleanOp(int))); m_mouseWasInsideHandles = false; m_lastHandle = KoFlake::NoHandle; useCursor(Qt::ArrowCursor); repaintDecorations(); updateActions(); if (m_tabbedOptionWidget) { m_tabbedOptionWidget->activate(); } } void DefaultTool::deactivate() { KoToolBase::deactivate(); QAction *actionBringToFront = action("object_order_front"); disconnect(actionBringToFront, 0, this, 0); QAction *actionRaise = action("object_order_raise"); disconnect(actionRaise, 0, this, 0); QAction *actionLower = action("object_order_lower"); disconnect(actionLower, 0, this, 0); QAction *actionSendToBack = action("object_order_back"); disconnect(actionSendToBack, 0, this, 0); QAction *actionGroupBottom = action("object_group"); disconnect(actionGroupBottom, 0, this, 0); QAction *actionUngroupBottom = action("object_ungroup"); disconnect(actionUngroupBottom, 0, this, 0); QAction *actionSplit = action("object_split"); disconnect(actionSplit, 0, this, 0); disconnect(m_alignSignalsMapper, 0, this, 0); disconnect(m_distributeSignalsMapper, 0, this, 0); disconnect(m_transformSignalsMapper, 0, this, 0); disconnect(m_booleanSignalsMapper, 0, this, 0); if (m_tabbedOptionWidget) { m_tabbedOptionWidget->deactivate(); } } void DefaultTool::selectionGroup() { KoSelection *selection = koSelection(); if (!selection) return; QList selectedShapes = selection->selectedEditableShapes(); std::sort(selectedShapes.begin(), selectedShapes.end(), KoShape::compareShapeZIndex); if (selectedShapes.isEmpty()) return; const int groupZIndex = selectedShapes.last()->zIndex(); KoShapeGroup *group = new KoShapeGroup(); group->setZIndex(groupZIndex); // TODO what if only one shape is left? KUndo2Command *cmd = new KUndo2Command(kundo2_i18n("Group shapes")); new KoKeepShapesSelectedCommand(selectedShapes, {}, canvas()->selectedShapesProxy(), false, cmd); canvas()->shapeController()->addShapeDirect(group, 0, cmd); new KoShapeGroupCommand(group, selectedShapes, true, cmd); new KoKeepShapesSelectedCommand({}, {group}, canvas()->selectedShapesProxy(), true, cmd); canvas()->addCommand(cmd); // update selection so we can ungroup immediately again selection->deselectAll(); selection->select(group); } void DefaultTool::selectionUngroup() { KoSelection *selection = koSelection(); if (!selection) return; QList selectedShapes = selection->selectedEditableShapes(); std::sort(selectedShapes.begin(), selectedShapes.end(), KoShape::compareShapeZIndex); KUndo2Command *cmd = 0; QList newShapes; // add a ungroup command for each found shape container to the macro command Q_FOREACH (KoShape *shape, selectedShapes) { KoShapeGroup *group = dynamic_cast(shape); if (group) { if (!cmd) { cmd = new KUndo2Command(kundo2_i18n("Ungroup shapes")); new KoKeepShapesSelectedCommand(selectedShapes, {}, canvas()->selectedShapesProxy(), false, cmd); } newShapes << group->shapes(); new KoShapeUngroupCommand(group, group->shapes(), group->parent() ? QList() : shapeManager()->topLevelShapes(), cmd); canvas()->shapeController()->removeShape(group, cmd); } } if (cmd) { new KoKeepShapesSelectedCommand({}, newShapes, canvas()->selectedShapesProxy(), true, cmd); canvas()->addCommand(cmd); } } void DefaultTool::selectionTransform(int transformAction) { KoSelection *selection = koSelection(); if (!selection) return; QList editableShapes = selection->selectedEditableShapes(); if (editableShapes.isEmpty()) { return; } QTransform applyTransform; bool shouldReset = false; KUndo2MagicString actionName = kundo2_noi18n("BUG: No transform action"); switch (TransformActionType(transformAction)) { case TransformRotate90CW: applyTransform.rotate(90.0); actionName = kundo2_i18n("Rotate Object 90° CW"); break; case TransformRotate90CCW: applyTransform.rotate(-90.0); actionName = kundo2_i18n("Rotate Object 90° CCW"); break; case TransformRotate180: applyTransform.rotate(180.0); actionName = kundo2_i18n("Rotate Object 180°"); break; case TransformMirrorX: applyTransform.scale(-1.0, 1.0); actionName = kundo2_i18n("Mirror Object Horizontally"); break; case TransformMirrorY: applyTransform.scale(1.0, -1.0); actionName = kundo2_i18n("Mirror Object Vertically"); break; case TransformReset: shouldReset = true; actionName = kundo2_i18n("Reset Object Transformations"); break; } if (!shouldReset && applyTransform.isIdentity()) return; QList oldTransforms; QList newTransforms; const QRectF outlineRect = KoShape::absoluteOutlineRect(editableShapes); const QPointF centerPoint = outlineRect.center(); const QTransform centerTrans = QTransform::fromTranslate(centerPoint.x(), centerPoint.y()); const QTransform centerTransInv = QTransform::fromTranslate(-centerPoint.x(), -centerPoint.y()); // we also add selection to the list of transformed shapes, so that its outline is updated correctly QList transformedShapes = editableShapes; transformedShapes << selection; Q_FOREACH (KoShape *shape, transformedShapes) { oldTransforms.append(shape->transformation()); QTransform t; if (!shouldReset) { const QTransform world = shape->absoluteTransformation(0); t = world * centerTransInv * applyTransform * centerTrans * world.inverted() * shape->transformation(); } else { const QPointF center = shape->outlineRect().center(); const QPointF offset = shape->transformation().map(center) - center; t = QTransform::fromTranslate(offset.x(), offset.y()); } newTransforms.append(t); } KoShapeTransformCommand *cmd = new KoShapeTransformCommand(transformedShapes, oldTransforms, newTransforms); cmd->setText(actionName); canvas()->addCommand(cmd); } void DefaultTool::selectionBooleanOp(int booleanOp) { KoSelection *selection = koSelection(); if (!selection) return; QList editableShapes = selection->selectedEditableShapes(); if (editableShapes.isEmpty()) { return; } QVector srcOutlines; QPainterPath dstOutline; KUndo2MagicString actionName = kundo2_noi18n("BUG: boolean action name"); // TODO: implement a reference shape selection dialog! const int referenceShapeIndex = 0; KoShape *referenceShape = editableShapes[referenceShapeIndex]; Q_FOREACH (KoShape *shape, editableShapes) { srcOutlines << shape->absoluteTransformation(0).map(shape->outline()); } if (booleanOp == BooleanUnion) { Q_FOREACH (const QPainterPath &path, srcOutlines) { dstOutline |= path; } actionName = kundo2_i18n("Unite Shapes"); } else if (booleanOp == BooleanIntersection) { for (int i = 0; i < srcOutlines.size(); i++) { if (i == 0) { dstOutline = srcOutlines[i]; } else { dstOutline &= srcOutlines[i]; } } // there is a bug in Qt, sometimes it leaves the resulting // outline open, so just close it explicitly. dstOutline.closeSubpath(); actionName = kundo2_i18n("Intersect Shapes"); } else if (booleanOp == BooleanSubtraction) { for (int i = 0; i < srcOutlines.size(); i++) { dstOutline = srcOutlines[referenceShapeIndex]; if (i != referenceShapeIndex) { dstOutline -= srcOutlines[i]; } } actionName = kundo2_i18n("Subtract Shapes"); } KoShape *newShape = 0; if (!dstOutline.isEmpty()) { newShape = KoPathShape::createShapeFromPainterPath(dstOutline); } KUndo2Command *cmd = new KUndo2Command(actionName); new KoKeepShapesSelectedCommand(editableShapes, {}, canvas()->selectedShapesProxy(), false, cmd); QList newSelectedShapes; if (newShape) { newShape->setBackground(referenceShape->background()); newShape->setStroke(referenceShape->stroke()); newShape->setZIndex(referenceShape->zIndex()); KoShapeContainer *parent = referenceShape->parent(); canvas()->shapeController()->addShapeDirect(newShape, parent, cmd); newSelectedShapes << newShape; } canvas()->shapeController()->removeShapes(editableShapes, cmd); new KoKeepShapesSelectedCommand({}, newSelectedShapes, canvas()->selectedShapesProxy(), true, cmd); canvas()->addCommand(cmd); } void DefaultTool::selectionSplitShapes() { KoSelection *selection = koSelection(); if (!selection) return; QList editableShapes = selection->selectedEditableShapes(); if (editableShapes.isEmpty()) { return; } KUndo2Command *cmd = new KUndo2Command(kundo2_i18n("Split Shapes")); new KoKeepShapesSelectedCommand(editableShapes, {}, canvas()->selectedShapesProxy(), false, cmd); QList newShapes; Q_FOREACH (KoShape *shape, editableShapes) { KoPathShape *pathShape = dynamic_cast(shape); if (!pathShape) return; QList splitShapes; if (pathShape->separate(splitShapes)) { QList normalShapes = implicitCastList(splitShapes); KoShapeContainer *parent = shape->parent(); canvas()->shapeController()->addShapesDirect(normalShapes, parent, cmd); canvas()->shapeController()->removeShape(shape, cmd); newShapes << normalShapes; } } new KoKeepShapesSelectedCommand({}, newShapes, canvas()->selectedShapesProxy(), true, cmd); canvas()->addCommand(cmd); } void DefaultTool::selectionAlign(int _align) { KoShapeAlignCommand::Align align = static_cast(_align); KoSelection *selection = koSelection(); if (!selection) return; QList editableShapes = selection->selectedEditableShapes(); if (editableShapes.isEmpty()) { return; } // TODO add an option to the widget so that one can align to the page // with multiple selected shapes too QRectF bb; // single selected shape is automatically aligned to document rect if (editableShapes.count() == 1) { if (!canvas()->resourceManager()->hasResource(KoCanvasResourceProvider::PageSize)) { return; } bb = QRectF(QPointF(0, 0), canvas()->resourceManager()->sizeResource(KoCanvasResourceProvider::PageSize)); } else { bb = KoShape::absoluteOutlineRect(editableShapes); } KoShapeAlignCommand *cmd = new KoShapeAlignCommand(editableShapes, align, bb); canvas()->addCommand(cmd); } void DefaultTool::selectionDistribute(int _distribute) { KoShapeDistributeCommand::Distribute distribute = static_cast(_distribute); KoSelection *selection = koSelection(); if (!selection) return; QList editableShapes = selection->selectedEditableShapes(); if (editableShapes.size() < 3) { return; } QRectF bb = KoShape::absoluteOutlineRect(editableShapes); KoShapeDistributeCommand *cmd = new KoShapeDistributeCommand(editableShapes, distribute, bb); canvas()->addCommand(cmd); } void DefaultTool::selectionBringToFront() { selectionReorder(KoShapeReorderCommand::BringToFront); } void DefaultTool::selectionMoveUp() { selectionReorder(KoShapeReorderCommand::RaiseShape); } void DefaultTool::selectionMoveDown() { selectionReorder(KoShapeReorderCommand::LowerShape); } void DefaultTool::selectionSendToBack() { selectionReorder(KoShapeReorderCommand::SendToBack); } void DefaultTool::selectionReorder(KoShapeReorderCommand::MoveShapeType order) { KoSelection *selection = koSelection(); if (!selection) { return; } QList selectedShapes = selection->selectedEditableShapes(); if (selectedShapes.isEmpty()) { return; } KUndo2Command *cmd = KoShapeReorderCommand::createCommand(selectedShapes, shapeManager(), order); if (cmd) { canvas()->addCommand(cmd); } } QList > DefaultTool::createOptionWidgets() { QList > widgets; m_tabbedOptionWidget = new DefaultToolTabbedWidget(this); if (isActivated()) { m_tabbedOptionWidget->activate(); } widgets.append(m_tabbedOptionWidget); connect(m_tabbedOptionWidget, SIGNAL(sigSwitchModeEditFillGradient(bool)), SLOT(slotActivateEditFillGradient(bool))); connect(m_tabbedOptionWidget, SIGNAL(sigSwitchModeEditStrokeGradient(bool)), SLOT(slotActivateEditStrokeGradient(bool))); return widgets; } void DefaultTool::canvasResourceChanged(int key, const QVariant &res) { if (key == HotPosition) { m_hotPosition = KoFlake::AnchorPosition(res.toInt()); repaintDecorations(); } } KoInteractionStrategy *DefaultTool::createStrategy(KoPointerEvent *event) { KoSelection *selection = koSelection(); if (!selection) return nullptr; bool insideSelection = false; KoFlake::SelectionHandle handle = handleAt(event->point, &insideSelection); bool editableShape = !selection->selectedEditableShapes().isEmpty(); const bool selectMultiple = event->modifiers() & Qt::ShiftModifier; const bool selectNextInStack = event->modifiers() & Qt::ControlModifier; const bool avoidSelection = event->modifiers() & Qt::AltModifier; if (selectNextInStack) { // change the hot selection position when middle clicking on a handle KoFlake::AnchorPosition newHotPosition = m_hotPosition; switch (handle) { case KoFlake::TopMiddleHandle: newHotPosition = KoFlake::Top; break; case KoFlake::TopRightHandle: newHotPosition = KoFlake::TopRight; break; case KoFlake::RightMiddleHandle: newHotPosition = KoFlake::Right; break; case KoFlake::BottomRightHandle: newHotPosition = KoFlake::BottomRight; break; case KoFlake::BottomMiddleHandle: newHotPosition = KoFlake::Bottom; break; case KoFlake::BottomLeftHandle: newHotPosition = KoFlake::BottomLeft; break; case KoFlake::LeftMiddleHandle: newHotPosition = KoFlake::Left; break; case KoFlake::TopLeftHandle: newHotPosition = KoFlake::TopLeft; break; case KoFlake::NoHandle: default: // check if we had hit the center point const KoViewConverter *converter = canvas()->viewConverter(); QPointF pt = converter->documentToView(event->point); // TODO: use calculated values instead! QPointF centerPt = converter->documentToView(selection->absolutePosition()); if (kisSquareDistance(pt, centerPt) < HANDLE_DISTANCE_SQ) { newHotPosition = KoFlake::Center; } break; } if (m_hotPosition != newHotPosition) { canvas()->resourceManager()->setResource(HotPosition, newHotPosition); return new NopInteractionStrategy(this); } } if (!avoidSelection && editableShape) { // manipulation of selected shapes goes first if (handle != KoFlake::NoHandle) { // resizing or shearing only with left mouse button if (insideSelection) { bool forceUniformScaling = m_tabbedOptionWidget && m_tabbedOptionWidget->useUniformScaling(); return new ShapeResizeStrategy(this, selection, event->point, handle, forceUniformScaling); } if (handle == KoFlake::TopMiddleHandle || handle == KoFlake::RightMiddleHandle || handle == KoFlake::BottomMiddleHandle || handle == KoFlake::LeftMiddleHandle) { return new ShapeShearStrategy(this, selection, event->point, handle); } // rotating is allowed for right mouse button too if (handle == KoFlake::TopLeftHandle || handle == KoFlake::TopRightHandle || handle == KoFlake::BottomLeftHandle || handle == KoFlake::BottomRightHandle) { return new ShapeRotateStrategy(this, selection, event->point, event->buttons()); } } if (!selectMultiple && !selectNextInStack) { if (insideSelection) { return new ShapeMoveStrategy(this, selection, event->point); } } } KoShape *shape = shapeManager()->shapeAt(event->point, selectNextInStack ? KoFlake::NextUnselected : KoFlake::ShapeOnTop); if (avoidSelection || (!shape && handle == KoFlake::NoHandle)) { if (!selectMultiple) { repaintDecorations(); selection->deselectAll(); } return new SelectionInteractionStrategy(this, event->point, false); } if (selection->isSelected(shape)) { if (selectMultiple) { repaintDecorations(); selection->deselect(shape); } } else if (handle == KoFlake::NoHandle) { // clicked on shape which is not selected repaintDecorations(); if (!selectMultiple) { selection->deselectAll(); } selection->select(shape); repaintDecorations(); // tablet selection isn't precise and may lead to a move, preventing that if (event->isTabletEvent()) { return new NopInteractionStrategy(this); } return new ShapeMoveStrategy(this, selection, event->point); } return 0; } void DefaultTool::updateActions() { QList editableShapes; if (koSelection()) { editableShapes = koSelection()->selectedEditableShapes(); } const bool hasEditableShapes = !editableShapes.isEmpty(); action("object_order_front")->setEnabled(hasEditableShapes); action("object_order_raise")->setEnabled(hasEditableShapes); action("object_order_lower")->setEnabled(hasEditableShapes); action("object_order_back")->setEnabled(hasEditableShapes); action("object_transform_rotate_90_cw")->setEnabled(hasEditableShapes); action("object_transform_rotate_90_ccw")->setEnabled(hasEditableShapes); action("object_transform_rotate_180")->setEnabled(hasEditableShapes); action("object_transform_mirror_horizontally")->setEnabled(hasEditableShapes); action("object_transform_mirror_vertically")->setEnabled(hasEditableShapes); action("object_transform_reset")->setEnabled(hasEditableShapes); const bool multipleSelected = editableShapes.size() > 1; const bool alignmentEnabled = multipleSelected || (!editableShapes.isEmpty() && canvas()->resourceManager()->hasResource(KoCanvasResourceProvider::PageSize)); action("object_align_horizontal_left")->setEnabled(alignmentEnabled); action("object_align_horizontal_center")->setEnabled(alignmentEnabled); action("object_align_horizontal_right")->setEnabled(alignmentEnabled); action("object_align_vertical_top")->setEnabled(alignmentEnabled); action("object_align_vertical_center")->setEnabled(alignmentEnabled); action("object_align_vertical_bottom")->setEnabled(alignmentEnabled); const bool distributionEnabled = editableShapes.size() > 2; action("object_distribute_horizontal_left")->setEnabled(distributionEnabled); action("object_distribute_horizontal_center")->setEnabled(distributionEnabled); action("object_distribute_horizontal_right")->setEnabled(distributionEnabled); action("object_distribute_horizontal_gaps")->setEnabled(distributionEnabled); action("object_distribute_vertical_top")->setEnabled(distributionEnabled); action("object_distribute_vertical_center")->setEnabled(distributionEnabled); action("object_distribute_vertical_bottom")->setEnabled(distributionEnabled); action("object_distribute_vertical_gaps")->setEnabled(distributionEnabled); updateDistinctiveActions(editableShapes); emit selectionChanged(editableShapes.size()); } void DefaultTool::updateDistinctiveActions(const QList &editableShapes) { const bool multipleSelected = editableShapes.size() > 1; action("object_group")->setEnabled(multipleSelected); action("object_unite")->setEnabled(multipleSelected); action("object_intersect")->setEnabled(multipleSelected); action("object_subtract")->setEnabled(multipleSelected); bool hasShapesWithMultipleSegments = false; Q_FOREACH (KoShape *shape, editableShapes) { KoPathShape *pathShape = dynamic_cast(shape); if (pathShape && pathShape->subpathCount() > 1) { hasShapesWithMultipleSegments = true; break; } } action("object_split")->setEnabled(hasShapesWithMultipleSegments); bool hasGroupShape = false; foreach (KoShape *shape, editableShapes) { if (dynamic_cast(shape)) { hasGroupShape = true; break; } } action("object_ungroup")->setEnabled(hasGroupShape); } KoToolSelection *DefaultTool::selection() { return m_selectionHandler; } QMenu* DefaultTool::popupActionsMenu() { if (m_contextMenu) { m_contextMenu->clear(); m_contextMenu->addSection(i18n("Vector Shape Actions")); m_contextMenu->addSeparator(); QMenu *transform = m_contextMenu->addMenu(i18n("Transform")); transform->addAction(action("object_transform_rotate_90_cw")); transform->addAction(action("object_transform_rotate_90_ccw")); transform->addAction(action("object_transform_rotate_180")); transform->addSeparator(); transform->addAction(action("object_transform_mirror_horizontally")); transform->addAction(action("object_transform_mirror_vertically")); transform->addSeparator(); transform->addAction(action("object_transform_reset")); if (action("object_unite")->isEnabled() || action("object_intersect")->isEnabled() || action("object_subtract")->isEnabled() || action("object_split")->isEnabled()) { QMenu *transform = m_contextMenu->addMenu(i18n("Logical Operations")); transform->addAction(action("object_unite")); transform->addAction(action("object_intersect")); transform->addAction(action("object_subtract")); transform->addAction(action("object_split")); } m_contextMenu->addSeparator(); m_contextMenu->addAction(action("edit_cut")); m_contextMenu->addAction(action("edit_copy")); m_contextMenu->addAction(action("edit_paste")); m_contextMenu->addSeparator(); m_contextMenu->addAction(action("object_order_front")); m_contextMenu->addAction(action("object_order_raise")); m_contextMenu->addAction(action("object_order_lower")); m_contextMenu->addAction(action("object_order_back")); if (action("object_group")->isEnabled() || action("object_ungroup")->isEnabled()) { m_contextMenu->addSeparator(); m_contextMenu->addAction(action("object_group")); m_contextMenu->addAction(action("object_ungroup")); } } return m_contextMenu.data(); } void DefaultTool::addTransformActions(QMenu *menu) const { menu->addAction(action("object_transform_rotate_90_cw")); menu->addAction(action("object_transform_rotate_90_ccw")); menu->addAction(action("object_transform_rotate_180")); menu->addSeparator(); menu->addAction(action("object_transform_mirror_horizontally")); menu->addAction(action("object_transform_mirror_vertically")); menu->addSeparator(); menu->addAction(action("object_transform_reset")); } void DefaultTool::explicitUserStrokeEndRequest() { QList shapes = koSelection()->selectedEditableShapesAndDelegates(); emit activateTemporary(KoToolManager::instance()->preferredToolForSelection(shapes)); } diff --git a/plugins/tools/defaulttool/defaulttool/SelectionDecorator.cpp b/plugins/tools/defaulttool/defaulttool/SelectionDecorator.cpp index 2c8c7dbe0d..ab560d6f44 100644 --- a/plugins/tools/defaulttool/defaulttool/SelectionDecorator.cpp +++ b/plugins/tools/defaulttool/defaulttool/SelectionDecorator.cpp @@ -1,202 +1,204 @@ /* This file is part of the KDE project Copyright (C) 2006 Thorsten Zachmann Copyright (C) 2006-2007 Thomas Zander This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "SelectionDecorator.h" +#include + #include #include #include #include "kis_algebra_2d.h" #include "kis_debug.h" #include #include #include #include "KoShapeGradientHandles.h" #include #include #include "kis_painting_tweaks.h" #include "kis_coordinates_converter.h" #include "kis_icon_utils.h" #define HANDLE_DISTANCE 10 SelectionDecorator::SelectionDecorator(KoCanvasResourceProvider *resourceManager) : m_hotPosition(KoFlake::Center) , m_handleRadius(7) , m_lineWidth(2) , m_showFillGradientHandles(false) , m_showStrokeFillGradientHandles(false) , m_forceShapeOutlines(false) { m_hotPosition = KoFlake::AnchorPosition( resourceManager->resource(KoFlake::HotPosition).toInt()); } void SelectionDecorator::setSelection(KoSelection *selection) { m_selection = selection; } void SelectionDecorator::setHandleRadius(int radius) { m_handleRadius = radius; m_lineWidth = qMax(1, (int)(radius / 2)); } void SelectionDecorator::setShowFillGradientHandles(bool value) { m_showFillGradientHandles = value; } void SelectionDecorator::setShowStrokeFillGradientHandles(bool value) { m_showStrokeFillGradientHandles = value; } void SelectionDecorator::paint(QPainter &painter, const KoViewConverter &converter) { QList selectedShapes = m_selection->selectedVisibleShapes(); if (selectedShapes.isEmpty()) return; const bool haveOnlyOneEditableShape = m_selection->selectedEditableShapes().size() == 1 && selectedShapes.size() == 1; bool editable = false; bool forceBoundngRubberLine = false; Q_FOREACH (KoShape *shape, KoShape::linearizeSubtree(selectedShapes)) { if (!haveOnlyOneEditableShape || !m_showStrokeFillGradientHandles) { KisHandlePainterHelper helper = KoShape::createHandlePainterHelper(&painter, shape, converter, m_handleRadius); helper.setHandleStyle(KisHandleStyle::secondarySelection()); if (!m_forceShapeOutlines) { helper.drawRubberLine(shape->outlineRect()); } else { QList polys = shape->outline().toSubpathPolygons(); if (polys.size() == 1) { const QPolygonF poly1 = polys[0]; const QPolygonF poly2 = QPolygonF(polys[0].boundingRect()); const QPolygonF nonoverlap = poly2.subtracted(poly1); forceBoundngRubberLine |= !nonoverlap.isEmpty(); } Q_FOREACH (const QPolygonF &poly, polys) { helper.drawRubberLine(poly); } } } if (shape->isShapeEditable()) { editable = true; } } const QRectF handleArea = m_selection->outlineRect(); // draw extra rubber line around all the shapes if (selectedShapes.size() > 1 || forceBoundngRubberLine) { KisHandlePainterHelper helper = KoShape::createHandlePainterHelper(&painter, m_selection, converter, m_handleRadius); helper.setHandleStyle(KisHandleStyle::primarySelection()); helper.drawRubberLine(handleArea); } // if we have no editable shape selected there // is no need drawing the selection handles if (editable) { KisHandlePainterHelper helper = KoShape::createHandlePainterHelper(&painter, m_selection, converter, m_handleRadius); helper.setHandleStyle(KisHandleStyle::primarySelection()); QPolygonF outline = handleArea; { helper.drawHandleRect(outline.value(0)); helper.drawHandleRect(outline.value(1)); helper.drawHandleRect(outline.value(2)); helper.drawHandleRect(outline.value(3)); helper.drawHandleRect(0.5 * (outline.value(0) + outline.value(1))); helper.drawHandleRect(0.5 * (outline.value(1) + outline.value(2))); helper.drawHandleRect(0.5 * (outline.value(2) + outline.value(3))); helper.drawHandleRect(0.5 * (outline.value(3) + outline.value(0))); QPointF hotPos = KoFlake::anchorToPoint(m_hotPosition, handleArea); helper.setHandleStyle(KisHandleStyle::highlightedPrimaryHandles()); helper.drawHandleRect(hotPos); } } if (haveOnlyOneEditableShape) { KoShape *shape = selectedShapes.first(); if (m_showFillGradientHandles) { paintGradientHandles(shape, KoFlake::Fill, painter, converter); } else if (m_showStrokeFillGradientHandles) { paintGradientHandles(shape, KoFlake::StrokeFill, painter, converter); } } } void SelectionDecorator::paintGradientHandles(KoShape *shape, KoFlake::FillVariant fillVariant, QPainter &painter, const KoViewConverter &converter) { KoShapeGradientHandles gradientHandles(fillVariant, shape); QVector handles = gradientHandles.handles(); KisHandlePainterHelper helper = KoShape::createHandlePainterHelper(&painter, shape, converter, m_handleRadius); const QTransform t = shape->absoluteTransformation(0).inverted(); if (gradientHandles.type() == QGradient::LinearGradient) { KIS_SAFE_ASSERT_RECOVER_NOOP(handles.size() == 2); if (handles.size() == 2) { helper.setHandleStyle(KisHandleStyle::gradientArrows()); helper.drawGradientArrow(t.map(handles[0].pos), t.map(handles[1].pos), 1.5 * m_handleRadius); } } helper.setHandleStyle(KisHandleStyle::gradientHandles()); Q_FOREACH (const KoShapeGradientHandles::Handle &h, handles) { if (h.type == KoShapeGradientHandles::Handle::RadialCenter) { helper.drawGradientCrossHandle(t.map(h.pos), 1.2 * m_handleRadius); } else { helper.drawGradientHandle(t.map(h.pos), 1.2 * m_handleRadius); } } } void SelectionDecorator::setForceShapeOutlines(bool value) { m_forceShapeOutlines = value; } diff --git a/plugins/tools/karbonplugins/tools/CalligraphyTool/KarbonCalligraphicShape.cpp b/plugins/tools/karbonplugins/tools/CalligraphyTool/KarbonCalligraphicShape.cpp index 2a461cbdc2..1b9d81b733 100644 --- a/plugins/tools/karbonplugins/tools/CalligraphyTool/KarbonCalligraphicShape.cpp +++ b/plugins/tools/karbonplugins/tools/CalligraphyTool/KarbonCalligraphicShape.cpp @@ -1,433 +1,434 @@ /* This file is part of the KDE project Copyright (C) 2008 Fela Winkelmolen This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "KarbonCalligraphicShape.h" #include #include #include "KarbonSimplifyPath.h" #include #include #include #include +#include #include #include #undef M_PI const qreal M_PI = 3.1415927; KarbonCalligraphicShape::KarbonCalligraphicShape(qreal caps) : m_lastWasFlip(false) , m_caps(caps) { setShapeId(KoPathShapeId); setFillRule(Qt::WindingFill); setBackground(QSharedPointer(new KoColorBackground(QColor(Qt::black)))); setStroke(KoShapeStrokeModelSP()); } KarbonCalligraphicShape::KarbonCalligraphicShape(const KarbonCalligraphicShape &rhs) : KoParameterShape(new KoParameterShapePrivate(*rhs.d_func(), this)), m_points(rhs.m_points), m_lastWasFlip(rhs.m_lastWasFlip), m_caps(rhs.m_caps) { } KarbonCalligraphicShape::~KarbonCalligraphicShape() { } KoShape *KarbonCalligraphicShape::cloneShape() const { return new KarbonCalligraphicShape(*this); } void KarbonCalligraphicShape::appendPoint(const QPointF &point, qreal angle, qreal width) { // convert the point from canvas to shape coordinates QPointF p = point - position(); KarbonCalligraphicPoint *calligraphicPoint = new KarbonCalligraphicPoint(p, angle, width); QList handles = this->handles(); handles.append(p); setHandles(handles); m_points.append(calligraphicPoint); appendPointToPath(*calligraphicPoint); // make the angle of the first point more in line with the actual // direction if (m_points.count() == 4) { m_points[0]->setAngle(angle); m_points[1]->setAngle(angle); m_points[2]->setAngle(angle); } } void KarbonCalligraphicShape::appendPointToPath(const KarbonCalligraphicPoint &p) { qreal dx = std::cos(p.angle()) * p.width(); qreal dy = std::sin(p.angle()) * p.width(); // find the outline points QPointF p1 = p.point() - QPointF(dx / 2, dy / 2); QPointF p2 = p.point() + QPointF(dx / 2, dy / 2); if (pointCount() == 0) { moveTo(p1); lineTo(p2); normalize(); return; } // pointCount > 0 bool flip = (pointCount() >= 2) ? flipDetected(p1, p2) : false; // if there was a flip add additional points if (flip) { appendPointsToPathAux(p2, p1); if (pointCount() > 4) { smoothLastPoints(); } } appendPointsToPathAux(p1, p2); if (pointCount() > 4) { smoothLastPoints(); if (flip) { int index = pointCount() / 2; // find the last two points KoPathPoint *last1 = pointByIndex(KoPathPointIndex(0, index - 1)); KoPathPoint *last2 = pointByIndex(KoPathPointIndex(0, index)); last1->removeControlPoint1(); last1->removeControlPoint2(); last2->removeControlPoint1(); last2->removeControlPoint2(); m_lastWasFlip = true; } if (m_lastWasFlip) { int index = pointCount() / 2; // find the previous two points KoPathPoint *prev1 = pointByIndex(KoPathPointIndex(0, index - 2)); KoPathPoint *prev2 = pointByIndex(KoPathPointIndex(0, index + 1)); prev1->removeControlPoint1(); prev1->removeControlPoint2(); prev2->removeControlPoint1(); prev2->removeControlPoint2(); if (!flip) { m_lastWasFlip = false; } } } normalize(); // add initial cap if it's the fourth added point // this code is here because this function is called from different places // pointCount() == 8 may causes crashes because it doesn't take possible // flips into account if (m_points.count() >= 4 && &p == m_points[3]) { addCap(3, 0, 0, true); // duplicate the last point to make the points remain "balanced" // needed to keep all indexes code (else I would need to change // everything in the code...) KoPathPoint *last = pointByIndex(KoPathPointIndex(0, pointCount() - 1)); KoPathPoint *newPoint = new KoPathPoint(this, last->point()); insertPoint(newPoint, KoPathPointIndex(0, pointCount())); close(); } } void KarbonCalligraphicShape::appendPointsToPathAux(const QPointF &p1, const QPointF &p2) { KoPathPoint *pathPoint1 = new KoPathPoint(this, p1); KoPathPoint *pathPoint2 = new KoPathPoint(this, p2); // calculate the index of the insertion position int index = pointCount() / 2; insertPoint(pathPoint2, KoPathPointIndex(0, index)); insertPoint(pathPoint1, KoPathPointIndex(0, index)); } void KarbonCalligraphicShape::smoothLastPoints() { int index = pointCount() / 2; smoothPoint(index - 2); smoothPoint(index + 1); } void KarbonCalligraphicShape::smoothPoint(const int index) { if (pointCount() < index + 2) { return; } else if (index < 1) { return; } const KoPathPointIndex PREV(0, index - 1); const KoPathPointIndex INDEX(0, index); const KoPathPointIndex NEXT(0, index + 1); QPointF prev = pointByIndex(PREV)->point(); QPointF point = pointByIndex(INDEX)->point(); QPointF next = pointByIndex(NEXT)->point(); QPointF vector = next - prev; qreal dist = (QLineF(prev, next)).length(); // normalize the vector (make it's size equal to 1) if (!qFuzzyCompare(dist + 1, 1)) { vector /= dist; } qreal mult = 0.35; // found by trial and error, might not be perfect... // distance of the control points from the point qreal dist1 = (QLineF(point, prev)).length() * mult; qreal dist2 = (QLineF(point, next)).length() * mult; QPointF vector1 = vector * dist1; QPointF vector2 = vector * dist2; QPointF controlPoint1 = point - vector1; QPointF controlPoint2 = point + vector2; pointByIndex(INDEX)->setControlPoint1(controlPoint1); pointByIndex(INDEX)->setControlPoint2(controlPoint2); } const QRectF KarbonCalligraphicShape::lastPieceBoundingRect() { if (pointCount() < 6) { return QRectF(); } int index = pointCount() / 2; QPointF p1 = pointByIndex(KoPathPointIndex(0, index - 3))->point(); QPointF p2 = pointByIndex(KoPathPointIndex(0, index - 2))->point(); QPointF p3 = pointByIndex(KoPathPointIndex(0, index - 1))->point(); QPointF p4 = pointByIndex(KoPathPointIndex(0, index))->point(); QPointF p5 = pointByIndex(KoPathPointIndex(0, index + 1))->point(); QPointF p6 = pointByIndex(KoPathPointIndex(0, index + 2))->point(); // TODO: also take the control points into account QPainterPath p; p.moveTo(p1); p.lineTo(p2); p.lineTo(p3); p.lineTo(p4); p.lineTo(p5); p.lineTo(p6); return p.boundingRect().translated(position()); } bool KarbonCalligraphicShape::flipDetected(const QPointF &p1, const QPointF &p2) { // detect the flip caused by the angle changing 180 degrees // thus detect the boundary crossing int index = pointCount() / 2; QPointF last1 = pointByIndex(KoPathPointIndex(0, index - 1))->point(); QPointF last2 = pointByIndex(KoPathPointIndex(0, index))->point(); int sum1 = std::abs(ccw(p1, p2, last1) + ccw(p1, last2, last1)); int sum2 = std::abs(ccw(p2, p1, last2) + ccw(p2, last1, last2)); // if there was a flip return sum1 < 2 && sum2 < 2; } int KarbonCalligraphicShape::ccw(const QPointF &p1, const QPointF &p2,const QPointF &p3) { // calculate two times the area of the triangle formed by the points given qreal area2 = (p2.x() - p1.x()) * (p3.y() - p1.y()) - (p2.y() - p1.y()) * (p3.x() - p1.x()); if (area2 > 0) { return +1; // the points are given in counterclockwise order } else if (area2 < 0) { return -1; // the points are given in clockwise order } else { return 0; // the points form a degenerate triangle } } void KarbonCalligraphicShape::setSize(const QSizeF &newSize) { // QSizeF oldSize = size(); // TODO: check KoParameterShape::setSize(newSize); } QPointF KarbonCalligraphicShape::normalize() { QPointF offset(KoParameterShape::normalize()); QTransform matrix; matrix.translate(-offset.x(), -offset.y()); for (int i = 0; i < m_points.size(); ++i) { m_points[i]->setPoint(matrix.map(m_points[i]->point())); } return offset; } void KarbonCalligraphicShape::moveHandleAction(int handleId, const QPointF &point, Qt::KeyboardModifiers modifiers) { Q_UNUSED(modifiers); m_points[handleId]->setPoint(point); } void KarbonCalligraphicShape::updatePath(const QSizeF &size) { Q_UNUSED(size); QPointF pos = position(); // remove all points clear(); setPosition(QPoint(0, 0)); Q_FOREACH (KarbonCalligraphicPoint *p, m_points) { appendPointToPath(*p); } simplifyPath(); QList handles; Q_FOREACH (KarbonCalligraphicPoint *p, m_points) { handles.append(p->point()); } setHandles(handles); setPosition(pos); } void KarbonCalligraphicShape::simplifyPath() { if (m_points.count() < 2) { return; } close(); // add final cap addCap(m_points.count() - 2, m_points.count() - 1, pointCount() / 2); // TODO: the error should be proportional to the width // and it shouldn't be a magic number karbonSimplifyPath(this, 0.3); } void KarbonCalligraphicShape::addCap(int index1, int index2, int pointIndex, bool inverted) { QPointF p1 = m_points[index1]->point(); QPointF p2 = m_points[index2]->point(); // TODO: review why spikes can appear with a lower limit QPointF delta = p2 - p1; if (delta.manhattanLength() < 1.0) { return; } QPointF direction = QLineF(QPointF(0, 0), delta).unitVector().p2(); qreal width = m_points[index2]->width(); QPointF p = p2 + direction * m_caps * width; KoPathPoint *newPoint = new KoPathPoint(this, p); qreal angle = m_points[index2]->angle(); if (inverted) { angle += M_PI; } qreal dx = std::cos(angle) * width; qreal dy = std::sin(angle) * width; newPoint->setControlPoint1(QPointF(p.x() - dx / 2, p.y() - dy / 2)); newPoint->setControlPoint2(QPointF(p.x() + dx / 2, p.y() + dy / 2)); insertPoint(newPoint, KoPathPointIndex(0, pointIndex)); } QString KarbonCalligraphicShape::pathShapeId() const { return KarbonCalligraphicShapeId; } void KarbonCalligraphicShape::simplifyGuidePath() { // do not attempt to simplify if there are too few points if (m_points.count() < 3) { return; } QList points; Q_FOREACH (KarbonCalligraphicPoint *p, m_points) { points.append(p->point()); } // cumulative data used to determine if the point can be removed qreal widthChange = 0; qreal directionChange = 0; QList::iterator i = m_points.begin() + 2; while (i != m_points.end() - 1) { QPointF point = (*i)->point(); qreal width = (*i)->width(); qreal prevWidth = (*(i - 1))->width(); qreal widthDiff = width - prevWidth; widthDiff /= qMax(width, prevWidth); qreal directionDiff = 0; if ((i + 1) != m_points.end()) { QPointF prev = (*(i - 1))->point(); QPointF next = (*(i + 1))->point(); directionDiff = QLineF(prev, point).angleTo(QLineF(point, next)); if (directionDiff > 180) { directionDiff -= 360; } } if (directionChange * directionDiff >= 0 && qAbs(directionChange + directionDiff) < 20 && widthChange * widthDiff >= 0 && qAbs(widthChange + widthDiff) < 0.1) { // deleted point delete *i; i = m_points.erase(i); directionChange += directionDiff; widthChange += widthDiff; } else { // keep point directionChange = 0; widthChange = 0; ++i; } } updatePath(QSizeF()); } diff --git a/plugins/tools/karbonplugins/tools/CalligraphyTool/KarbonCalligraphyTool.h b/plugins/tools/karbonplugins/tools/CalligraphyTool/KarbonCalligraphyTool.h index 5bf3a8d320..e2329d1d9b 100644 --- a/plugins/tools/karbonplugins/tools/CalligraphyTool/KarbonCalligraphyTool.h +++ b/plugins/tools/karbonplugins/tools/CalligraphyTool/KarbonCalligraphyTool.h @@ -1,114 +1,115 @@ /* This file is part of the KDE project * Copyright (C) 2008 Fela Winkelmolen * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KARBONCALLIGRAPHYTOOL_H #define KARBONCALLIGRAPHYTOOL_H #include #include +#include #include #include "KarbonCalligraphyOptionWidget.h" class KoPathShape; class KarbonCalligraphicShape; class KarbonCalligraphyTool : public KoToolBase { Q_OBJECT public: explicit KarbonCalligraphyTool(KoCanvasBase *canvas); ~KarbonCalligraphyTool() override; void paint(QPainter &painter, const KoViewConverter &converter) override; void mousePressEvent(KoPointerEvent *event) override; void mouseMoveEvent(KoPointerEvent *event) override; void mouseReleaseEvent(KoPointerEvent *event) override; QList > createOptionWidgets() override; void activate(ToolActivation activation, const QSet &shapes) override; void deactivate() override; Q_SIGNALS: void pathSelectedChanged(bool selection); private Q_SLOTS: void setUsePath(bool usePath); void setUsePressure(bool usePressure); void setUseAngle(bool useAngle); void setStrokeWidth(double width); void setThinning(double thinning); void setAngle(int angle); // set theangle in degrees void setFixation(double fixation); void setCaps(double caps); void setMass(double mass); // set the mass in user friendly format void setDrag(double drag); void updateSelectedPath(); private: void addPoint(KoPointerEvent *event); // auxiliary function that sets m_angle void setAngle(KoPointerEvent *event); // auxiliary functions to calculate the dynamic parameters // returns the new point and sets speed to the speed QPointF calculateNewPoint(const QPointF &mousePos, QPointF *speed); qreal calculateWidth(qreal pressure); qreal calculateAngle(const QPointF &oldSpeed, const QPointF &newSpeed); QPointF m_lastPoint; KarbonCalligraphicShape *m_shape; // used to determine if the device supports tilt bool m_deviceSupportsTilt; bool m_usePath; // follow selected path bool m_usePressure; // use tablet pressure bool m_useAngle; // use tablet angle qreal m_strokeWidth; qreal m_lastWidth; qreal m_customAngle; // angle set by the user qreal m_angle; // angle to use, may use the device angle, in radians!!! qreal m_fixation; qreal m_thinning; qreal m_caps; qreal m_mass; // in raw format (not user friendly) qreal m_drag; // from 0.0 to 1.0 KoPathShape *m_selectedPath; QPainterPath m_selectedPathOutline; qreal m_followPathPosition; bool m_endOfPath; QPointF m_lastMousePos; bool m_isDrawing; int m_pointCount; // dynamic parameters QPointF m_speed; // used as a vector // last calligraphic shape drawn, if any KarbonCalligraphicShape *m_lastShape; KarbonCalligraphyOptionWidget *m_widget {0}; }; #endif // KARBONCALLIGRAPHYTOOL_H diff --git a/plugins/tools/svgtexttool/SvgTextTool.cpp b/plugins/tools/svgtexttool/SvgTextTool.cpp index b735ea7457..91325f52db 100644 --- a/plugins/tools/svgtexttool/SvgTextTool.cpp +++ b/plugins/tools/svgtexttool/SvgTextTool.cpp @@ -1,435 +1,436 @@ /* This file is part of the KDE project Copyright 2017 Boudewijn Rempt This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "SvgTextTool.h" #include "KoSvgTextShape.h" #include "SvgTextChangeCommand.h" #include +#include #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_assert.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "KoToolManager.h" #include "KoCanvasResourceProvider.h" #include "SvgTextEditor.h" #include "KisHandlePainterHelper.h" #include SvgTextTool::SvgTextTool(KoCanvasBase *canvas) : KoToolBase(canvas) , m_editor(0) , m_dragStart( 0, 0) , m_dragEnd( 0, 0) , m_dragging(false) { } SvgTextTool::~SvgTextTool() { if(m_editor) { m_editor->close(); } } void SvgTextTool::activate(ToolActivation activation, const QSet &shapes) { KoToolBase::activate(activation, shapes); useCursor(Qt::ArrowCursor); if (shapes.size() == 1) { KoSvgTextShape *textShape = dynamic_cast(*shapes.constBegin()); if (!textShape) { koSelection()->deselectAll(); } else { // if we are a text shape...and the proxy tells us we want to edit the shape. open the text editor if (canvas()->selectedShapesProxy()->isRequestingToBeEdited()) { showEditor(); } } } else if (shapes.size() > 1) { KoSvgTextShape *foundTextShape = 0; Q_FOREACH (KoShape *shape, shapes) { KoSvgTextShape *textShape = dynamic_cast(shape); if (textShape) { foundTextShape = textShape; break; } } koSelection()->deselectAll(); if (foundTextShape) { koSelection()->select(foundTextShape); } } } void SvgTextTool::deactivate() { KoToolBase::deactivate(); QRectF updateRect = m_hoveredShapeHighlightRect; KoSvgTextShape *shape = selectedShape(); if (shape) { updateRect |= shape->boundingRect(); } m_hoveredShapeHighlightRect = QRectF(); canvas()->updateCanvas(updateRect); } QWidget *SvgTextTool::createOptionWidget() { QWidget *optionWidget = new QWidget(); QGridLayout *layout = new QGridLayout(optionWidget); m_configGroup = KSharedConfig::openConfig()->group(toolId()); QGroupBox *defsOptions = new QGroupBox(i18n("Create new texts with...")); QVBoxLayout *defOptionsLayout = new QVBoxLayout(); defsOptions->setLayout(defOptionsLayout); m_defFont = new QFontComboBox(); QString storedFont = m_configGroup.readEntry("defaultFont", QApplication::font().family()); m_defFont->setCurrentFont(QFont(storedFont)); defsOptions->layout()->addWidget(m_defFont); m_defPointSize = new QComboBox(); Q_FOREACH (int size, QFontDatabase::standardSizes()) { m_defPointSize->addItem(QString::number(size)+" pt"); } int storedSize = m_configGroup.readEntry("defaultSize", QApplication::font().pointSize()); int sizeIndex = 0; if (QFontDatabase::standardSizes().contains(storedSize)) { sizeIndex = QFontDatabase::standardSizes().indexOf(storedSize); } m_defPointSize->setCurrentIndex(sizeIndex); int checkedAlignment = m_configGroup.readEntry("defaultAlignment", 0); m_defAlignment = new QButtonGroup(); QHBoxLayout *alignButtons = new QHBoxLayout(); alignButtons->addWidget(m_defPointSize); QToolButton *alignLeft = new QToolButton(); alignLeft->setIcon(KisIconUtils::loadIcon("format-justify-left")); alignLeft->setCheckable(true); alignLeft->setToolTip(i18n("Anchor text to the left.")); m_defAlignment->addButton(alignLeft, 0); alignButtons->addWidget(alignLeft); QToolButton *alignCenter = new QToolButton(); alignCenter->setIcon(KisIconUtils::loadIcon("format-justify-center")); alignCenter->setCheckable(true); m_defAlignment->addButton(alignCenter, 1); alignCenter->setToolTip(i18n("Anchor text to the middle.")); alignButtons->addWidget(alignCenter); QToolButton *alignRight = new QToolButton(); alignRight->setIcon(KisIconUtils::loadIcon("format-justify-right")); alignRight->setCheckable(true); m_defAlignment->addButton(alignRight, 2); alignRight->setToolTip(i18n("Anchor text to the right.")); alignButtons->addWidget(alignRight); m_defAlignment->setExclusive(true); if (checkedAlignment<1) { alignLeft->setChecked(true); } else if (checkedAlignment==1) { alignCenter->setChecked(true); } else if (checkedAlignment==2) { alignRight->setChecked(true); } else { alignLeft->setChecked(true); } defOptionsLayout->addLayout(alignButtons); layout->addWidget(defsOptions); connect(m_defAlignment, SIGNAL(buttonClicked(int)), this, SLOT(storeDefaults())); connect(m_defFont, SIGNAL(currentFontChanged(QFont)), this, SLOT(storeDefaults())); connect(m_defPointSize, SIGNAL(currentIndexChanged(int)), this, SLOT(storeDefaults())); m_edit = new QPushButton(optionWidget); m_edit->setText(i18n("Edit Text")); connect(m_edit, SIGNAL(clicked(bool)), SLOT(showEditor())); layout->addWidget(m_edit); return optionWidget; } KoSelection *SvgTextTool::koSelection() const { KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(canvas(), 0); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(canvas()->selectedShapesProxy(), 0); return canvas()->selectedShapesProxy()->selection(); } KoSvgTextShape *SvgTextTool::selectedShape() const { KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(canvas(), 0); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(canvas()->selectedShapesProxy(), 0); QList shapes = koSelection()->selectedEditableShapes(); if (shapes.isEmpty()) return 0; KIS_SAFE_ASSERT_RECOVER_NOOP(shapes.size() == 1); KoSvgTextShape *textShape = dynamic_cast(shapes.first()); return textShape; } void SvgTextTool::showEditor() { KoSvgTextShape *shape = selectedShape(); if (!shape) return; if (!m_editor) { m_editor = new SvgTextEditor(QApplication::activeWindow()); m_editor->setWindowTitle(i18nc("@title:window", "Krita - Edit Text")); m_editor->setWindowModality(Qt::ApplicationModal); m_editor->setAttribute( Qt::WA_QuitOnClose, false ); connect(m_editor, SIGNAL(textUpdated(KoSvgTextShape*,QString,QString,bool)), SLOT(textUpdated(KoSvgTextShape*,QString,QString,bool))); connect(m_editor, SIGNAL(textEditorClosed()), SLOT(slotTextEditorClosed())); m_editor->activateWindow(); // raise on creation only } m_editor->setShape(shape); m_editor->show(); } void SvgTextTool::textUpdated(KoSvgTextShape *shape, const QString &svg, const QString &defs, bool richTextUpdated) { SvgTextChangeCommand *cmd = new SvgTextChangeCommand(shape, svg, defs, richTextUpdated); canvas()->addCommand(cmd); } void SvgTextTool::slotTextEditorClosed() { // change tools to the shape selection tool when we close the text editor to allow moving and further editing of the object. // most of the time when we edit text, the shape selection tool is where we left off anyway KoToolManager::instance()->switchToolRequested("InteractionTool"); } QString SvgTextTool::generateDefs() { QString font = m_defFont->currentFont().family(); QString size = QString::number(QFontDatabase::standardSizes().at(m_defPointSize->currentIndex() > -1 ? m_defPointSize->currentIndex() : 0)); QString textAnchor = "middle"; if (m_defAlignment->button(0)->isChecked()) { textAnchor = "start"; } if (m_defAlignment->button(2)->isChecked()) { textAnchor = "end"; } QString fontColor = canvas()->resourceManager()->foregroundColor().toQColor().name(); return QString("\n \n").arg(font, size, fontColor, textAnchor); } void SvgTextTool::storeDefaults() { m_configGroup = KSharedConfig::openConfig()->group(toolId()); m_configGroup.writeEntry("defaultFont", m_defFont->currentFont().family()); m_configGroup.writeEntry("defaultSize", QFontDatabase::standardSizes().at(m_defPointSize->currentIndex() > -1 ? m_defPointSize->currentIndex() : 0)); m_configGroup.writeEntry("defaultAlignment", m_defAlignment->checkedId()); } void SvgTextTool::paint(QPainter &gc, const KoViewConverter &converter) { if (!isActivated()) return; KoShape::applyConversion(gc, converter); KisHandlePainterHelper handlePainter(&gc); if (m_dragging) { QPolygonF poly(QRectF(m_dragStart, m_dragEnd)); handlePainter.setHandleStyle(KisHandleStyle::primarySelection()); handlePainter.drawRubberLine(poly); } KoSvgTextShape *shape = selectedShape(); if (shape) { handlePainter.setHandleStyle(KisHandleStyle::primarySelection()); QPainterPath path; path.addRect(shape->boundingRect()); handlePainter.drawPath(path); } if (!m_hoveredShapeHighlightRect.isEmpty()) { handlePainter.setHandleStyle(KisHandleStyle::highlightedPrimaryHandlesWithSolidOutline()); QPainterPath path; path.addRect(m_hoveredShapeHighlightRect); handlePainter.drawPath(path); } } void SvgTextTool::mousePressEvent(KoPointerEvent *event) { KoSvgTextShape *selectedShape = this->selectedShape(); KoSvgTextShape *hoveredShape = dynamic_cast(canvas()->shapeManager()->shapeAt(event->point)); if (!selectedShape || hoveredShape != selectedShape) { canvas()->shapeManager()->selection()->deselectAll(); if (hoveredShape) { canvas()->shapeManager()->selection()->select(hoveredShape); } else { m_dragStart = m_dragEnd = event->point; m_dragging = true; event->accept(); } } } void SvgTextTool::mouseMoveEvent(KoPointerEvent *event) { QRectF updateRect = m_hoveredShapeHighlightRect; if (m_dragging) { m_dragEnd = event->point; m_hoveredShapeHighlightRect = QRectF(); updateRect |= QRectF(m_dragStart, m_dragEnd).normalized().toAlignedRect(); event->accept(); } else { KoSvgTextShape *hoveredShape = dynamic_cast(canvas()->shapeManager()->shapeAt(event->point)); if (hoveredShape) { m_hoveredShapeHighlightRect = hoveredShape->boundingRect(); updateRect |= m_hoveredShapeHighlightRect; } else { m_hoveredShapeHighlightRect = QRect(); } event->ignore(); } if (!updateRect.isEmpty()) { canvas()->updateCanvas(kisGrowRect(updateRect, 100)); } } void SvgTextTool::mouseReleaseEvent(KoPointerEvent *event) { if (m_dragging) { QRectF rectangle = QRectF(m_dragStart, m_dragEnd).normalized(); if (rectangle.width() < 4 && rectangle.height() < 4) { m_dragging = false; event->accept(); return; } KoShapeFactoryBase *factory = KoShapeRegistry::instance()->value("KoSvgTextShapeID"); KoProperties *params = new KoProperties();//Fill these with "svgText", "defs" and "shapeRect" params->setProperty("defs", QVariant(generateDefs())); if (m_dragging) { m_dragEnd = event->point; m_dragging = false; //The following show only happen when we're creating preformatted text. If we're making //Word-wrapped text, it should take the rectangle unmodified. int size = QFontDatabase::standardSizes().at(m_defPointSize->currentIndex() > -1 ? m_defPointSize->currentIndex() : 0); QFont font = m_defFont->currentFont(); font.setPointSize(size); rectangle.setTop(rectangle.top()+QFontMetrics(font).lineSpacing()); if (m_defAlignment->button(1)->isChecked()) { rectangle.setLeft(rectangle.center().x()); } else if (m_defAlignment->button(2)->isChecked()) { qreal right = rectangle.right(); rectangle.setRight(right+10); rectangle.setLeft(right); } params->setProperty("shapeRect", QVariant(rectangle)); } KoShape *textShape = factory->createShape( params, canvas()->shapeController()->resourceManager()); KUndo2Command *parentCommand = new KUndo2Command(); new KoKeepShapesSelectedCommand(koSelection()->selectedShapes(), {}, canvas()->selectedShapesProxy(), false, parentCommand); KUndo2Command *cmd = canvas()->shapeController()->addShape(textShape, 0, parentCommand); parentCommand->setText(cmd->text()); new KoKeepShapesSelectedCommand({}, {textShape}, canvas()->selectedShapesProxy(), true, parentCommand); canvas()->addCommand(parentCommand); showEditor(); event->accept(); } else if (m_editor) { showEditor(); event->accept(); } } void SvgTextTool::keyPressEvent(QKeyEvent *event) { if (event->key()==Qt::Key_Enter || event->key()==Qt::Key_Return) { showEditor(); event->accept(); } else { event->ignore(); } } void SvgTextTool::mouseDoubleClickEvent(KoPointerEvent *event) { if (canvas()->shapeManager()->shapeAt(event->point) != selectedShape()) { event->ignore(); // allow the event to be used by another return; } showEditor(); if(m_editor) { m_editor->raise(); m_editor->activateWindow(); } event->accept(); } diff --git a/plugins/tools/tool_transform2/kis_free_transform_strategy.cpp b/plugins/tools/tool_transform2/kis_free_transform_strategy.cpp index 9fd5578c6d..6f5c298f1d 100644 --- a/plugins/tools/tool_transform2/kis_free_transform_strategy.cpp +++ b/plugins/tools/tool_transform2/kis_free_transform_strategy.cpp @@ -1,731 +1,732 @@ /* * 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_free_transform_strategy.h" #include #include +#include #include #include #include "kis_coordinates_converter.h" #include "tool_transform_args.h" #include "transform_transaction_properties.h" #include "krita_utils.h" #include "kis_cursor.h" #include "kis_transform_utils.h" #include "kis_free_transform_strategy_gsl_helpers.h" #include "kis_algebra_2d.h" enum StrokeFunction { ROTATE = 0, MOVE, RIGHTSCALE, TOPRIGHTSCALE, TOPSCALE, TOPLEFTSCALE, LEFTSCALE, BOTTOMLEFTSCALE, BOTTOMSCALE, BOTTOMRIGHTSCALE, BOTTOMSHEAR, RIGHTSHEAR, TOPSHEAR, LEFTSHEAR, MOVECENTER, PERSPECTIVE }; struct KisFreeTransformStrategy::Private { Private(KisFreeTransformStrategy *_q, const KisCoordinatesConverter *_converter, ToolTransformArgs &_currentArgs, TransformTransactionProperties &_transaction) : q(_q), converter(_converter), currentArgs(_currentArgs), transaction(_transaction), imageTooBig(false), isTransforming(false) { scaleCursors[0] = KisCursor::sizeHorCursor(); scaleCursors[1] = KisCursor::sizeFDiagCursor(); scaleCursors[2] = KisCursor::sizeVerCursor(); scaleCursors[3] = KisCursor::sizeBDiagCursor(); scaleCursors[4] = KisCursor::sizeHorCursor(); scaleCursors[5] = KisCursor::sizeFDiagCursor(); scaleCursors[6] = KisCursor::sizeVerCursor(); scaleCursors[7] = KisCursor::sizeBDiagCursor(); shearCursorPixmap.load(":/shear_cursor.png"); } KisFreeTransformStrategy *q; /// standard members /// const KisCoordinatesConverter *converter; ////// ToolTransformArgs ¤tArgs; ////// TransformTransactionProperties &transaction; QTransform thumbToImageTransform; QImage originalImage; QTransform paintingTransform; QPointF paintingOffset; QTransform handlesTransform; /// custom members /// StrokeFunction function; struct HandlePoints { QPointF topLeft; QPointF topMiddle; QPointF topRight; QPointF middleLeft; QPointF rotationCenter; QPointF middleRight; QPointF bottomLeft; QPointF bottomMiddle; QPointF bottomRight; }; HandlePoints transformedHandles; QTransform transform; QCursor scaleCursors[8]; // cursors for the 8 directions QPixmap shearCursorPixmap; bool imageTooBig; ToolTransformArgs clickArgs; QPointF clickPos; bool isTransforming; QCursor getScaleCursor(const QPointF &handlePt); QCursor getShearCursor(const QPointF &start, const QPointF &end); void recalculateTransformations(); void recalculateTransformedHandles(); }; KisFreeTransformStrategy::KisFreeTransformStrategy(const KisCoordinatesConverter *converter, KoSnapGuide *snapGuide, ToolTransformArgs ¤tArgs, TransformTransactionProperties &transaction) : KisSimplifiedActionPolicyStrategy(converter, snapGuide), m_d(new Private(this, converter, currentArgs, transaction)) { } KisFreeTransformStrategy::~KisFreeTransformStrategy() { } void KisFreeTransformStrategy::Private::recalculateTransformedHandles() { transformedHandles.topLeft = transform.map(transaction.originalTopLeft()); transformedHandles.topMiddle = transform.map(transaction.originalMiddleTop()); transformedHandles.topRight = transform.map(transaction.originalTopRight()); transformedHandles.middleLeft = transform.map(transaction.originalMiddleLeft()); transformedHandles.rotationCenter = transform.map(currentArgs.originalCenter() + currentArgs.rotationCenterOffset()); transformedHandles.middleRight = transform.map(transaction.originalMiddleRight()); transformedHandles.bottomLeft = transform.map(transaction.originalBottomLeft()); transformedHandles.bottomMiddle = transform.map(transaction.originalMiddleBottom()); transformedHandles.bottomRight = transform.map(transaction.originalBottomRight()); } void KisFreeTransformStrategy::setTransformFunction(const QPointF &mousePos, bool perspectiveModifierActive) { if (perspectiveModifierActive && !m_d->transaction.shouldAvoidPerspectiveTransform()) { m_d->function = PERSPECTIVE; return; } QPolygonF transformedPolygon = m_d->transform.map(QPolygonF(m_d->transaction.originalRect())); qreal handleRadius = KisTransformUtils::effectiveHandleGrabRadius(m_d->converter); qreal rotationHandleRadius = KisTransformUtils::effectiveHandleGrabRadius(m_d->converter); StrokeFunction defaultFunction = transformedPolygon.containsPoint(mousePos, Qt::OddEvenFill) ? MOVE : ROTATE; KisTransformUtils::HandleChooser handleChooser(mousePos, defaultFunction); handleChooser.addFunction(m_d->transformedHandles.topMiddle, handleRadius, TOPSCALE); handleChooser.addFunction(m_d->transformedHandles.topRight, handleRadius, TOPRIGHTSCALE); handleChooser.addFunction(m_d->transformedHandles.middleRight, handleRadius, RIGHTSCALE); handleChooser.addFunction(m_d->transformedHandles.bottomRight, handleRadius, BOTTOMRIGHTSCALE); handleChooser.addFunction(m_d->transformedHandles.bottomMiddle, handleRadius, BOTTOMSCALE); handleChooser.addFunction(m_d->transformedHandles.bottomLeft, handleRadius, BOTTOMLEFTSCALE); handleChooser.addFunction(m_d->transformedHandles.middleLeft, handleRadius, LEFTSCALE); handleChooser.addFunction(m_d->transformedHandles.topLeft, handleRadius, TOPLEFTSCALE); handleChooser.addFunction(m_d->transformedHandles.rotationCenter, rotationHandleRadius, MOVECENTER); m_d->function = handleChooser.function(); if (m_d->function == ROTATE || m_d->function == MOVE) { QRectF originalRect = m_d->transaction.originalRect(); QPointF t = m_d->transform.inverted().map(mousePos); if (t.x() >= originalRect.left() && t.x() <= originalRect.right()) { if (fabs(t.y() - originalRect.top()) <= handleRadius) m_d->function = TOPSHEAR; if (fabs(t.y() - originalRect.bottom()) <= handleRadius) m_d->function = BOTTOMSHEAR; } if (t.y() >= originalRect.top() && t.y() <= originalRect.bottom()) { if (fabs(t.x() - originalRect.left()) <= handleRadius) m_d->function = LEFTSHEAR; if (fabs(t.x() - originalRect.right()) <= handleRadius) m_d->function = RIGHTSHEAR; } } } QCursor KisFreeTransformStrategy::Private::getScaleCursor(const QPointF &handlePt) { QPointF handlePtInWidget = converter->imageToWidget(handlePt); QPointF centerPtInWidget = converter->imageToWidget(currentArgs.transformedCenter()); QPointF direction = handlePtInWidget - centerPtInWidget; qreal angle = atan2(direction.y(), direction.x()); angle = normalizeAngle(angle); int octant = qRound(angle * 4. / M_PI) % 8; return scaleCursors[octant]; } QCursor KisFreeTransformStrategy::Private::getShearCursor(const QPointF &start, const QPointF &end) { QPointF startPtInWidget = converter->imageToWidget(start); QPointF endPtInWidget = converter->imageToWidget(end); QPointF direction = endPtInWidget - startPtInWidget; qreal angle = atan2(-direction.y(), direction.x()); return QCursor(shearCursorPixmap.transformed(QTransform().rotateRadians(-angle))); } QCursor KisFreeTransformStrategy::getCurrentCursor() const { QCursor cursor; switch (m_d->function) { case MOVE: cursor = KisCursor::moveCursor(); break; case ROTATE: cursor = KisCursor::rotateCursor(); break; case PERSPECTIVE: //TODO: find another cursor for perspective cursor = KisCursor::rotateCursor(); break; case RIGHTSCALE: cursor = m_d->getScaleCursor(m_d->transformedHandles.middleRight); break; case TOPSCALE: cursor = m_d->getScaleCursor(m_d->transformedHandles.topMiddle); break; case LEFTSCALE: cursor = m_d->getScaleCursor(m_d->transformedHandles.middleLeft); break; case BOTTOMSCALE: cursor = m_d->getScaleCursor(m_d->transformedHandles.bottomMiddle); break; case TOPRIGHTSCALE: cursor = m_d->getScaleCursor(m_d->transformedHandles.topRight); break; case BOTTOMLEFTSCALE: cursor = m_d->getScaleCursor(m_d->transformedHandles.bottomLeft); break; case TOPLEFTSCALE: cursor = m_d->getScaleCursor(m_d->transformedHandles.topLeft); break; case BOTTOMRIGHTSCALE: cursor = m_d->getScaleCursor(m_d->transformedHandles.bottomRight); break; case MOVECENTER: cursor = KisCursor::handCursor(); break; case BOTTOMSHEAR: cursor = m_d->getShearCursor(m_d->transformedHandles.bottomLeft, m_d->transformedHandles.bottomRight); break; case RIGHTSHEAR: cursor = m_d->getShearCursor(m_d->transformedHandles.bottomRight, m_d->transformedHandles.topRight); break; case TOPSHEAR: cursor = m_d->getShearCursor(m_d->transformedHandles.topRight, m_d->transformedHandles.topLeft); break; case LEFTSHEAR: cursor = m_d->getShearCursor(m_d->transformedHandles.topLeft, m_d->transformedHandles.bottomLeft); break; } return cursor; } void KisFreeTransformStrategy::paint(QPainter &gc) { gc.save(); gc.setOpacity(m_d->transaction.basePreviewOpacity()); gc.setTransform(m_d->paintingTransform, true); gc.drawImage(m_d->paintingOffset, originalImage()); gc.restore(); // Draw Handles QRectF handleRect = KisTransformUtils::handleRect(KisTransformUtils::handleVisualRadius, m_d->handlesTransform, m_d->transaction.originalRect(), 0, 0); qreal rX = 1; qreal rY = 1; QRectF rotationCenterRect = KisTransformUtils::handleRect(KisTransformUtils::rotationHandleVisualRadius, m_d->handlesTransform, m_d->transaction.originalRect(), &rX, &rY); QPainterPath handles; handles.moveTo(m_d->transaction.originalTopLeft()); handles.lineTo(m_d->transaction.originalTopRight()); handles.lineTo(m_d->transaction.originalBottomRight()); handles.lineTo(m_d->transaction.originalBottomLeft()); handles.lineTo(m_d->transaction.originalTopLeft()); handles.addRect(handleRect.translated(m_d->transaction.originalTopLeft())); handles.addRect(handleRect.translated(m_d->transaction.originalTopRight())); handles.addRect(handleRect.translated(m_d->transaction.originalBottomLeft())); handles.addRect(handleRect.translated(m_d->transaction.originalBottomRight())); handles.addRect(handleRect.translated(m_d->transaction.originalMiddleLeft())); handles.addRect(handleRect.translated(m_d->transaction.originalMiddleRight())); handles.addRect(handleRect.translated(m_d->transaction.originalMiddleTop())); handles.addRect(handleRect.translated(m_d->transaction.originalMiddleBottom())); QPointF rotationCenter = m_d->currentArgs.originalCenter() + m_d->currentArgs.rotationCenterOffset(); QPointF dx(rX + 3, 0); QPointF dy(0, rY + 3); handles.addEllipse(rotationCenterRect.translated(rotationCenter)); handles.moveTo(rotationCenter - dx); handles.lineTo(rotationCenter + dx); handles.moveTo(rotationCenter - dy); handles.lineTo(rotationCenter + dy); gc.save(); if (m_d->isTransforming) { gc.setOpacity(0.1); } //gc.setTransform(m_d->handlesTransform, true); <-- don't do like this! QPainterPath mappedHandles = m_d->handlesTransform.map(handles); QPen pen[2]; pen[0].setWidth(1); pen[1].setWidth(2); pen[1].setColor(Qt::lightGray); for (int i = 1; i >= 0; --i) { gc.setPen(pen[i]); gc.drawPath(mappedHandles); } gc.restore(); } void KisFreeTransformStrategy::externalConfigChanged() { m_d->recalculateTransformations(); } bool KisFreeTransformStrategy::beginPrimaryAction(const QPointF &pt) { m_d->clickArgs = m_d->currentArgs; m_d->clickPos = pt; return true; } void KisFreeTransformStrategy::continuePrimaryAction(const QPointF &mousePos, bool shiftModifierActive, bool altModifierActive) { // Note: "shiftModifierActive" just tells us if the shift key is being pressed // Note: "altModifierActive" just tells us if the alt key is being pressed m_d->isTransforming = true; const QPointF anchorPoint = m_d->clickArgs.originalCenter() + m_d->clickArgs.rotationCenterOffset(); switch (m_d->function) { case MOVE: { QPointF diff = mousePos - m_d->clickPos; if (shiftModifierActive) { KisTransformUtils::MatricesPack m(m_d->clickArgs); QTransform t = m.S * m.projectedP; QPointF originalDiff = t.inverted().map(diff); if (qAbs(originalDiff.x()) >= qAbs(originalDiff.y())) { originalDiff.setY(0); } else { originalDiff.setX(0); } diff = t.map(originalDiff); } m_d->currentArgs.setTransformedCenter(m_d->clickArgs.transformedCenter() + diff); break; } case ROTATE: { const KisTransformUtils::MatricesPack clickM(m_d->clickArgs); const QTransform clickT = clickM.finalTransform(); const QPointF rotationCenter = m_d->clickArgs.originalCenter() + m_d->clickArgs.rotationCenterOffset(); const QPointF clickMouseImagePos = clickT.inverted().map(m_d->clickPos) - rotationCenter; const QPointF mouseImagePos = clickT.inverted().map(mousePos) - rotationCenter; const qreal a1 = atan2(clickMouseImagePos.y(), clickMouseImagePos.x()); const qreal a2 = atan2(mouseImagePos.y(), mouseImagePos.x()); const qreal theta = KisAlgebra2D::signZZ(clickT.determinant()) * (a2 - a1); // Snap with shift key if (shiftModifierActive) { const qreal snapAngle = M_PI_4 / 6.0; // fifteen degrees qint32 thetaIndex = static_cast((theta / snapAngle) + 0.5); m_d->currentArgs.setAZ(normalizeAngle(thetaIndex * snapAngle)); } else { m_d->currentArgs.setAZ(normalizeAngle(m_d->clickArgs.aZ() + theta)); } KisTransformUtils::MatricesPack m(m_d->currentArgs); QTransform t = m.finalTransform(); QPointF newRotationCenter = t.map(m_d->currentArgs.originalCenter() + m_d->currentArgs.rotationCenterOffset()); QPointF oldRotationCenter = clickT.map(m_d->clickArgs.originalCenter() + m_d->clickArgs.rotationCenterOffset()); m_d->currentArgs.setTransformedCenter(m_d->currentArgs.transformedCenter() + oldRotationCenter - newRotationCenter); } break; case PERSPECTIVE: { QPointF diff = mousePos - m_d->clickPos; double thetaX = - diff.y() * M_PI / m_d->transaction.originalHalfHeight() / 2 / fabs(m_d->currentArgs.scaleY()); m_d->currentArgs.setAX(normalizeAngle(m_d->clickArgs.aX() + thetaX)); qreal sign = qAbs(m_d->currentArgs.aX() - M_PI) < M_PI / 2 ? -1.0 : 1.0; double thetaY = sign * diff.x() * M_PI / m_d->transaction.originalHalfWidth() / 2 / fabs(m_d->currentArgs.scaleX()); m_d->currentArgs.setAY(normalizeAngle(m_d->clickArgs.aY() + thetaY)); KisTransformUtils::MatricesPack m(m_d->currentArgs); QTransform t = m.finalTransform(); QPointF newRotationCenter = t.map(m_d->currentArgs.originalCenter() + m_d->currentArgs.rotationCenterOffset()); KisTransformUtils::MatricesPack clickM(m_d->clickArgs); QTransform clickT = clickM.finalTransform(); QPointF oldRotationCenter = clickT.map(m_d->clickArgs.originalCenter() + m_d->clickArgs.rotationCenterOffset()); m_d->currentArgs.setTransformedCenter(m_d->currentArgs.transformedCenter() + oldRotationCenter - newRotationCenter); } break; case TOPSCALE: case BOTTOMSCALE: { QPointF staticPoint; QPointF movingPoint; qreal extraSign; if (m_d->function == TOPSCALE) { staticPoint = m_d->transaction.originalMiddleBottom(); movingPoint = m_d->transaction.originalMiddleTop(); extraSign = -1.0; } else { staticPoint = m_d->transaction.originalMiddleTop(); movingPoint = m_d->transaction.originalMiddleBottom(); extraSign = 1.0; } // override scale static point if it is locked if ((m_d->currentArgs.transformAroundRotationCenter() ^ altModifierActive) && !qFuzzyCompare(anchorPoint.y(), movingPoint.y())) { staticPoint = anchorPoint; } QPointF mouseImagePos = m_d->transform.inverted().map(mousePos); qreal sign = mouseImagePos.y() <= staticPoint.y() ? -extraSign : extraSign; m_d->currentArgs.setScaleY(sign * m_d->currentArgs.scaleY()); QPointF staticPointInView = m_d->transform.map(staticPoint); qreal dist = kisDistance(staticPointInView, mousePos); GSL::ScaleResult1D result = GSL::calculateScaleY(m_d->currentArgs, staticPoint, staticPointInView, movingPoint, dist); if (shiftModifierActive || m_d->currentArgs.keepAspectRatio()) { qreal aspectRatio = m_d->clickArgs.scaleX() / m_d->clickArgs.scaleY(); m_d->currentArgs.setScaleX(aspectRatio * result.scale); } m_d->currentArgs.setScaleY(result.scale); m_d->currentArgs.setTransformedCenter(result.transformedCenter); break; } case LEFTSCALE: case RIGHTSCALE: { QPointF staticPoint; QPointF movingPoint; qreal extraSign; if (m_d->function == LEFTSCALE) { staticPoint = m_d->transaction.originalMiddleRight(); movingPoint = m_d->transaction.originalMiddleLeft(); extraSign = -1.0; } else { staticPoint = m_d->transaction.originalMiddleLeft(); movingPoint = m_d->transaction.originalMiddleRight(); extraSign = 1.0; } // override scale static point if it is locked if ((m_d->currentArgs.transformAroundRotationCenter() ^ altModifierActive) && !qFuzzyCompare(anchorPoint.x(), movingPoint.x())) { staticPoint = anchorPoint; } QPointF mouseImagePos = m_d->transform.inverted().map(mousePos); qreal sign = mouseImagePos.x() <= staticPoint.x() ? -extraSign : extraSign; m_d->currentArgs.setScaleX(sign * m_d->currentArgs.scaleX()); QPointF staticPointInView = m_d->transform.map(staticPoint); qreal dist = kisDistance(staticPointInView, mousePos); GSL::ScaleResult1D result = GSL::calculateScaleX(m_d->currentArgs, staticPoint, staticPointInView, movingPoint, dist); if (shiftModifierActive || m_d->currentArgs.keepAspectRatio()) { qreal aspectRatio = m_d->clickArgs.scaleY() / m_d->clickArgs.scaleX(); m_d->currentArgs.setScaleY(aspectRatio * result.scale); } m_d->currentArgs.setScaleX(result.scale); m_d->currentArgs.setTransformedCenter(result.transformedCenter); break; } case TOPRIGHTSCALE: case BOTTOMRIGHTSCALE: case TOPLEFTSCALE: case BOTTOMLEFTSCALE: { QPointF staticPoint; QPointF movingPoint; if (m_d->function == TOPRIGHTSCALE) { staticPoint = m_d->transaction.originalBottomLeft(); movingPoint = m_d->transaction.originalTopRight(); } else if (m_d->function == BOTTOMRIGHTSCALE) { staticPoint = m_d->transaction.originalTopLeft(); movingPoint = m_d->transaction.originalBottomRight(); } else if (m_d->function == TOPLEFTSCALE) { staticPoint = m_d->transaction.originalBottomRight(); movingPoint = m_d->transaction.originalTopLeft(); } else { staticPoint = m_d->transaction.originalTopRight(); movingPoint = m_d->transaction.originalBottomLeft(); } // override scale static point if it is locked if ((m_d->currentArgs.transformAroundRotationCenter() ^ altModifierActive) && !(qFuzzyCompare(anchorPoint.x(), movingPoint.x()) || qFuzzyCompare(anchorPoint.y(), movingPoint.y()))) { staticPoint = anchorPoint; } QPointF staticPointInView = m_d->transform.map(staticPoint); QPointF movingPointInView = mousePos; if (shiftModifierActive || m_d->currentArgs.keepAspectRatio()) { KisTransformUtils::MatricesPack m(m_d->clickArgs); QTransform t = m.finalTransform(); QPointF refDiff = t.map(movingPoint) - staticPointInView; QPointF realDiff = mousePos - staticPointInView; realDiff = kisProjectOnVector(refDiff, realDiff); movingPointInView = staticPointInView + realDiff; } GSL::ScaleResult2D result = GSL::calculateScale2D(m_d->currentArgs, staticPoint, staticPointInView, movingPoint, movingPointInView); m_d->currentArgs.setScaleX(result.scaleX); m_d->currentArgs.setScaleY(result.scaleY); m_d->currentArgs.setTransformedCenter(result.transformedCenter); break; } case MOVECENTER: { QPointF pt = m_d->transform.inverted().map(mousePos); pt = KisTransformUtils::clipInRect(pt, m_d->transaction.originalRect()); QPointF newRotationCenterOffset = pt - m_d->currentArgs.originalCenter(); if (shiftModifierActive) { if (qAbs(newRotationCenterOffset.x()) > qAbs(newRotationCenterOffset.y())) { newRotationCenterOffset.ry() = 0; } else { newRotationCenterOffset.rx() = 0; } } m_d->currentArgs.setRotationCenterOffset(newRotationCenterOffset); emit requestResetRotationCenterButtons(); } break; case TOPSHEAR: case BOTTOMSHEAR: { KisTransformUtils::MatricesPack m(m_d->clickArgs); QTransform backwardT = (m.S * m.projectedP).inverted(); QPointF diff = backwardT.map(mousePos - m_d->clickPos); qreal sign = m_d->function == BOTTOMSHEAR ? 1.0 : -1.0; // get the dx pixels corresponding to the current shearX factor qreal dx = sign * m_d->clickArgs.shearX() * m_d->clickArgs.scaleY() * m_d->transaction.originalHalfHeight(); // get the dx pixels corresponding to the current shearX factor dx += diff.x(); // calculate the new shearX factor m_d->currentArgs.setShearX(sign * dx / m_d->currentArgs.scaleY() / m_d->transaction.originalHalfHeight()); // calculate the new shearX factor break; } case LEFTSHEAR: case RIGHTSHEAR: { KisTransformUtils::MatricesPack m(m_d->clickArgs); QTransform backwardT = (m.S * m.projectedP).inverted(); QPointF diff = backwardT.map(mousePos - m_d->clickPos); qreal sign = m_d->function == RIGHTSHEAR ? 1.0 : -1.0; // get the dx pixels corresponding to the current shearX factor qreal dy = sign * m_d->clickArgs.shearY() * m_d->clickArgs.scaleX() * m_d->transaction.originalHalfWidth(); dy += diff.y(); // calculate the new shearY factor m_d->currentArgs.setShearY(sign * dy / m_d->clickArgs.scaleX() / m_d->transaction.originalHalfWidth()); break; } } m_d->recalculateTransformations(); } bool KisFreeTransformStrategy::endPrimaryAction() { bool shouldSave = !m_d->imageTooBig; m_d->isTransforming = false; if (m_d->imageTooBig) { m_d->currentArgs = m_d->clickArgs; m_d->recalculateTransformations(); } return shouldSave; } void KisFreeTransformStrategy::Private::recalculateTransformations() { KisTransformUtils::MatricesPack m(currentArgs); QTransform sanityCheckMatrix = m.TS * m.SC * m.S * m.projectedP; /** * The center of the original image should still * stay the origin of CS */ KIS_ASSERT_RECOVER_NOOP(sanityCheckMatrix.map(currentArgs.originalCenter()).manhattanLength() < 1e-4); transform = m.finalTransform(); QTransform viewScaleTransform = converter->imageToDocumentTransform() * converter->documentToFlakeTransform(); handlesTransform = transform * viewScaleTransform; QTransform tl = QTransform::fromTranslate(transaction.originalTopLeft().x(), transaction.originalTopLeft().y()); paintingTransform = tl.inverted() * q->thumbToImageTransform() * tl * transform * viewScaleTransform; paintingOffset = transaction.originalTopLeft(); // check whether image is too big to be displayed or not imageTooBig = KisTransformUtils::checkImageTooBig(transaction.originalRect(), m); // recalculate cached handles position recalculateTransformedHandles(); emit q->requestShowImageTooBig(imageTooBig); } diff --git a/plugins/tools/tool_transform2/kis_liquify_paint_helper.cpp b/plugins/tools/tool_transform2/kis_liquify_paint_helper.cpp index 93b0067db8..83b79d4f7f 100644 --- a/plugins/tools/tool_transform2/kis_liquify_paint_helper.cpp +++ b/plugins/tools/tool_transform2/kis_liquify_paint_helper.cpp @@ -1,142 +1,144 @@ /* * 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_liquify_paint_helper.h" +#include + #include "kis_algebra_2d.h" #include "KoPointerEvent.h" #include #include "kis_painting_information_builder.h" #include "kis_liquify_transform_worker.h" #include #include "kis_coordinates_converter.h" #include "kis_liquify_paintop.h" #include "kis_liquify_properties.h" struct KisLiquifyPaintHelper::Private { Private(const KisCoordinatesConverter *_converter) : converter(_converter), infoBuilder(new KisConverterPaintingInformationBuilder(converter)), hasPaintedAtLeastOnce(false) { } KisPaintInformation previousPaintInfo; QScopedPointer paintOp; KisDistanceInformation currentDistance; const KisCoordinatesConverter *converter; QScopedPointer infoBuilder; QTime strokeTime; bool hasPaintedAtLeastOnce; KisDistanceInformation previousDistanceInfo; KisPaintOpUtils::PositionHistory lastOutlinePos; void updatePreviousPaintInfo(const KisPaintInformation &info); }; KisLiquifyPaintHelper::KisLiquifyPaintHelper(const KisCoordinatesConverter *converter) : m_d(new Private(converter)) { } KisLiquifyPaintHelper::~KisLiquifyPaintHelper() { } void KisLiquifyPaintHelper::Private::updatePreviousPaintInfo(const KisPaintInformation &info) { QPointF prevPos = lastOutlinePos.pushThroughHistory(info.pos(), converter->effectiveZoom()); qreal angle = KisAlgebra2D::directionBetweenPoints(prevPos, info.pos(), 0); previousDistanceInfo = KisDistanceInformation(prevPos, angle); previousPaintInfo = info; } QPainterPath KisLiquifyPaintHelper::brushOutline(const KisLiquifyProperties &props) { KisPaintInformation::DistanceInformationRegistrar registrar = m_d->previousPaintInfo.registerDistanceInformation(&m_d->previousDistanceInfo); return KisLiquifyPaintop::brushOutline(props, m_d->previousPaintInfo); } void KisLiquifyPaintHelper::configurePaintOp(const KisLiquifyProperties &props, KisLiquifyTransformWorker *worker) { m_d->paintOp.reset(new KisLiquifyPaintop(props, worker)); } void KisLiquifyPaintHelper::startPaint(KoPointerEvent *event, const KoCanvasResourceProvider *manager) { KIS_ASSERT_RECOVER_RETURN(m_d->paintOp); m_d->strokeTime.start(); KisPaintInformation pi = m_d->infoBuilder->startStroke(event, m_d->strokeTime.elapsed(), manager); m_d->updatePreviousPaintInfo(pi); m_d->hasPaintedAtLeastOnce = false; } void KisLiquifyPaintHelper::continuePaint(KoPointerEvent *event) { KIS_ASSERT_RECOVER_RETURN(m_d->paintOp); KisPaintInformation pi = m_d->infoBuilder->continueStroke(event, m_d->strokeTime.elapsed()); KisPaintOpUtils::paintLine(*m_d->paintOp.data(), m_d->previousPaintInfo, pi, &m_d->currentDistance, false, false); m_d->updatePreviousPaintInfo(pi); m_d->hasPaintedAtLeastOnce = true; } bool KisLiquifyPaintHelper::endPaint(KoPointerEvent *event) { KIS_ASSERT_RECOVER(m_d->paintOp) { return false; } if (!m_d->hasPaintedAtLeastOnce) { KisPaintInformation pi = m_d->infoBuilder->continueStroke(event, m_d->strokeTime.elapsed()); pi.paintAt(*m_d->paintOp.data(), &m_d->previousDistanceInfo); } m_d->paintOp.reset(); return !m_d->hasPaintedAtLeastOnce; } void KisLiquifyPaintHelper::hoverPaint(KoPointerEvent *event) { QPointF imagePoint = m_d->converter->documentToImage(event->pos()); KisPaintInformation pi = m_d->infoBuilder->hover(imagePoint, event); m_d->updatePreviousPaintInfo(pi); } diff --git a/plugins/tools/tool_transform2/kis_liquify_transform_strategy.cpp b/plugins/tools/tool_transform2/kis_liquify_transform_strategy.cpp index 96628801aa..12d569bf6d 100644 --- a/plugins/tools/tool_transform2/kis_liquify_transform_strategy.cpp +++ b/plugins/tools/tool_transform2/kis_liquify_transform_strategy.cpp @@ -1,309 +1,310 @@ /* * 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_liquify_transform_strategy.h" #include #include #include +#include #include "KoPointerEvent.h" #include "kis_coordinates_converter.h" #include "tool_transform_args.h" #include "transform_transaction_properties.h" #include "krita_utils.h" #include "kis_cursor.h" #include "kis_transform_utils.h" #include "kis_algebra_2d.h" #include "kis_liquify_paint_helper.h" #include "kis_liquify_transform_worker.h" #include "KoCanvasResourceProvider.h" struct KisLiquifyTransformStrategy::Private { Private(KisLiquifyTransformStrategy *_q, const KisCoordinatesConverter *_converter, ToolTransformArgs &_currentArgs, TransformTransactionProperties &_transaction, const KoCanvasResourceProvider *_manager) : manager(_manager), q(_q), converter(_converter), currentArgs(_currentArgs), transaction(_transaction), helper(_converter), recalculateOnNextRedraw(false) { } const KoCanvasResourceProvider *manager; KisLiquifyTransformStrategy * const q; /// standard members /// const KisCoordinatesConverter *converter; ////// ToolTransformArgs ¤tArgs; ////// TransformTransactionProperties &transaction; QTransform paintingTransform; QPointF paintingOffset; QTransform handlesTransform; /// custom members /// QImage transformedImage; // size-gesture-related QPointF lastMouseWidgetPos; QPointF startResizeImagePos; QPoint startResizeGlobalCursorPos; KisLiquifyPaintHelper helper; bool recalculateOnNextRedraw; void recalculateTransformations(); inline QPointF imageToThumb(const QPointF &pt, bool useFlakeOptimization); }; KisLiquifyTransformStrategy::KisLiquifyTransformStrategy(const KisCoordinatesConverter *converter, ToolTransformArgs ¤tArgs, TransformTransactionProperties &transaction, const KoCanvasResourceProvider *manager) : m_d(new Private(this, converter, currentArgs, transaction, manager)) { } KisLiquifyTransformStrategy::~KisLiquifyTransformStrategy() { } QPainterPath KisLiquifyTransformStrategy::getCursorOutline() const { return m_d->helper.brushOutline(*m_d->currentArgs.liquifyProperties()); } void KisLiquifyTransformStrategy::setTransformFunction(const QPointF &mousePos, bool perspectiveModifierActive) { Q_UNUSED(mousePos); Q_UNUSED(perspectiveModifierActive); } QCursor KisLiquifyTransformStrategy::getCurrentCursor() const { return Qt::BlankCursor; } void KisLiquifyTransformStrategy::paint(QPainter &gc) { // Draw preview image if (m_d->recalculateOnNextRedraw) { m_d->recalculateTransformations(); m_d->recalculateOnNextRedraw = false; } gc.save(); gc.setOpacity(m_d->transaction.basePreviewOpacity()); gc.setTransform(m_d->paintingTransform, true); gc.drawImage(m_d->paintingOffset, m_d->transformedImage); gc.restore(); } void KisLiquifyTransformStrategy::externalConfigChanged() { if (!m_d->currentArgs.liquifyWorker()) return; m_d->recalculateTransformations(); } bool KisLiquifyTransformStrategy::acceptsClicks() const { return true; } bool KisLiquifyTransformStrategy::beginPrimaryAction(KoPointerEvent *event) { m_d->helper.configurePaintOp(*m_d->currentArgs.liquifyProperties(), m_d->currentArgs.liquifyWorker()); m_d->helper.startPaint(event, m_d->manager); m_d->recalculateTransformations(); return true; } void KisLiquifyTransformStrategy::continuePrimaryAction(KoPointerEvent *event) { m_d->helper.continuePaint(event); // the updates should be compressed m_d->recalculateOnNextRedraw = true; emit requestCanvasUpdate(); } bool KisLiquifyTransformStrategy::endPrimaryAction(KoPointerEvent *event) { if (m_d->helper.endPaint(event)) { m_d->recalculateTransformations(); emit requestCanvasUpdate(); } return true; } void KisLiquifyTransformStrategy::hoverActionCommon(KoPointerEvent *event) { m_d->helper.hoverPaint(event); } void KisLiquifyTransformStrategy::activateAlternateAction(KisTool::AlternateAction action) { if (action == KisTool::PickFgNode || action == KisTool::PickBgNode || action == KisTool::PickFgImage || action == KisTool::PickBgImage) { KisLiquifyProperties *props = m_d->currentArgs.liquifyProperties(); props->setReverseDirection(!props->reverseDirection()); emit requestUpdateOptionWidget(); } } void KisLiquifyTransformStrategy::deactivateAlternateAction(KisTool::AlternateAction action) { if (action == KisTool::PickFgNode || action == KisTool::PickBgNode || action == KisTool::PickFgImage || action == KisTool::PickBgImage) { KisLiquifyProperties *props = m_d->currentArgs.liquifyProperties(); props->setReverseDirection(!props->reverseDirection()); emit requestUpdateOptionWidget(); } } bool KisLiquifyTransformStrategy::beginAlternateAction(KoPointerEvent *event, KisTool::AlternateAction action) { if (action == KisTool::ChangeSize) { QPointF widgetPoint = m_d->converter->documentToWidget(event->point); m_d->lastMouseWidgetPos = widgetPoint; m_d->startResizeImagePos = m_d->converter->documentToImage(event->point); m_d->startResizeGlobalCursorPos = QCursor::pos(); return true; } else if (action == KisTool::PickFgNode || action == KisTool::PickBgNode || action == KisTool::PickFgImage || action == KisTool::PickBgImage) { return beginPrimaryAction(event); } return false; } void KisLiquifyTransformStrategy::continueAlternateAction(KoPointerEvent *event, KisTool::AlternateAction action) { if (action == KisTool::ChangeSize) { QPointF widgetPoint = m_d->converter->documentToWidget(event->point); QPointF diff = widgetPoint - m_d->lastMouseWidgetPos; KisLiquifyProperties *props = m_d->currentArgs.liquifyProperties(); const qreal linearizedOffset = diff.x() / KisTransformUtils::scaleFromAffineMatrix(m_d->converter->imageToWidgetTransform()); const qreal newSize = qBound(props->minSize(), props->size() + linearizedOffset, props->maxSize()); props->setSize(newSize); m_d->currentArgs.saveLiquifyTransformMode(); m_d->lastMouseWidgetPos = widgetPoint; emit requestCursorOutlineUpdate(m_d->startResizeImagePos); } else if (action == KisTool::PickFgNode || action == KisTool::PickBgNode || action == KisTool::PickFgImage || action == KisTool::PickBgImage) { return continuePrimaryAction(event); } } bool KisLiquifyTransformStrategy::endAlternateAction(KoPointerEvent *event, KisTool::AlternateAction action) { Q_UNUSED(event); if (action == KisTool::ChangeSize) { QCursor::setPos(m_d->startResizeGlobalCursorPos); return true; } else if (action == KisTool::PickFgNode || action == KisTool::PickBgNode || action == KisTool::PickFgImage || action == KisTool::PickBgImage) { return endPrimaryAction(event); } return false; } inline QPointF KisLiquifyTransformStrategy::Private::imageToThumb(const QPointF &pt, bool useFlakeOptimization) { return useFlakeOptimization ? converter->imageToDocument(converter->documentToFlake((pt))) : q->thumbToImageTransform().inverted().map(pt); } void KisLiquifyTransformStrategy::Private::recalculateTransformations() { KIS_ASSERT_RECOVER_RETURN(currentArgs.liquifyWorker()); QTransform scaleTransform = KisTransformUtils::imageToFlakeTransform(converter); QTransform resultThumbTransform = q->thumbToImageTransform() * scaleTransform; qreal scale = KisTransformUtils::scaleFromAffineMatrix(resultThumbTransform); bool useFlakeOptimization = scale < 1.0 && !KisTransformUtils::thumbnailTooSmall(resultThumbTransform, q->originalImage().rect()); paintingOffset = transaction.originalTopLeft(); if (!q->originalImage().isNull()) { if (useFlakeOptimization) { transformedImage = q->originalImage().transformed(resultThumbTransform); paintingTransform = QTransform(); } else { transformedImage = q->originalImage(); paintingTransform = resultThumbTransform; } QTransform imageToRealThumbTransform = useFlakeOptimization ? scaleTransform : q->thumbToImageTransform().inverted(); QPointF origTLInFlake = imageToRealThumbTransform.map(transaction.originalTopLeft()); transformedImage = currentArgs.liquifyWorker()->runOnQImage(transformedImage, origTLInFlake, imageToRealThumbTransform, &paintingOffset); } else { transformedImage = q->originalImage(); paintingOffset = imageToThumb(transaction.originalTopLeft(), false); paintingTransform = resultThumbTransform; } handlesTransform = scaleTransform; } diff --git a/plugins/tools/tool_transform2/kis_perspective_transform_strategy.cpp b/plugins/tools/tool_transform2/kis_perspective_transform_strategy.cpp index 0cff5ed2a6..b74d986c0b 100644 --- a/plugins/tools/tool_transform2/kis_perspective_transform_strategy.cpp +++ b/plugins/tools/tool_transform2/kis_perspective_transform_strategy.cpp @@ -1,692 +1,693 @@ /* * 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_perspective_transform_strategy.h" #include #include +#include #include #include #include #include "kis_coordinates_converter.h" #include "tool_transform_args.h" #include "transform_transaction_properties.h" #include "krita_utils.h" #include "kis_cursor.h" #include "kis_transform_utils.h" #include "kis_free_transform_strategy_gsl_helpers.h" enum StrokeFunction { DRAG_HANDLE = 0, DRAG_X_VANISHING_POINT, DRAG_Y_VANISHING_POINT, MOVE, NONE }; struct KisPerspectiveTransformStrategy::Private { Private(KisPerspectiveTransformStrategy *_q, const KisCoordinatesConverter *_converter, ToolTransformArgs &_currentArgs, TransformTransactionProperties &_transaction) : q(_q), converter(_converter), currentArgs(_currentArgs), transaction(_transaction), imageTooBig(false), isTransforming(false) { } KisPerspectiveTransformStrategy *q; /// standard members /// const KisCoordinatesConverter *converter; ////// ToolTransformArgs ¤tArgs; ////// TransformTransactionProperties &transaction; QTransform thumbToImageTransform; QImage originalImage; QTransform paintingTransform; QPointF paintingOffset; QTransform handlesTransform; /// custom members /// StrokeFunction function; struct HandlePoints { bool xVanishingExists; bool yVanishingExists; QPointF xVanishing; QPointF yVanishing; }; HandlePoints transformedHandles; QTransform transform; QVector srcCornerPoints; QVector dstCornerPoints; int currentDraggingCornerPoint; bool imageTooBig; QPointF clickPos; ToolTransformArgs clickArgs; bool isTransforming; QCursor getScaleCursor(const QPointF &handlePt); QCursor getShearCursor(const QPointF &start, const QPointF &end); void recalculateTransformations(); void recalculateTransformedHandles(); void transformIntoArgs(const Eigen::Matrix3f &t); QTransform transformFromArgs(); }; KisPerspectiveTransformStrategy::KisPerspectiveTransformStrategy(const KisCoordinatesConverter *converter, KoSnapGuide *snapGuide, ToolTransformArgs ¤tArgs, TransformTransactionProperties &transaction) : KisSimplifiedActionPolicyStrategy(converter, snapGuide), m_d(new Private(this, converter, currentArgs, transaction)) { } KisPerspectiveTransformStrategy::~KisPerspectiveTransformStrategy() { } void KisPerspectiveTransformStrategy::Private::recalculateTransformedHandles() { srcCornerPoints.clear(); srcCornerPoints << transaction.originalTopLeft(); srcCornerPoints << transaction.originalTopRight(); srcCornerPoints << transaction.originalBottomLeft(); srcCornerPoints << transaction.originalBottomRight(); dstCornerPoints.clear(); Q_FOREACH (const QPointF &pt, srcCornerPoints) { dstCornerPoints << transform.map(pt); } QMatrix4x4 realMatrix(transform); QVector4D v; v = QVector4D(1, 0, 0, 0); v = realMatrix * v; transformedHandles.xVanishingExists = !qFuzzyCompare(v.w(), 0); transformedHandles.xVanishing = v.toVector2DAffine().toPointF(); v = QVector4D(0, 1, 0, 0); v = realMatrix * v; transformedHandles.yVanishingExists = !qFuzzyCompare(v.w(), 0); transformedHandles.yVanishing = v.toVector2DAffine().toPointF(); } void KisPerspectiveTransformStrategy::setTransformFunction(const QPointF &mousePos, bool perspectiveModifierActive) { Q_UNUSED(perspectiveModifierActive); QPolygonF transformedPolygon = m_d->transform.map(QPolygonF(m_d->transaction.originalRect())); StrokeFunction defaultFunction = transformedPolygon.containsPoint(mousePos, Qt::OddEvenFill) ? MOVE : NONE; KisTransformUtils::HandleChooser handleChooser(mousePos, defaultFunction); qreal handleRadius = KisTransformUtils::effectiveHandleGrabRadius(m_d->converter); if (!m_d->transformedHandles.xVanishing.isNull()) { handleChooser.addFunction(m_d->transformedHandles.xVanishing, handleRadius, DRAG_X_VANISHING_POINT); } if (!m_d->transformedHandles.yVanishing.isNull()) { handleChooser.addFunction(m_d->transformedHandles.yVanishing, handleRadius, DRAG_Y_VANISHING_POINT); } m_d->currentDraggingCornerPoint = -1; for (int i = 0; i < m_d->dstCornerPoints.size(); i++) { if (handleChooser.addFunction(m_d->dstCornerPoints[i], handleRadius, DRAG_HANDLE)) { m_d->currentDraggingCornerPoint = i; } } m_d->function = handleChooser.function(); } QCursor KisPerspectiveTransformStrategy::getCurrentCursor() const { QCursor cursor; switch (m_d->function) { case NONE: cursor = KisCursor::arrowCursor(); break; case MOVE: cursor = KisCursor::moveCursor(); break; case DRAG_HANDLE: case DRAG_X_VANISHING_POINT: case DRAG_Y_VANISHING_POINT: cursor = KisCursor::pointingHandCursor(); break; } return cursor; } void KisPerspectiveTransformStrategy::paint(QPainter &gc) { gc.save(); gc.setOpacity(m_d->transaction.basePreviewOpacity()); gc.setTransform(m_d->paintingTransform, true); gc.drawImage(m_d->paintingOffset, originalImage()); gc.restore(); // Draw Handles QPainterPath handles; handles.moveTo(m_d->transaction.originalTopLeft()); handles.lineTo(m_d->transaction.originalTopRight()); handles.lineTo(m_d->transaction.originalBottomRight()); handles.lineTo(m_d->transaction.originalBottomLeft()); handles.lineTo(m_d->transaction.originalTopLeft()); auto addHandleRectFunc = [&](const QPointF &pt) { handles.addRect( KisTransformUtils::handleRect(KisTransformUtils::handleVisualRadius, m_d->handlesTransform, m_d->transaction.originalRect(), pt) .translated(pt)); }; addHandleRectFunc(m_d->transaction.originalTopLeft()); addHandleRectFunc(m_d->transaction.originalTopRight()); addHandleRectFunc(m_d->transaction.originalBottomLeft()); addHandleRectFunc(m_d->transaction.originalBottomRight()); gc.save(); if (m_d->isTransforming) { gc.setOpacity(0.1); } /** * WARNING: we cannot install a transform to paint the handles here! * * There is a bug in Qt that prevents painting of cosmetic-pen * brushes in openGL mode when a TxProject matrix is active on * a QPainter. So just convert it manually. * * https://bugreports.qt-project.org/browse/QTBUG-42658 */ //gc.setTransform(m_d->handlesTransform, true); <-- don't do like this! QPainterPath mappedHandles = m_d->handlesTransform.map(handles); QPen pen[2]; pen[0].setWidth(1); pen[1].setWidth(2); pen[1].setColor(Qt::lightGray); for (int i = 1; i >= 0; --i) { gc.setPen(pen[i]); gc.drawPath(mappedHandles); } gc.restore(); { // painting perspective handles QPainterPath perspectiveHandles; QRectF handleRect = KisTransformUtils::handleRect(KisTransformUtils::handleVisualRadius, QTransform(), m_d->transaction.originalRect(), 0, 0); if (m_d->transformedHandles.xVanishingExists) { QRectF rc = handleRect.translated(m_d->transformedHandles.xVanishing); perspectiveHandles.addEllipse(rc); } if (m_d->transformedHandles.yVanishingExists) { QRectF rc = handleRect.translated(m_d->transformedHandles.yVanishing); perspectiveHandles.addEllipse(rc); } if (!perspectiveHandles.isEmpty()) { gc.save(); gc.setTransform(m_d->converter->imageToWidgetTransform()); gc.setBrush(Qt::red); for (int i = 1; i >= 0; --i) { gc.setPen(pen[i]); gc.drawPath(perspectiveHandles); } gc.restore(); } } } void KisPerspectiveTransformStrategy::externalConfigChanged() { m_d->recalculateTransformations(); } bool KisPerspectiveTransformStrategy::beginPrimaryAction(const QPointF &pt) { Q_UNUSED(pt); if (m_d->function == NONE) return false; m_d->clickPos = pt; m_d->clickArgs = m_d->currentArgs; return true; } Eigen::Matrix3f getTransitionMatrix(const QVector &sp) { Eigen::Matrix3f A; Eigen::Vector3f v3; A << sp[0].x() , sp[1].x() , sp[2].x() ,sp[0].y() , sp[1].y() , sp[2].y() , 1 , 1 , 1; v3 << sp[3].x() , sp[3].y() , 1; Eigen::Vector3f coeffs = A.colPivHouseholderQr().solve(v3); A.col(0) *= coeffs(0); A.col(1) *= coeffs(1); A.col(2) *= coeffs(2); return A; } QTransform toQTransform(const Eigen::Matrix3f &m) { return QTransform(m(0,0), m(1,0), m(2,0), m(0,1), m(1,1), m(2,1), m(0,2), m(1,2), m(2,2)); } Eigen::Matrix3f fromQTransform(const QTransform &t) { Eigen::Matrix3f m; m << t.m11() , t.m21() , t.m31() ,t.m12() , t.m22() , t.m32() ,t.m13() , t.m23() , t.m33(); return m; } Eigen::Matrix3f fromTranslate(const QPointF &pt) { Eigen::Matrix3f m; m << 1 , 0 , pt.x() ,0 , 1 , pt.y() ,0 , 0 , 1; return m; } Eigen::Matrix3f fromScale(qreal sx, qreal sy) { Eigen::Matrix3f m; m << sx , 0 , 0 ,0 , sy , 0 ,0 , 0 , 1; return m; } Eigen::Matrix3f fromShear(qreal sx, qreal sy) { Eigen::Matrix3f m; m << 1 , sx , 0 ,sy , sx*sy + 1, 0 ,0 , 0 , 1; return m; } void KisPerspectiveTransformStrategy::Private::transformIntoArgs(const Eigen::Matrix3f &t) { Eigen::Matrix3f TS = fromTranslate(-currentArgs.originalCenter()); Eigen::Matrix3f m = t * TS.inverse(); qreal tX = m(0,2) / m(2,2); qreal tY = m(1,2) / m(2,2); Eigen::Matrix3f T = fromTranslate(QPointF(tX, tY)); m = T.inverse() * m; // TODO: implement matrix decomposition as described here // https://www.w3.org/TR/css-transforms-1/#decomposing-a-3d-matrix // For now use an extremely hackish approximation if (m(0,1) != 0.0 && m(0,0) != 0.0 && m(2,2) != 0.0) { const qreal factor = (m(1,1) / m(0,1) - m(1,0) / m(0,0)); qreal scaleX = m(0,0) / m(2,2); qreal scaleY = m(0,1) / m(2,2) * factor; Eigen::Matrix3f SC = fromScale(scaleX, scaleY); qreal shearX = 1.0 / factor; qreal shearY = m(1,0) / m(0,0); Eigen::Matrix3f S = fromShear(shearX, shearY); currentArgs.setScaleX(scaleX); currentArgs.setScaleY(scaleY); currentArgs.setShearX(shearX); currentArgs.setShearY(shearY); m = m * SC.inverse(); m = m * S.inverse(); m /= m(2,2); } else { currentArgs.setScaleX(1.0); currentArgs.setScaleY(1.0); currentArgs.setShearX(0.0); currentArgs.setShearY(0.0); } currentArgs.setTransformedCenter(QPointF(tX, tY)); currentArgs.setFlattenedPerspectiveTransform(toQTransform(m)); } QTransform KisPerspectiveTransformStrategy::Private::transformFromArgs() { KisTransformUtils::MatricesPack m(currentArgs); return m.finalTransform(); } QVector4D fromQPointF(const QPointF &pt) { return QVector4D(pt.x(), pt.y(), 0, 1.0); } QPointF toQPointF(const QVector4D &v) { return v.toVector2DAffine().toPointF(); } void KisPerspectiveTransformStrategy::continuePrimaryAction(const QPointF &mousePos, bool shiftModifierActve, bool altModifierActive) { Q_UNUSED(shiftModifierActve); Q_UNUSED(altModifierActive); m_d->isTransforming = true; switch (m_d->function) { case NONE: break; case MOVE: { QPointF diff = mousePos - m_d->clickPos; m_d->currentArgs.setTransformedCenter( m_d->clickArgs.transformedCenter() + diff); break; } case DRAG_HANDLE: { KIS_ASSERT_RECOVER_RETURN(m_d->currentDraggingCornerPoint >=0); m_d->dstCornerPoints[m_d->currentDraggingCornerPoint] = mousePos; Eigen::Matrix3f A = getTransitionMatrix(m_d->srcCornerPoints); Eigen::Matrix3f B = getTransitionMatrix(m_d->dstCornerPoints); Eigen::Matrix3f result = B * A.inverse(); m_d->transformIntoArgs(result); break; } case DRAG_X_VANISHING_POINT: case DRAG_Y_VANISHING_POINT: { QMatrix4x4 m(m_d->transform); QPointF tl = m_d->transaction.originalTopLeft(); QPointF tr = m_d->transaction.originalTopRight(); QPointF bl = m_d->transaction.originalBottomLeft(); QPointF br = m_d->transaction.originalBottomRight(); QVector4D v(1,0,0,0); QVector4D otherV(0,1,0,0); if (m_d->function == DRAG_X_VANISHING_POINT) { v = QVector4D(1,0,0,0); otherV = QVector4D(0,1,0,0); } else { v = QVector4D(0,1,0,0); otherV = QVector4D(1,0,0,0); } QPointF tl_dst = toQPointF(m * fromQPointF(tl)); QPointF tr_dst = toQPointF(m * fromQPointF(tr)); QPointF bl_dst = toQPointF(m * fromQPointF(bl)); QPointF br_dst = toQPointF(m * fromQPointF(br)); QPointF v_dst = toQPointF(m * v); QPointF otherV_dst = toQPointF(m * otherV); QVector srcPoints; QVector dstPoints; QPointF far1_src; QPointF far2_src; QPointF near1_src; QPointF near2_src; QPointF far1_dst; QPointF far2_dst; QPointF near1_dst; QPointF near2_dst; if (m_d->function == DRAG_X_VANISHING_POINT) { // topLeft (far) --- topRight (near) --- vanishing if (kisSquareDistance(v_dst, tl_dst) > kisSquareDistance(v_dst, tr_dst)) { far1_src = tl; far2_src = bl; near1_src = tr; near2_src = br; far1_dst = tl_dst; far2_dst = bl_dst; near1_dst = tr_dst; near2_dst = br_dst; // topRight (far) --- topLeft (near) --- vanishing } else { far1_src = tr; far2_src = br; near1_src = tl; near2_src = bl; far1_dst = tr_dst; far2_dst = br_dst; near1_dst = tl_dst; near2_dst = bl_dst; } } else /* if (m_d->function == DRAG_Y_VANISHING_POINT) */{ // topLeft (far) --- bottomLeft (near) --- vanishing if (kisSquareDistance(v_dst, tl_dst) > kisSquareDistance(v_dst, bl_dst)) { far1_src = tl; far2_src = tr; near1_src = bl; near2_src = br; far1_dst = tl_dst; far2_dst = tr_dst; near1_dst = bl_dst; near2_dst = br_dst; // bottomLeft (far) --- topLeft (near) --- vanishing } else { far1_src = bl; far2_src = br; near1_src = tl; near2_src = tr; far1_dst = bl_dst; far2_dst = br_dst; near1_dst = tl_dst; near2_dst = tr_dst; } } QLineF l0(far1_dst, mousePos); QLineF l1(far2_dst, mousePos); QLineF l2(otherV_dst, near1_dst); l0.intersect(l2, &near1_dst); l1.intersect(l2, &near2_dst); srcPoints << far1_src; srcPoints << far2_src; srcPoints << near1_src; srcPoints << near2_src; dstPoints << far1_dst; dstPoints << far2_dst; dstPoints << near1_dst; dstPoints << near2_dst; Eigen::Matrix3f A = getTransitionMatrix(srcPoints); Eigen::Matrix3f B = getTransitionMatrix(dstPoints); Eigen::Matrix3f result = B * A.inverse(); m_d->transformIntoArgs(result); break; } } m_d->recalculateTransformations(); } bool KisPerspectiveTransformStrategy::endPrimaryAction() { bool shouldSave = !m_d->imageTooBig; m_d->isTransforming = false; if (m_d->imageTooBig) { m_d->currentArgs = m_d->clickArgs; m_d->recalculateTransformations(); } return shouldSave; } void KisPerspectiveTransformStrategy::Private::recalculateTransformations() { transform = transformFromArgs(); QTransform viewScaleTransform = converter->imageToDocumentTransform() * converter->documentToFlakeTransform(); handlesTransform = transform * viewScaleTransform; QTransform tl = QTransform::fromTranslate(transaction.originalTopLeft().x(), transaction.originalTopLeft().y()); paintingTransform = tl.inverted() * q->thumbToImageTransform() * tl * transform * viewScaleTransform; paintingOffset = transaction.originalTopLeft(); // check whether image is too big to be displayed or not const qreal maxScale = 20.0; imageTooBig = false; if (qAbs(currentArgs.scaleX()) > maxScale || qAbs(currentArgs.scaleY()) > maxScale) { imageTooBig = true; } else { QVector points; points << transaction.originalRect().topLeft(); points << transaction.originalRect().topRight(); points << transaction.originalRect().bottomRight(); points << transaction.originalRect().bottomLeft(); for (int i = 0; i < points.size(); i++) { points[i] = transform.map(points[i]); } for (int i = 0; i < points.size(); i++) { const QPointF &pt = points[i]; const QPointF &prev = points[(i - 1 + 4) % 4]; const QPointF &next = points[(i + 1) % 4]; const QPointF &other = points[(i + 2) % 4]; QLineF l1(pt, other); QLineF l2(prev, next); QPointF intersection; l1.intersect(l2, &intersection); qreal maxDistance = kisSquareDistance(pt, other); if (kisSquareDistance(pt, intersection) > maxDistance || kisSquareDistance(other, intersection) > maxDistance) { imageTooBig = true; break; } const qreal thresholdDistance = 0.02 * l2.length(); if (kisDistanceToLine(pt, l2) < thresholdDistance) { imageTooBig = true; break; } } } // recalculate cached handles position recalculateTransformedHandles(); emit q->requestShowImageTooBig(imageTooBig); } diff --git a/plugins/tools/tool_transform2/kis_transform_strategy_base.cpp b/plugins/tools/tool_transform2/kis_transform_strategy_base.cpp index 1e7603ef39..0671ed5c9f 100644 --- a/plugins/tools/tool_transform2/kis_transform_strategy_base.cpp +++ b/plugins/tools/tool_transform2/kis_transform_strategy_base.cpp @@ -1,104 +1,105 @@ /* * 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_transform_strategy_base.h" #include +#include #include #include "KoPointerEvent.h" struct KisTransformStrategyBase::Private { QTransform thumbToImageTransform; QImage originalImage; }; KisTransformStrategyBase::KisTransformStrategyBase() : m_d(new Private()) { } KisTransformStrategyBase::~KisTransformStrategyBase() { } QPainterPath KisTransformStrategyBase::getCursorOutline() const { return QPainterPath(); } void KisTransformStrategyBase::activatePrimaryAction() { } void KisTransformStrategyBase::deactivatePrimaryAction() { } QImage KisTransformStrategyBase::originalImage() const { return m_d->originalImage; } QTransform KisTransformStrategyBase::thumbToImageTransform() const { return m_d->thumbToImageTransform; } void KisTransformStrategyBase::setThumbnailImage(const QImage &image, QTransform thumbToImageTransform) { m_d->originalImage = image; m_d->thumbToImageTransform = thumbToImageTransform; } bool KisTransformStrategyBase::acceptsClicks() const { return false; } void KisTransformStrategyBase::activateAlternateAction(KisTool::AlternateAction action) { Q_UNUSED(action); } void KisTransformStrategyBase::deactivateAlternateAction(KisTool::AlternateAction action) { Q_UNUSED(action); } bool KisTransformStrategyBase::beginAlternateAction(KoPointerEvent *event, KisTool::AlternateAction action) { Q_UNUSED(event); Q_UNUSED(action); return false; } void KisTransformStrategyBase::continueAlternateAction(KoPointerEvent *event, KisTool::AlternateAction action) { Q_UNUSED(event); Q_UNUSED(action); } bool KisTransformStrategyBase::endAlternateAction(KoPointerEvent *event, KisTool::AlternateAction action) { Q_UNUSED(event); Q_UNUSED(action); return false; } diff --git a/plugins/tools/tool_transform2/kis_transform_utils.cpp b/plugins/tools/tool_transform2/kis_transform_utils.cpp index 1fbbd525be..c7792f393c 100644 --- a/plugins/tools/tool_transform2/kis_transform_utils.cpp +++ b/plugins/tools/tool_transform2/kis_transform_utils.cpp @@ -1,490 +1,491 @@ /* * 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_transform_utils.h" #include +#include #include #include #include "tool_transform_args.h" #include "kis_paint_device.h" #include "kis_algebra_2d.h" #include "transform_transaction_properties.h" struct TransformTransactionPropertiesRegistrar { TransformTransactionPropertiesRegistrar() { qRegisterMetaType("TransformTransactionProperties"); } }; static TransformTransactionPropertiesRegistrar __registrar1; struct ToolTransformArgsRegistrar { ToolTransformArgsRegistrar() { qRegisterMetaType("ToolTransformArgs"); } }; static ToolTransformArgsRegistrar __registrar2; struct QPainterPathRegistrar { QPainterPathRegistrar() { qRegisterMetaType("QPainterPath"); } }; static QPainterPathRegistrar __registrar3; const int KisTransformUtils::rotationHandleVisualRadius = 12; const int KisTransformUtils::rotationHandleRadius = 8; const int KisTransformUtils::handleVisualRadius = 12; const int KisTransformUtils::handleRadius = 8; QTransform KisTransformUtils::imageToFlakeTransform(const KisCoordinatesConverter *converter) { return converter->imageToDocumentTransform() * converter->documentToFlakeTransform(); } qreal KisTransformUtils::effectiveHandleGrabRadius(const KisCoordinatesConverter *converter) { QPointF handleRadiusPt = flakeToImage(converter, QPointF(handleRadius, handleRadius)); return (handleRadiusPt.x() > handleRadiusPt.y()) ? handleRadiusPt.x() : handleRadiusPt.y(); } qreal KisTransformUtils::effectiveRotationHandleGrabRadius(const KisCoordinatesConverter *converter) { QPointF handleRadiusPt = flakeToImage(converter, QPointF(rotationHandleRadius, rotationHandleRadius)); return (handleRadiusPt.x() > handleRadiusPt.y()) ? handleRadiusPt.x() : handleRadiusPt.y(); } qreal KisTransformUtils::scaleFromAffineMatrix(const QTransform &t) { return KoUnit::approxTransformScale(t); } qreal KisTransformUtils::scaleFromPerspectiveMatrixX(const QTransform &t, const QPointF &basePt) { const QPointF pt = basePt + QPointF(1.0, 0); return kisDistance(t.map(pt), t.map(basePt)); } qreal KisTransformUtils::scaleFromPerspectiveMatrixY(const QTransform &t, const QPointF &basePt) { const QPointF pt = basePt + QPointF(0, 1.0); return kisDistance(t.map(pt), t.map(basePt)); } qreal KisTransformUtils::effectiveSize(const QRectF &rc) { return 0.5 * (rc.width() + rc.height()); } bool KisTransformUtils::thumbnailTooSmall(const QTransform &resultThumbTransform, const QRect &originalImageRect) { return KisAlgebra2D::minDimension(resultThumbTransform.mapRect(originalImageRect)) < 32; } QRectF handleRectImpl(qreal radius, const QTransform &t, const QRectF &limitingRect, const QPointF &basePoint, qreal *dOutX, qreal *dOutY) { const qreal handlesExtraScaleX = KisTransformUtils::scaleFromPerspectiveMatrixX(t, basePoint); const qreal handlesExtraScaleY = KisTransformUtils::scaleFromPerspectiveMatrixY(t, basePoint); const qreal maxD = 0.2 * KisTransformUtils::effectiveSize(limitingRect); const qreal dX = qMin(maxD, radius / handlesExtraScaleX); const qreal dY = qMin(maxD, radius / handlesExtraScaleY); QRectF handleRect(-0.5 * dX, -0.5 * dY, dX, dY); if (dOutX) { *dOutX = dX; } if (dOutY) { *dOutY = dY; } return handleRect; } QRectF KisTransformUtils::handleRect(qreal radius, const QTransform &t, const QRectF &limitingRect, qreal *dOutX, qreal *dOutY) { return handleRectImpl(radius, t, limitingRect, limitingRect.center(), dOutX, dOutY); } QRectF KisTransformUtils::handleRect(qreal radius, const QTransform &t, const QRectF &limitingRect, const QPointF &basePoint) { return handleRectImpl(radius, t, limitingRect, basePoint, 0, 0); } QPointF KisTransformUtils::clipInRect(QPointF p, QRectF r) { QPointF center = r.center(); QPointF t = p - center; r.translate(- center); if (t.y() != 0) { if (t.x() != 0) { double slope = t.y() / t.x(); if (t.x() < r.left()) { t.setY(r.left() * slope); t.setX(r.left()); } else if (t.x() > r.right()) { t.setY(r.right() * slope); t.setX(r.right()); } if (t.y() < r.top()) { t.setX(r.top() / slope); t.setY(r.top()); } else if (t.y() > r.bottom()) { t.setX(r.bottom() / slope); t.setY(r.bottom()); } } else { if (t.y() < r.top()) t.setY(r.top()); else if (t.y() > r.bottom()) t.setY(r.bottom()); } } else { if (t.x() < r.left()) t.setX(r.left()); else if (t.x() > r.right()) t.setX(r.right()); } t += center; return t; } KisTransformUtils::MatricesPack::MatricesPack(const ToolTransformArgs &args) { TS = QTransform::fromTranslate(-args.originalCenter().x(), -args.originalCenter().y()); SC = QTransform::fromScale(args.scaleX(), args.scaleY()); S.shear(0, args.shearY()); S.shear(args.shearX(), 0); if (args.mode() == ToolTransformArgs::FREE_TRANSFORM) { P.rotate(180. * args.aX() / M_PI, QVector3D(1, 0, 0)); P.rotate(180. * args.aY() / M_PI, QVector3D(0, 1, 0)); P.rotate(180. * args.aZ() / M_PI, QVector3D(0, 0, 1)); projectedP = P.toTransform(args.cameraPos().z()); } else if (args.mode() == ToolTransformArgs::PERSPECTIVE_4POINT) { projectedP = args.flattenedPerspectiveTransform(); P = QMatrix4x4(projectedP); } QPointF translation = args.transformedCenter(); T = QTransform::fromTranslate(translation.x(), translation.y()); } QTransform KisTransformUtils::MatricesPack::finalTransform() const { return TS * SC * S * projectedP * T; } bool KisTransformUtils::checkImageTooBig(const QRectF &bounds, const MatricesPack &m) { bool imageTooBig = false; QMatrix4x4 unprojectedMatrix = QMatrix4x4(m.T) * m.P * QMatrix4x4(m.TS * m.SC * m.S); QVector points; points << bounds.topLeft(); points << bounds.topRight(); points << bounds.bottomRight(); points << bounds.bottomLeft(); Q_FOREACH (const QPointF &pt, points) { QVector4D v(pt.x(), pt.y(), 0, 1); v = unprojectedMatrix * v; qreal z = v.z() / v.w(); imageTooBig = z > 1024.0; if (imageTooBig) { break; } } return imageTooBig; } #include #include #include #include #include KisTransformWorker KisTransformUtils::createTransformWorker(const ToolTransformArgs &config, KisPaintDeviceSP device, KoUpdaterPtr updater, QVector3D *transformedCenter /* OUT */) { { KisTransformWorker t(0, config.scaleX(), config.scaleY(), config.shearX(), config.shearY(), config.originalCenter().x(), config.originalCenter().y(), config.aZ(), 0, // set X and Y translation 0, // to null for calculation 0, config.filter()); *transformedCenter = QVector3D(t.transform().map(config.originalCenter())); } QPointF translation = config.transformedCenter() - (*transformedCenter).toPointF(); KisTransformWorker transformWorker(device, config.scaleX(), config.scaleY(), config.shearX(), config.shearY(), config.originalCenter().x(), config.originalCenter().y(), config.aZ(), (int)(translation.x()), (int)(translation.y()), updater, config.filter()); return transformWorker; } void KisTransformUtils::transformDevice(const ToolTransformArgs &config, KisPaintDeviceSP device, KisProcessingVisitor::ProgressHelper *helper) { if (config.mode() == ToolTransformArgs::WARP) { KoUpdaterPtr updater = helper->updater(); KisWarpTransformWorker worker(config.warpType(), device, config.origPoints(), config.transfPoints(), config.alpha(), updater); worker.run(); } else if (config.mode() == ToolTransformArgs::CAGE) { KoUpdaterPtr updater = helper->updater(); KisCageTransformWorker worker(device, config.origPoints(), updater, config.pixelPrecision()); worker.prepareTransform(); worker.setTransformedCage(config.transfPoints()); worker.run(); } else if (config.mode() == ToolTransformArgs::LIQUIFY) { KoUpdaterPtr updater = helper->updater(); //FIXME: Q_UNUSED(updater); config.liquifyWorker()->run(device); } else { QVector3D transformedCenter; KoUpdaterPtr updater1 = helper->updater(); KoUpdaterPtr updater2 = helper->updater(); KisTransformWorker transformWorker = createTransformWorker(config, device, updater1, &transformedCenter); transformWorker.run(); if (config.mode() == ToolTransformArgs::FREE_TRANSFORM) { KisPerspectiveTransformWorker perspectiveWorker(device, config.transformedCenter(), config.aX(), config.aY(), config.cameraPos().z(), updater2); perspectiveWorker.run(); } else if (config.mode() == ToolTransformArgs::PERSPECTIVE_4POINT) { QTransform T = QTransform::fromTranslate(config.transformedCenter().x(), config.transformedCenter().y()); KisPerspectiveTransformWorker perspectiveWorker(device, T.inverted() * config.flattenedPerspectiveTransform() * T, updater2); perspectiveWorker.run(); } } } QRect KisTransformUtils::needRect(const ToolTransformArgs &config, const QRect &rc, const QRect &srcBounds) { QRect result = rc; if (config.mode() == ToolTransformArgs::WARP) { KisWarpTransformWorker worker(config.warpType(), 0, config.origPoints(), config.transfPoints(), config.alpha(), 0); result = worker.approxNeedRect(rc, srcBounds); } else if (config.mode() == ToolTransformArgs::CAGE) { KisCageTransformWorker worker(0, config.origPoints(), 0, config.pixelPrecision()); worker.setTransformedCage(config.transfPoints()); result = worker.approxNeedRect(rc, srcBounds); } else if (config.mode() == ToolTransformArgs::LIQUIFY) { result = config.liquifyWorker() ? config.liquifyWorker()->approxNeedRect(rc, srcBounds) : rc; } else { KIS_ASSERT_RECOVER_NOOP(0 && "this works for non-affine transformations only!"); } return result; } QRect KisTransformUtils::changeRect(const ToolTransformArgs &config, const QRect &rc) { QRect result = rc; if (config.mode() == ToolTransformArgs::WARP) { KisWarpTransformWorker worker(config.warpType(), 0, config.origPoints(), config.transfPoints(), config.alpha(), 0); result = worker.approxChangeRect(rc); } else if (config.mode() == ToolTransformArgs::CAGE) { KisCageTransformWorker worker(0, config.origPoints(), 0, config.pixelPrecision()); worker.setTransformedCage(config.transfPoints()); result = worker.approxChangeRect(rc); } else if (config.mode() == ToolTransformArgs::LIQUIFY) { result = config.liquifyWorker() ? config.liquifyWorker()->approxChangeRect(rc) : rc; } else { KIS_ASSERT_RECOVER_NOOP(0 && "this works for non-affine transformations only!"); } return result; } KisTransformUtils::AnchorHolder::AnchorHolder(bool enabled, ToolTransformArgs *config) : m_enabled(enabled), m_config(config) { if (!m_enabled) return; m_staticPoint = m_config->originalCenter() + m_config->rotationCenterOffset(); const KisTransformUtils::MatricesPack m(*m_config); m_oldStaticPointInView = m.finalTransform().map(m_staticPoint); } KisTransformUtils::AnchorHolder::~AnchorHolder() { if (!m_enabled) return; const KisTransformUtils::MatricesPack m(*m_config); const QPointF newStaticPointInView = m.finalTransform().map(m_staticPoint); const QPointF diff = m_oldStaticPointInView - newStaticPointInView; m_config->setTransformedCenter(m_config->transformedCenter() + diff); } void KisTransformUtils::setDefaultWarpPoints(int pointsPerLine, const TransformTransactionProperties *transaction, ToolTransformArgs *config) { static const int DEFAULT_POINTS_PER_LINE = 3; if (pointsPerLine < 0) { pointsPerLine = DEFAULT_POINTS_PER_LINE; } int nbPoints = pointsPerLine * pointsPerLine; QVector origPoints(nbPoints); QVector transfPoints(nbPoints); qreal gridSpaceX, gridSpaceY; if (nbPoints == 1) { //there is actually no grid origPoints[0] = transaction->originalCenterGeometric(); transfPoints[0] = transaction->originalCenterGeometric(); } else if (nbPoints > 1) { gridSpaceX = transaction->originalRect().width() / (pointsPerLine - 1); gridSpaceY = transaction->originalRect().height() / (pointsPerLine - 1); double y = transaction->originalRect().top(); for (int i = 0; i < pointsPerLine; ++i) { double x = transaction->originalRect().left(); for (int j = 0 ; j < pointsPerLine; ++j) { origPoints[i * pointsPerLine + j] = QPointF(x, y); transfPoints[i * pointsPerLine + j] = QPointF(x, y); x += gridSpaceX; } y += gridSpaceY; } } config->setDefaultPoints(nbPoints > 0); config->setPoints(origPoints, transfPoints); } ToolTransformArgs KisTransformUtils::resetArgsForMode(ToolTransformArgs::TransformMode mode, const QString &filterId, const TransformTransactionProperties &transaction) { ToolTransformArgs args; args.setOriginalCenter(transaction.originalCenterGeometric()); args.setTransformedCenter(transaction.originalCenterGeometric()); args.setFilterId(filterId); if (mode == ToolTransformArgs::FREE_TRANSFORM) { args.setMode(ToolTransformArgs::FREE_TRANSFORM); } else if (mode == ToolTransformArgs::WARP) { args.setMode(ToolTransformArgs::WARP); KisTransformUtils::setDefaultWarpPoints(-1, &transaction, &args); args.setEditingTransformPoints(false); } else if (mode == ToolTransformArgs::CAGE) { args.setMode(ToolTransformArgs::CAGE); args.setEditingTransformPoints(true); } else if (mode == ToolTransformArgs::LIQUIFY) { args.setMode(ToolTransformArgs::LIQUIFY); const QRect srcRect = transaction.originalRect().toAlignedRect(); if (!srcRect.isEmpty()) { args.initLiquifyTransformMode(transaction.originalRect().toAlignedRect()); } } else if (mode == ToolTransformArgs::PERSPECTIVE_4POINT) { args.setMode(ToolTransformArgs::PERSPECTIVE_4POINT); } return args; } diff --git a/plugins/tools/tool_transform2/kis_warp_transform_strategy.cpp b/plugins/tools/tool_transform2/kis_warp_transform_strategy.cpp index fdad9f32b2..9a2dd4daca 100644 --- a/plugins/tools/tool_transform2/kis_warp_transform_strategy.cpp +++ b/plugins/tools/tool_transform2/kis_warp_transform_strategy.cpp @@ -1,647 +1,648 @@ /* * 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_warp_transform_strategy.h" #include #include #include +#include #include "kis_coordinates_converter.h" #include "tool_transform_args.h" #include "transform_transaction_properties.h" #include "kis_painting_tweaks.h" #include "kis_cursor.h" #include "kis_transform_utils.h" #include "kis_algebra_2d.h" #include "KisHandlePainterHelper.h" #include "kis_signal_compressor.h" struct KisWarpTransformStrategy::Private { Private(KisWarpTransformStrategy *_q, const KisCoordinatesConverter *_converter, ToolTransformArgs &_currentArgs, TransformTransactionProperties &_transaction) : q(_q), converter(_converter), currentArgs(_currentArgs), transaction(_transaction), lastNumPoints(0), drawConnectionLines(false), // useful while developing drawOrigPoints(false), drawTransfPoints(true), closeOnStartPointClick(false), clipOriginalPointsPosition(true), pointWasDragged(false), recalculateSignalCompressor(40, KisSignalCompressor::FIRST_ACTIVE) { } KisWarpTransformStrategy * const q; /// standard members /// const KisCoordinatesConverter *converter; ////// ToolTransformArgs ¤tArgs; ////// TransformTransactionProperties &transaction; QTransform paintingTransform; QPointF paintingOffset; QTransform handlesTransform; /// custom members /// QImage transformedImage; int pointIndexUnderCursor; enum Mode { OVER_POINT = 0, MULTIPLE_POINT_SELECTION, MOVE_MODE, ROTATE_MODE, SCALE_MODE, NOTHING }; Mode mode; QVector pointsInAction; int lastNumPoints; bool drawConnectionLines; bool drawOrigPoints; bool drawTransfPoints; bool closeOnStartPointClick; bool clipOriginalPointsPosition; QPointF pointPosOnClick; bool pointWasDragged; QPointF lastMousePos; // cage transform also uses this logic. This helps this class know what transform type we are using TransformType transformType = TransformType::WARP_TRANSFORM; KisSignalCompressor recalculateSignalCompressor; void recalculateTransformations(); inline QPointF imageToThumb(const QPointF &pt, bool useFlakeOptimization); bool shouldCloseTheCage() const; QVector getSelectedPoints(QPointF *center, bool limitToSelectedOnly = false) const; }; KisWarpTransformStrategy::KisWarpTransformStrategy(const KisCoordinatesConverter *converter, ToolTransformArgs ¤tArgs, TransformTransactionProperties &transaction) : KisSimplifiedActionPolicyStrategy(converter), m_d(new Private(this, converter, currentArgs, transaction)) { connect(&m_d->recalculateSignalCompressor, SIGNAL(timeout()), SLOT(recalculateTransformations())); } KisWarpTransformStrategy::~KisWarpTransformStrategy() { } void KisWarpTransformStrategy::setTransformFunction(const QPointF &mousePos, bool perspectiveModifierActive) { double handleRadius = KisTransformUtils::effectiveHandleGrabRadius(m_d->converter); bool cursorOverPoint = false; m_d->pointIndexUnderCursor = -1; KisTransformUtils::HandleChooser handleChooser(mousePos, Private::NOTHING); const QVector &points = m_d->currentArgs.transfPoints(); for (int i = 0; i < points.size(); ++i) { if (handleChooser.addFunction(points[i], handleRadius, Private::NOTHING)) { cursorOverPoint = true; m_d->pointIndexUnderCursor = i; } } if (cursorOverPoint) { m_d->mode = perspectiveModifierActive && !m_d->currentArgs.isEditingTransformPoints() ? Private::MULTIPLE_POINT_SELECTION : Private::OVER_POINT; } else if (!m_d->currentArgs.isEditingTransformPoints()) { QPolygonF polygon(m_d->currentArgs.transfPoints()); bool insidePolygon = polygon.boundingRect().contains(mousePos); m_d->mode = insidePolygon ? Private::MOVE_MODE : !perspectiveModifierActive ? Private::ROTATE_MODE : Private::SCALE_MODE; } else { m_d->mode = Private::NOTHING; } } QCursor KisWarpTransformStrategy::getCurrentCursor() const { QCursor cursor; switch (m_d->mode) { case Private::OVER_POINT: cursor = KisCursor::pointingHandCursor(); break; case Private::MULTIPLE_POINT_SELECTION: cursor = KisCursor::crossCursor(); break; case Private::MOVE_MODE: cursor = KisCursor::moveCursor(); break; case Private::ROTATE_MODE: cursor = KisCursor::rotateCursor(); break; case Private::SCALE_MODE: cursor = KisCursor::sizeVerCursor(); break; case Private::NOTHING: cursor = KisCursor::arrowCursor(); break; } return cursor; } void KisWarpTransformStrategy::overrideDrawingItems(bool drawConnectionLines, bool drawOrigPoints, bool drawTransfPoints) { m_d->drawConnectionLines = drawConnectionLines; m_d->drawOrigPoints = drawOrigPoints; m_d->drawTransfPoints = drawTransfPoints; } void KisWarpTransformStrategy::setCloseOnStartPointClick(bool value) { m_d->closeOnStartPointClick = value; } void KisWarpTransformStrategy::setClipOriginalPointsPosition(bool value) { m_d->clipOriginalPointsPosition = value; } void KisWarpTransformStrategy::setTransformType(TransformType type) { m_d->transformType = type; } void KisWarpTransformStrategy::drawConnectionLines(QPainter &gc, const QVector &origPoints, const QVector &transfPoints, bool isEditingPoints) { Q_UNUSED(isEditingPoints); QPen antsPen; QPen outlinePen; KisPaintingTweaks::initAntsPen(&antsPen, &outlinePen); const int numPoints = origPoints.size(); for (int i = 0; i < numPoints; ++i) { gc.setPen(outlinePen); gc.drawLine(transfPoints[i], origPoints[i]); gc.setPen(antsPen); gc.drawLine(transfPoints[i], origPoints[i]); } } void KisWarpTransformStrategy::paint(QPainter &gc) { // Draw preview image gc.save(); gc.setOpacity(m_d->transaction.basePreviewOpacity()); gc.setTransform(m_d->paintingTransform, true); gc.drawImage(m_d->paintingOffset, m_d->transformedImage); gc.restore(); gc.save(); gc.setTransform(m_d->handlesTransform, true); if (m_d->drawConnectionLines) { gc.setOpacity(0.5); drawConnectionLines(gc, m_d->currentArgs.origPoints(), m_d->currentArgs.transfPoints(), m_d->currentArgs.isEditingTransformPoints()); } QPen mainPen(Qt::black); QPen outlinePen(Qt::white); // draw handles { const int numPoints = m_d->currentArgs.origPoints().size(); qreal handlesExtraScale = KisTransformUtils::scaleFromAffineMatrix(m_d->handlesTransform); qreal dstIn = 8 / handlesExtraScale; qreal dstOut = 10 / handlesExtraScale; qreal srcIn = 6 / handlesExtraScale; qreal srcOut = 6 / handlesExtraScale; QRectF handleRect1(-0.5 * dstIn, -0.5 * dstIn, dstIn, dstIn); QRectF handleRect2(-0.5 * dstOut, -0.5 * dstOut, dstOut, dstOut); if (m_d->drawTransfPoints) { gc.setOpacity(1.0); for (int i = 0; i < numPoints; ++i) { gc.setPen(outlinePen); gc.drawEllipse(handleRect2.translated(m_d->currentArgs.transfPoints()[i])); gc.setPen(mainPen); gc.drawEllipse(handleRect1.translated(m_d->currentArgs.transfPoints()[i])); } QPointF center; QVector selectedPoints = m_d->getSelectedPoints(¢er, true); QBrush selectionBrush = selectedPoints.size() > 1 ? Qt::red : Qt::black; QBrush oldBrush = gc.brush(); gc.setBrush(selectionBrush); Q_FOREACH (const QPointF *pt, selectedPoints) { gc.drawEllipse(handleRect1.translated(*pt)); } gc.setBrush(oldBrush); } if (m_d->drawOrigPoints) { QPainterPath inLine; inLine.moveTo(-0.5 * srcIn, 0); inLine.lineTo( 0.5 * srcIn, 0); inLine.moveTo( 0, -0.5 * srcIn); inLine.lineTo( 0, 0.5 * srcIn); QPainterPath outLine; outLine.moveTo(-0.5 * srcOut, -0.5 * srcOut); outLine.lineTo( 0.5 * srcOut, -0.5 * srcOut); outLine.lineTo( 0.5 * srcOut, 0.5 * srcOut); outLine.lineTo(-0.5 * srcOut, 0.5 * srcOut); outLine.lineTo(-0.5 * srcOut, -0.5 * srcOut); gc.setOpacity(0.5); for (int i = 0; i < numPoints; ++i) { gc.setPen(outlinePen); gc.drawPath(outLine.translated(m_d->currentArgs.origPoints()[i])); gc.setPen(mainPen); gc.drawPath(inLine.translated(m_d->currentArgs.origPoints()[i])); } } } // draw grid lines only if we are using the GRID mode. Also only use this logic for warp, not cage transforms if (m_d->currentArgs.warpCalculation() == KisWarpTransformWorker::WarpCalculation::GRID && m_d->transformType == TransformType::WARP_TRANSFORM ) { // see how many rows we have. we are only going to do lines up to 6 divisions/ // it is almost impossible to use with 6 even. const int numPoints = m_d->currentArgs.origPoints().size(); // grid is always square, so get the square root to find # of rows int rowsInWarp = sqrt(m_d->currentArgs.origPoints().size()); KisHandlePainterHelper handlePainter(&gc); handlePainter.setHandleStyle(KisHandleStyle::primarySelection()); // draw horizontal lines for (int i = 0; i < numPoints; i++) { if (i != 0 && i % rowsInWarp == rowsInWarp -1) { // skip line if it is the last in the row } else { handlePainter.drawConnectionLine(m_d->currentArgs.transfPoints()[i], m_d->currentArgs.transfPoints()[i+1] ); } } // draw vertical lines for (int i = 0; i < numPoints; i++) { if ( (numPoints - i - 1) < rowsInWarp ) { // last row doesn't need to draw vertical lines } else { handlePainter.drawConnectionLine(m_d->currentArgs.transfPoints()[i], m_d->currentArgs.transfPoints()[i+rowsInWarp] ); } } } // end if statement gc.restore(); } void KisWarpTransformStrategy::externalConfigChanged() { if (m_d->lastNumPoints != m_d->currentArgs.transfPoints().size()) { m_d->pointsInAction.clear(); } m_d->recalculateTransformations(); } bool KisWarpTransformStrategy::beginPrimaryAction(const QPointF &pt) { const bool isEditingPoints = m_d->currentArgs.isEditingTransformPoints(); bool retval = false; if (m_d->mode == Private::OVER_POINT || m_d->mode == Private::MULTIPLE_POINT_SELECTION || m_d->mode == Private::MOVE_MODE || m_d->mode == Private::ROTATE_MODE || m_d->mode == Private::SCALE_MODE) { retval = true; } else if (isEditingPoints) { QPointF newPos = m_d->clipOriginalPointsPosition ? KisTransformUtils::clipInRect(pt, m_d->transaction.originalRect()) : pt; m_d->currentArgs.refOriginalPoints().append(newPos); m_d->currentArgs.refTransformedPoints().append(newPos); m_d->mode = Private::OVER_POINT; m_d->pointIndexUnderCursor = m_d->currentArgs.origPoints().size() - 1; m_d->recalculateSignalCompressor.start(); retval = true; } if (m_d->mode == Private::OVER_POINT) { m_d->pointPosOnClick = m_d->currentArgs.transfPoints()[m_d->pointIndexUnderCursor]; m_d->pointWasDragged = false; m_d->pointsInAction.clear(); m_d->pointsInAction << m_d->pointIndexUnderCursor; m_d->lastNumPoints = m_d->currentArgs.transfPoints().size(); } else if (m_d->mode == Private::MULTIPLE_POINT_SELECTION) { QVector::iterator it = std::find(m_d->pointsInAction.begin(), m_d->pointsInAction.end(), m_d->pointIndexUnderCursor); if (it != m_d->pointsInAction.end()) { m_d->pointsInAction.erase(it); } else { m_d->pointsInAction << m_d->pointIndexUnderCursor; } m_d->lastNumPoints = m_d->currentArgs.transfPoints().size(); } m_d->lastMousePos = pt; return retval; } QVector KisWarpTransformStrategy::Private::getSelectedPoints(QPointF *center, bool limitToSelectedOnly) const { QVector &points = currentArgs.refTransformedPoints(); QRectF boundingRect; QVector selectedPoints; if (limitToSelectedOnly || pointsInAction.size() > 1) { Q_FOREACH (int index, pointsInAction) { selectedPoints << &points[index]; KisAlgebra2D::accumulateBounds(points[index], &boundingRect); } } else { QVector::iterator it = points.begin(); QVector::iterator end = points.end(); for (; it != end; ++it) { selectedPoints << &(*it); KisAlgebra2D::accumulateBounds(*it, &boundingRect); } } *center = boundingRect.center(); return selectedPoints; } void KisWarpTransformStrategy::continuePrimaryAction(const QPointF &pt, bool shiftModifierActve, bool altModifierActive) { Q_UNUSED(shiftModifierActve); Q_UNUSED(altModifierActive); // toplevel code switches to HOVER mode if nothing is selected KIS_ASSERT_RECOVER_RETURN(m_d->mode == Private::MOVE_MODE || m_d->mode == Private::ROTATE_MODE || m_d->mode == Private::SCALE_MODE || (m_d->mode == Private::OVER_POINT && m_d->pointIndexUnderCursor >= 0 && m_d->pointsInAction.size() == 1) || (m_d->mode == Private::MULTIPLE_POINT_SELECTION && m_d->pointIndexUnderCursor >= 0)); if (m_d->mode == Private::OVER_POINT) { if (m_d->currentArgs.isEditingTransformPoints()) { QPointF newPos = m_d->clipOriginalPointsPosition ? KisTransformUtils::clipInRect(pt, m_d->transaction.originalRect()) : pt; m_d->currentArgs.origPoint(m_d->pointIndexUnderCursor) = newPos; m_d->currentArgs.transfPoint(m_d->pointIndexUnderCursor) = newPos; } else { m_d->currentArgs.transfPoint(m_d->pointIndexUnderCursor) = pt; } const qreal handleRadiusSq = pow2(KisTransformUtils::effectiveHandleGrabRadius(m_d->converter)); qreal dist = kisSquareDistance( m_d->currentArgs.transfPoint(m_d->pointIndexUnderCursor), m_d->pointPosOnClick); if (dist > handleRadiusSq) { m_d->pointWasDragged = true; } } else if (m_d->mode == Private::MOVE_MODE) { QPointF center; QVector selectedPoints = m_d->getSelectedPoints(¢er); QPointF diff = pt - m_d->lastMousePos; QVector::iterator it = selectedPoints.begin(); QVector::iterator end = selectedPoints.end(); for (; it != end; ++it) { **it += diff; } } else if (m_d->mode == Private::ROTATE_MODE) { QPointF center; QVector selectedPoints = m_d->getSelectedPoints(¢er); QPointF oldDirection = m_d->lastMousePos - center; QPointF newDirection = pt - center; qreal rotateAngle = KisAlgebra2D::angleBetweenVectors(oldDirection, newDirection); QTransform R; R.rotateRadians(rotateAngle); QTransform t = QTransform::fromTranslate(-center.x(), -center.y()) * R * QTransform::fromTranslate(center.x(), center.y()); QVector::iterator it = selectedPoints.begin(); QVector::iterator end = selectedPoints.end(); for (; it != end; ++it) { **it = t.map(**it); } } else if (m_d->mode == Private::SCALE_MODE) { QPointF center; QVector selectedPoints = m_d->getSelectedPoints(¢er); QPolygonF polygon(m_d->currentArgs.origPoints()); QSizeF maxSize = polygon.boundingRect().size(); qreal maxDimension = qMax(maxSize.width(), maxSize.height()); qreal scale = 1.0 - (pt - m_d->lastMousePos).y() / maxDimension; QTransform t = QTransform::fromTranslate(-center.x(), -center.y()) * QTransform::fromScale(scale, scale) * QTransform::fromTranslate(center.x(), center.y()); QVector::iterator it = selectedPoints.begin(); QVector::iterator end = selectedPoints.end(); for (; it != end; ++it) { **it = t.map(**it); } } m_d->lastMousePos = pt; m_d->recalculateSignalCompressor.start(); } bool KisWarpTransformStrategy::Private::shouldCloseTheCage() const { return currentArgs.isEditingTransformPoints() && closeOnStartPointClick && pointIndexUnderCursor == 0 && currentArgs.origPoints().size() > 2 && !pointWasDragged; } bool KisWarpTransformStrategy::acceptsClicks() const { return m_d->shouldCloseTheCage() || m_d->currentArgs.isEditingTransformPoints(); } bool KisWarpTransformStrategy::endPrimaryAction() { if (m_d->shouldCloseTheCage()) { m_d->currentArgs.setEditingTransformPoints(false); } return true; } inline QPointF KisWarpTransformStrategy::Private::imageToThumb(const QPointF &pt, bool useFlakeOptimization) { return useFlakeOptimization ? converter->imageToDocument(converter->documentToFlake((pt))) : q->thumbToImageTransform().inverted().map(pt); } void KisWarpTransformStrategy::Private::recalculateTransformations() { QTransform scaleTransform = KisTransformUtils::imageToFlakeTransform(converter); QTransform resultThumbTransform = q->thumbToImageTransform() * scaleTransform; qreal scale = KisTransformUtils::scaleFromAffineMatrix(resultThumbTransform); bool useFlakeOptimization = scale < 1.0 && !KisTransformUtils::thumbnailTooSmall(resultThumbTransform, q->originalImage().rect()); QVector thumbOrigPoints(currentArgs.numPoints()); QVector thumbTransfPoints(currentArgs.numPoints()); for (int i = 0; i < currentArgs.numPoints(); ++i) { thumbOrigPoints[i] = imageToThumb(currentArgs.origPoints()[i], useFlakeOptimization); thumbTransfPoints[i] = imageToThumb(currentArgs.transfPoints()[i], useFlakeOptimization); } paintingOffset = transaction.originalTopLeft(); if (!q->originalImage().isNull() && !currentArgs.isEditingTransformPoints()) { QPointF origTLInFlake = imageToThumb(transaction.originalTopLeft(), useFlakeOptimization); if (useFlakeOptimization) { transformedImage = q->originalImage().transformed(resultThumbTransform); paintingTransform = QTransform(); } else { transformedImage = q->originalImage(); paintingTransform = resultThumbTransform; } transformedImage = q->calculateTransformedImage(currentArgs, transformedImage, thumbOrigPoints, thumbTransfPoints, origTLInFlake, &paintingOffset); } else { transformedImage = q->originalImage(); paintingOffset = imageToThumb(transaction.originalTopLeft(), false); paintingTransform = resultThumbTransform; } handlesTransform = scaleTransform; emit q->requestCanvasUpdate(); } QImage KisWarpTransformStrategy::calculateTransformedImage(ToolTransformArgs ¤tArgs, const QImage &srcImage, const QVector &origPoints, const QVector &transfPoints, const QPointF &srcOffset, QPointF *dstOffset) { return KisWarpTransformWorker::transformQImage( currentArgs.warpType(), origPoints, transfPoints, currentArgs.alpha(), srcImage, srcOffset, dstOffset); } #include "moc_kis_warp_transform_strategy.cpp"