diff --git a/libs/image/brushengine/kis_paintop.h b/libs/image/brushengine/kis_paintop.h index 13c7db5842..e97639249b 100644 --- a/libs/image/brushengine/kis_paintop.h +++ b/libs/image/brushengine/kis_paintop.h @@ -1,155 +1,155 @@ /* * Copyright (c) 2002 Patrick Julien * Copyright (c) 2004 Boudewijn Rempt * Copyright (c) 2004 Clarence Dang * Copyright (c) 2004 Adrian Page * Copyright (c) 2004,2010 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. */ #ifndef KIS_PAINTOP_H_ #define KIS_PAINTOP_H_ #include #include "kis_shared.h" #include "kis_types.h" #include class QPointF; class KoColorSpace; class KisPainter; class KisPaintInformation; /** * KisPaintOp are use by tools to draw on a paint device. A paintop takes settings * and input information, like pressure, tilt or motion and uses that to draw pixels */ class KRITAIMAGE_EXPORT KisPaintOp : public KisShared { struct Private; public: KisPaintOp(KisPainter * painter); virtual ~KisPaintOp(); /** * Paint at the subpixel point pos using the specified paint information.. * * The distance/time between two calls of the paintAt is always specified by spacing and timing, * which are automatically saved into the current distance information object. */ void paintAt(const KisPaintInformation& info, KisDistanceInformation *currentDistance); /** * Updates the spacing settings in currentDistance based on the provided information. Note that * the spacing is updated automatically in the paintAt method, so there is no need to call this * method if paintAt has just been called. */ void updateSpacing(const KisPaintInformation &info, KisDistanceInformation ¤tDistance) const; /** - * Updates the timing in currentDistance based on the provided information. Note that the timing - * is updated automatically in the paintAt method, so there is no need to call this method if - * paintAt has just been called. + * Updates the timing settings in currentDistance based on the provided information. Note that + * the timing is updated automatically in the paintAt method, so there is no need to call this + * method if paintAt has just been called. */ void updateTiming(const KisPaintInformation &info, KisDistanceInformation ¤tDistance) const; /** * Draw a line between pos1 and pos2 using the currently set brush and color. * If savedDist is less than zero, the brush is painted at pos1 before being * painted along the line using the spacing setting. * * @return the drag distance, that is the remains of the distance * between p1 and p2 not covered because the currenlty set brush * has a spacing greater than that distance. */ virtual void paintLine(const KisPaintInformation &pi1, const KisPaintInformation &pi2, KisDistanceInformation *currentDistance); /** * Draw a Bezier curve between pos1 and pos2 using control points 1 and 2. * If savedDist is less than zero, the brush is painted at pos1 before being * painted along the curve using the spacing setting. * @return the drag distance, that is the remains of the distance between p1 and p2 not covered * because the currenlty set brush has a spacing greater than that distance. */ virtual void paintBezierCurve(const KisPaintInformation &pi1, const QPointF &control1, const QPointF &control2, const KisPaintInformation &pi2, KisDistanceInformation *currentDistance); /** * Whether this paintop can paint. Can be false in case that some setting isn't read correctly. * @return if paintop is ready for painting, default is true */ virtual bool canPaint() const { return true; } /** * Split the coordinate into whole + fraction, where fraction is always >= 0. */ static void splitCoordinate(qreal coordinate, qint32 *whole, qreal *fraction); protected: friend class KisPaintInformation; /** * The implementation of painting of a dab and updating spacing. This does NOT need to update * the timing information. */ virtual KisSpacingInformation paintAt(const KisPaintInformation& info) = 0; /** * Implementation of a spacing update */ virtual KisSpacingInformation updateSpacingImpl(const KisPaintInformation &info) const = 0; /** * Implementation of a timing update. The default implementation always disables timing. This is * suitable for paintops that do not support airbrushing. */ virtual KisTimingInformation updateTimingImpl(const KisPaintInformation &info) const; KisFixedPaintDeviceSP cachedDab(); KisFixedPaintDeviceSP cachedDab(const KoColorSpace *cs); /** * Return the painter this paintop is owned by */ KisPainter* painter() const; /** * Return the paintdevice the painter this paintop is owned by */ KisPaintDeviceSP source() const; private: friend class KisPressureRotationOption; void setFanCornersInfo(bool fanCornersEnabled, qreal fanCornersStep); private: Private* const d; }; #endif // KIS_PAINTOP_H_ diff --git a/libs/image/brushengine/kis_paintop_utils.h b/libs/image/brushengine/kis_paintop_utils.h index 487b003e80..ca53683483 100644 --- a/libs/image/brushengine/kis_paintop_utils.h +++ b/libs/image/brushengine/kis_paintop_utils.h @@ -1,228 +1,228 @@ /* * Copyright (c) 2014 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __KIS_PAINTOP_UTILS_H #define __KIS_PAINTOP_UTILS_H #include "kis_global.h" #include "kis_paint_information.h" #include "kis_distance_information.h" #include "kis_spacing_information.h" #include "kis_timing_information.h" namespace KisPaintOpUtils { template bool paintFan(PaintOp &op, const KisPaintInformation &pi1, const KisPaintInformation &pi2, KisDistanceInformation *currentDistance, qreal fanCornersStep) { const qreal angleStep = fanCornersStep; const qreal initialAngle = currentDistance->lastDrawingAngle(); const qreal finalAngle = pi2.drawingAngleSafe(*currentDistance); const qreal fullDistance = shortestAngularDistance(initialAngle, finalAngle); qreal lastAngle = initialAngle; int i = 0; while (shortestAngularDistance(lastAngle, finalAngle) > angleStep) { lastAngle = incrementInDirection(lastAngle, angleStep, finalAngle); qreal t = angleStep * i++ / fullDistance; QPointF pt = pi1.pos() + t * (pi2.pos() - pi1.pos()); KisPaintInformation pi = KisPaintInformation::mix(pt, t, pi1, pi2); pi.overrideDrawingAngle(lastAngle); pi.paintAt(op, currentDistance); } return i; } template void paintLine(PaintOp &op, const KisPaintInformation &pi1, const KisPaintInformation &pi2, KisDistanceInformation *currentDistance, bool fanCornersEnabled, qreal fanCornersStep) { QPointF end = pi2.pos(); qreal endTime = pi2.currentTime(); KisPaintInformation pi = pi1; qreal t = 0.0; while ((t = currentDistance->getNextPointPosition(pi.pos(), end, pi.currentTime(), endTime)) >= 0.0) { pi = KisPaintInformation::mix(t, pi, pi2); if (fanCornersEnabled && currentDistance->hasLastPaintInformation()) { paintFan(op, currentDistance->lastPaintInformation(), pi, currentDistance, fanCornersStep); } /** * A bit complicated part to ensure the registration * of the distance information is done in right order */ pi.paintAt(op, currentDistance); } /* * Perform spacing and/or timing updates between dabs if appropriate. Typically, this will not * happen if the above loop actually painted anything. This is because the * getNextPointPosition() call before the paint operation will reset the accumulators in - * currentDistance and therefore make needsSpacingUpdate() needsTimingUpdate() false. The + * currentDistance and therefore make needsSpacingUpdate() and needsTimingUpdate() false. The * temporal distance between pi1 and pi2 is typically too small for the accumulators to build * back up enough to require a spacing or timing update after that. (The accumulated time values * are updated not during the paint operation, but during the call to getNextPointPosition(), * that is, updated during every paintLine() call.) */ if (currentDistance->needsSpacingUpdate()) { op.updateSpacing(pi2, *currentDistance); } if (currentDistance->needsTimingUpdate()) { op.updateTiming(pi2, *currentDistance); } } /** * A special class containing the previous position of the cursor for * the sake of painting the outline of the paint op. The main purpose * of this class is to ensure that the saved point does not equal to * the current one, which would cause a outline flicker. To echieve * this the class stores two previosly requested points instead of the * last one. */ class PositionHistory { public: /** * \return the previously used point, which is guaranteed not to * be equal to \p pt and updates the history if needed */ QPointF pushThroughHistory(const QPointF &pt) { QPointF result; const qreal pointSwapThreshold = 7.0; /** * We check x *and* y separately, because events generated by * a mouse device tend to come separately for x and y offsets. * Efficienty generating the 'stairs' pattern. */ if (qAbs(pt.x() - m_second.x()) > pointSwapThreshold && qAbs(pt.y() - m_second.y()) > pointSwapThreshold) { result = m_second; m_first = m_second; m_second = pt; } else { result = m_first; } return result; } private: QPointF m_first; QPointF m_second; }; bool checkSizeTooSmall(qreal scale, qreal width, qreal height) { return scale * width < 0.01 || scale * height < 0.01; } inline qreal calcAutoSpacing(qreal value, qreal coeff) { return coeff * (value < 1.0 ? value : sqrt(value)); } QPointF calcAutoSpacing(const QPointF &pt, qreal coeff, qreal lodScale) { const qreal invLodScale = 1.0 / lodScale; const QPointF lod0Point = invLodScale * pt; return lodScale * QPointF(calcAutoSpacing(lod0Point.x(), coeff), calcAutoSpacing(lod0Point.y(), coeff)); } KisSpacingInformation effectiveSpacing(qreal dabWidth, qreal dabHeight, qreal extraScale, bool distanceSpacingEnabled, bool isotropicSpacing, qreal rotation, bool axesFlipped, qreal spacingVal, bool autoSpacingActive, qreal autoSpacingCoeff, qreal lodScale) { QPointF spacing; if (!isotropicSpacing) { if (autoSpacingActive) { spacing = calcAutoSpacing(QPointF(dabWidth, dabHeight), autoSpacingCoeff, lodScale); } else { spacing = QPointF(dabWidth, dabHeight); spacing *= spacingVal; } } else { qreal significantDimension = qMax(dabWidth, dabHeight); if (autoSpacingActive) { significantDimension = calcAutoSpacing(significantDimension, autoSpacingCoeff); } else { significantDimension *= spacingVal; } spacing = QPointF(significantDimension, significantDimension); rotation = 0.0; axesFlipped = false; } spacing *= extraScale; return KisSpacingInformation(distanceSpacingEnabled, spacing, rotation, axesFlipped); } KisTimingInformation effectiveTiming(bool timingEnabled, qreal timingInterval, qreal rateExtraScale) { if (!timingEnabled) { return KisTimingInformation(); } else { qreal scaledInterval = rateExtraScale <= 0.0 ? LONG_TIME : timingInterval / rateExtraScale; return KisTimingInformation(scaledInterval); } } } #endif /* __KIS_PAINTOP_UTILS_H */ diff --git a/libs/image/kis_distance_information.cpp b/libs/image/kis_distance_information.cpp index bb3efb1382..76643df404 100644 --- a/libs/image/kis_distance_information.cpp +++ b/libs/image/kis_distance_information.cpp @@ -1,650 +1,649 @@ /* * Copyright (c) 2010 Cyrille Berger * Copyright (c) 2013 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 #include #include "kis_spacing_information.h" #include "kis_timing_information.h" #include "kis_debug.h" #include #include #include #include "kis_algebra_2d.h" #include "kis_dom_utils.h" #include "kis_lod_transform.h" const qreal MIN_DISTANCE_SPACING = 0.5; // Smallest allowed interval when timed spacing is enabled, in milliseconds. const qreal MIN_TIMED_INTERVAL = 0.5; // Largest allowed interval when timed spacing is enabled, in milliseconds. const qreal MAX_TIMED_INTERVAL = LONG_TIME; struct Q_DECL_HIDDEN KisDistanceInformation::Private { Private() : accumDistance(), accumTime(0.0), spacingUpdateInterval(LONG_TIME), timeSinceSpacingUpdate(0.0), timingUpdateInterval(LONG_TIME), timeSinceTimingUpdate(0.0), lastDabInfoValid(false), lastPaintInfoValid(false), lockedDrawingAngle(0.0), hasLockedDrawingAngle(false), totalDistance(0.0) {} // Accumulators of time/distance passed since the last painted dab QPointF accumDistance; qreal accumTime; KisSpacingInformation spacing; qreal spacingUpdateInterval; // Accumulator of time passed since the last spacing update qreal timeSinceSpacingUpdate; KisTimingInformation timing; qreal timingUpdateInterval; // Accumulator of time passed since the last timing update qreal timeSinceTimingUpdate; // Information about the last position considered (not necessarily a painted dab) QPointF lastPosition; qreal lastTime; qreal lastAngle; bool lastDabInfoValid; // Information about the last painted dab KisPaintInformation lastPaintInformation; bool lastPaintInfoValid; qreal lockedDrawingAngle; bool hasLockedDrawingAngle; qreal totalDistance; }; struct Q_DECL_HIDDEN KisDistanceInitInfo::Private { Private() : hasLastInfo(false), lastPosition(), lastTime(0.0), lastAngle(0.0), spacingUpdateInterval(LONG_TIME), timingUpdateInterval(LONG_TIME) {} // Indicates whether lastPosition, lastTime, and lastAngle are valid or not. bool hasLastInfo; QPointF lastPosition; qreal lastTime; qreal lastAngle; qreal spacingUpdateInterval; qreal timingUpdateInterval; }; KisDistanceInitInfo::KisDistanceInitInfo() : m_d(new Private) { } KisDistanceInitInfo::KisDistanceInitInfo(qreal spacingUpdateInterval, qreal timingUpdateInterval) : m_d(new Private) { m_d->spacingUpdateInterval = spacingUpdateInterval; m_d->timingUpdateInterval = timingUpdateInterval; } KisDistanceInitInfo::KisDistanceInitInfo(const QPointF &lastPosition, qreal lastTime, qreal lastAngle) : m_d(new Private) { m_d->hasLastInfo = true; m_d->lastPosition = lastPosition; m_d->lastTime = lastTime; m_d->lastAngle = lastAngle; } KisDistanceInitInfo::KisDistanceInitInfo(const QPointF &lastPosition, qreal lastTime, qreal lastAngle, qreal spacingUpdateInterval, qreal timingUpdateInterval) : m_d(new Private) { m_d->hasLastInfo = true; m_d->lastPosition = lastPosition; m_d->lastTime = lastTime; m_d->lastAngle = lastAngle; m_d->spacingUpdateInterval = spacingUpdateInterval; m_d->timingUpdateInterval = timingUpdateInterval; } KisDistanceInitInfo::KisDistanceInitInfo(const KisDistanceInitInfo &rhs) : m_d(new Private(*rhs.m_d)) { } KisDistanceInitInfo::~KisDistanceInitInfo() { delete m_d; } bool KisDistanceInitInfo::operator==(const KisDistanceInitInfo &other) const { if (m_d->spacingUpdateInterval != other.m_d->spacingUpdateInterval || m_d->timingUpdateInterval != other.m_d->timingUpdateInterval || m_d->hasLastInfo != other.m_d->hasLastInfo) { return false; } if (m_d->hasLastInfo) { if (m_d->lastPosition != other.m_d->lastPosition || m_d->lastTime != other.m_d->lastTime || m_d->lastAngle != other.m_d->lastAngle) { return false; } } return true; } bool KisDistanceInitInfo::operator!=(const KisDistanceInitInfo &other) const { return !(*this == other); } KisDistanceInitInfo &KisDistanceInitInfo::operator=(const KisDistanceInitInfo &rhs) { *m_d = *rhs.m_d; return *this; } KisDistanceInformation KisDistanceInitInfo::makeDistInfo() { if (m_d->hasLastInfo) { return KisDistanceInformation(m_d->lastPosition, m_d->lastTime, m_d->lastAngle, m_d->spacingUpdateInterval, m_d->timingUpdateInterval); } else { return KisDistanceInformation(m_d->spacingUpdateInterval, m_d->timingUpdateInterval); } } void KisDistanceInitInfo::toXML(QDomDocument &doc, QDomElement &elt) const { elt.setAttribute("spacingUpdateInterval", QString::number(m_d->spacingUpdateInterval, 'g', 15)); elt.setAttribute("timingUpdateInterval", QString::number(m_d->timingUpdateInterval, 'g', 15)); if (m_d->hasLastInfo) { QDomElement lastInfoElt = doc.createElement("LastInfo"); lastInfoElt.setAttribute("lastPosX", QString::number(m_d->lastPosition.x(), 'g', 15)); lastInfoElt.setAttribute("lastPosY", QString::number(m_d->lastPosition.y(), 'g', 15)); lastInfoElt.setAttribute("lastTime", QString::number(m_d->lastTime, 'g', 15)); lastInfoElt.setAttribute("lastAngle", QString::number(m_d->lastAngle, 'g', 15)); elt.appendChild(lastInfoElt); } } KisDistanceInitInfo KisDistanceInitInfo::fromXML(const QDomElement &elt) { const qreal spacingUpdateInterval = qreal(KisDomUtils::toDouble(elt.attribute("spacingUpdateInterval", QString::number(LONG_TIME, 'g', 15)))); const qreal timingUpdateInterval = qreal(KisDomUtils::toDouble(elt.attribute("timingUpdateInterval", QString::number(LONG_TIME, 'g', 15)))); const QDomElement lastInfoElt = elt.firstChildElement("LastInfo"); const bool hasLastInfo = !lastInfoElt.isNull(); if (hasLastInfo) { const qreal lastPosX = qreal(KisDomUtils::toDouble(lastInfoElt.attribute("lastPosX", "0.0"))); const qreal lastPosY = qreal(KisDomUtils::toDouble(lastInfoElt.attribute("lastPosY", "0.0"))); const qreal lastTime = qreal(KisDomUtils::toDouble(lastInfoElt.attribute("lastTime", "0.0"))); const qreal lastAngle = qreal(KisDomUtils::toDouble(lastInfoElt.attribute("lastAngle", "0.0"))); return KisDistanceInitInfo(QPointF(lastPosX, lastPosY), lastTime, lastAngle, spacingUpdateInterval, timingUpdateInterval); } else { return KisDistanceInitInfo(spacingUpdateInterval, timingUpdateInterval); } } KisDistanceInformation::KisDistanceInformation() : m_d(new Private) { } KisDistanceInformation::KisDistanceInformation(qreal spacingUpdateInterval, qreal timingUpdateInterval) : m_d(new Private) { m_d->spacingUpdateInterval = spacingUpdateInterval; m_d->timingUpdateInterval = timingUpdateInterval; } KisDistanceInformation::KisDistanceInformation(const QPointF &lastPosition, qreal lastTime, qreal lastAngle) : m_d(new Private) { m_d->lastPosition = lastPosition; m_d->lastTime = lastTime; m_d->lastAngle = lastAngle; m_d->lastDabInfoValid = true; } KisDistanceInformation::KisDistanceInformation(const QPointF &lastPosition, qreal lastTime, qreal lastAngle, qreal spacingUpdateInterval, qreal timingUpdateInterval) : KisDistanceInformation(lastPosition, lastTime, lastAngle) { m_d->spacingUpdateInterval = spacingUpdateInterval; m_d->timingUpdateInterval = timingUpdateInterval; } KisDistanceInformation::KisDistanceInformation(const KisDistanceInformation &rhs) : m_d(new Private(*rhs.m_d)) { } KisDistanceInformation::KisDistanceInformation(const KisDistanceInformation &rhs, int levelOfDetail) : m_d(new Private(*rhs.m_d)) { KIS_ASSERT_RECOVER_NOOP(!m_d->lastPaintInfoValid && "The distance information " "should be cloned before the " "actual painting is started"); KisLodTransform t(levelOfDetail); m_d->lastPosition = t.map(m_d->lastPosition); } KisDistanceInformation& KisDistanceInformation::operator=(const KisDistanceInformation &rhs) { *m_d = *rhs.m_d; return *this; } void KisDistanceInformation::overrideLastValues(const QPointF &lastPosition, qreal lastTime, qreal lastAngle) { m_d->lastPosition = lastPosition; m_d->lastTime = lastTime; m_d->lastAngle = lastAngle; m_d->lastDabInfoValid = true; } KisDistanceInformation::~KisDistanceInformation() { delete m_d; } const KisSpacingInformation& KisDistanceInformation::currentSpacing() const { return m_d->spacing; } void KisDistanceInformation::updateSpacing(const KisSpacingInformation &spacing) { m_d->spacing = spacing; m_d->timeSinceSpacingUpdate = 0.0; } bool KisDistanceInformation::needsSpacingUpdate() const { return m_d->timeSinceSpacingUpdate >= m_d->spacingUpdateInterval; } const KisTimingInformation &KisDistanceInformation::currentTiming() const { return m_d->timing; } void KisDistanceInformation::updateTiming(const KisTimingInformation &timing) { m_d->timing = timing; m_d->timeSinceTimingUpdate = 0.0; } bool KisDistanceInformation::needsTimingUpdate() const { return m_d->timeSinceTimingUpdate >= m_d->timingUpdateInterval; } bool KisDistanceInformation::hasLastDabInformation() const { return m_d->lastDabInfoValid; } QPointF KisDistanceInformation::lastPosition() const { return m_d->lastPosition; } qreal KisDistanceInformation::lastTime() const { return m_d->lastTime; } qreal KisDistanceInformation::lastDrawingAngle() const { return m_d->lastAngle; } bool KisDistanceInformation::hasLastPaintInformation() const { return m_d->lastPaintInfoValid; } const KisPaintInformation& KisDistanceInformation::lastPaintInformation() const { return m_d->lastPaintInformation; } bool KisDistanceInformation::isStarted() const { return m_d->lastPaintInfoValid; } void KisDistanceInformation::registerPaintedDab(const KisPaintInformation &info, const KisSpacingInformation &spacing, const KisTimingInformation &timing) { m_d->totalDistance += KisAlgebra2D::norm(info.pos() - m_d->lastPosition); m_d->lastPaintInformation = info; m_d->lastPaintInfoValid = true; m_d->lastAngle = nextDrawingAngle(info.pos()); m_d->lastPosition = info.pos(); m_d->lastTime = info.currentTime(); m_d->lastDabInfoValid = true; m_d->spacing = spacing; m_d->timing = timing; } qreal KisDistanceInformation::getNextPointPosition(const QPointF &start, const QPointF &end, qreal startTime, qreal endTime) { // Compute interpolation factor based on distance. qreal distanceFactor = -1.0; if (m_d->spacing.isDistanceSpacingEnabled()) { distanceFactor = m_d->spacing.isIsotropic() ? getNextPointPositionIsotropic(start, end) : getNextPointPositionAnisotropic(start, end); } // Compute interpolation factor based on time. qreal timeFactor = -1.0; if (m_d->timing.isTimedSpacingEnabled()) { timeFactor = getNextPointPositionTimed(startTime, endTime); } // Return the distance-based or time-based factor, whichever is smallest. qreal t = -1.0; if (distanceFactor < 0.0) { t = timeFactor; } else if (timeFactor < 0.0) { t = distanceFactor; } else { t = qMin(distanceFactor, timeFactor); } // If we aren't ready to paint a dab, accumulate time for the spacing/timing updates that might // be needed between dabs. if (t < 0.0) { m_d->timeSinceSpacingUpdate += endTime - startTime; m_d->timeSinceTimingUpdate += endTime - startTime; } // If we are ready to paint a dab, reset the accumulated time for spacing/timing updates. else { m_d->timeSinceSpacingUpdate = 0.0; m_d->timeSinceTimingUpdate = 0.0; } return t; } qreal KisDistanceInformation::getNextPointPositionIsotropic(const QPointF &start, const QPointF &end) { qreal distance = m_d->accumDistance.x(); qreal spacing = qMax(MIN_DISTANCE_SPACING, m_d->spacing.distanceSpacing().x()); if (start == end) { return -1; } qreal dragVecLength = QVector2D(end - start).length(); qreal nextPointDistance = spacing - distance; qreal t; // nextPointDistance can sometimes be negative if the spacing info has been modified since the // last interpolation attempt. In that case, have a point painted immediately. if (nextPointDistance <= 0.0) { - t = 0.0; resetAccumulators(); + t = 0.0; } else if (nextPointDistance <= dragVecLength) { t = nextPointDistance / dragVecLength; resetAccumulators(); } else { t = -1; - m_d->accumDistance.rx() += dragVecLength; } return t; } qreal KisDistanceInformation::getNextPointPositionAnisotropic(const QPointF &start, const QPointF &end) { if (start == end) { return -1; } qreal a_rev = 1.0 / qMax(MIN_DISTANCE_SPACING, m_d->spacing.distanceSpacing().x()); qreal b_rev = 1.0 / qMax(MIN_DISTANCE_SPACING, m_d->spacing.distanceSpacing().y()); qreal x = m_d->accumDistance.x(); qreal y = m_d->accumDistance.y(); qreal gamma = pow2(x * a_rev) + pow2(y * b_rev) - 1; // If the distance accumulator is already past the spacing ellipse, have a point painted // immediately. This can happen if the spacing info has been modified since the last // interpolation attempt. if (gamma >= 0.0) { resetAccumulators(); return 0.0; } static const qreal eps = 2e-3; // < 0.2 deg qreal currentRotation = m_d->spacing.rotation(); if (m_d->spacing.coordinateSystemFlipped()) { currentRotation = 2 * M_PI - currentRotation; } QPointF diff = end - start; if (currentRotation > eps) { QTransform rot; // since the ellipse is symmetrical, the sign // of rotation doesn't matter rot.rotateRadians(currentRotation); diff = rot.map(diff); } qreal dx = qAbs(diff.x()); qreal dy = qAbs(diff.y()); qreal alpha = pow2(dx * a_rev) + pow2(dy * b_rev); qreal beta = x * dx * a_rev * a_rev + y * dy * b_rev * b_rev; qreal D_4 = pow2(beta) - alpha * gamma; qreal t = -1.0; if (D_4 >= 0) { qreal k = (-beta + qSqrt(D_4)) / alpha; if (k >= 0.0 && k <= 1.0) { t = k; resetAccumulators(); } else { m_d->accumDistance += KisAlgebra2D::abs(diff); } } else { warnKrita << "BUG: No solution for elliptical spacing equation has been found. This shouldn't have happened."; } return t; } qreal KisDistanceInformation::getNextPointPositionTimed(qreal startTime, qreal endTime) { // If start time is not before end time, do not interpolate. if (!(startTime < endTime)) { return -1.0; } qreal timedSpacingInterval = qBound(MIN_TIMED_INTERVAL, m_d->timing.timedSpacingInterval(), MAX_TIMED_INTERVAL); qreal nextPointInterval = timedSpacingInterval - m_d->accumTime; qreal t = -1.0; // nextPointInterval can sometimes be negative if the spacing info has been modified since the // last interpolation attempt. In that case, have a point painted immediately. if (nextPointInterval <= 0.0) { resetAccumulators(); t = 0.0; } else if (nextPointInterval <= endTime - startTime) { resetAccumulators(); t = nextPointInterval / (endTime - startTime); } else { m_d->accumTime += endTime - startTime; t = -1.0; } return t; } void KisDistanceInformation::resetAccumulators() { m_d->accumDistance = QPointF(); m_d->accumTime = 0.0; } bool KisDistanceInformation::hasLockedDrawingAngle() const { return m_d->hasLockedDrawingAngle; } qreal KisDistanceInformation::lockedDrawingAngle() const { return m_d->lockedDrawingAngle; } void KisDistanceInformation::setLockedDrawingAngle(qreal angle) { m_d->hasLockedDrawingAngle = true; m_d->lockedDrawingAngle = angle; } qreal KisDistanceInformation::nextDrawingAngle(const QPointF &nextPos, bool considerLockedAngle) const { if (!m_d->lastDabInfoValid) { warnKrita << "KisDistanceInformation::nextDrawingAngle()" << "No last dab data"; return 0.0; } // Compute the drawing angle. If the new position is the same as the previous position, an angle // can't be computed. In that case, act as if the angle is the same as in the previous dab. return drawingAngleImpl(m_d->lastPosition, nextPos, considerLockedAngle, m_d->lastAngle); } QPointF KisDistanceInformation::nextDrawingDirectionVector(const QPointF &nextPos, bool considerLockedAngle) const { if (!m_d->lastDabInfoValid) { warnKrita << "KisDistanceInformation::nextDrawingDirectionVector()" << "No last dab data"; return QPointF(1.0, 0.0); } // Compute the direction vector. If the new position is the same as the previous position, a // direction can't be computed. In that case, act as if the direction is the same as in the // previous dab. return drawingDirectionVectorImpl(m_d->lastPosition, nextPos, considerLockedAngle, m_d->lastAngle); } qreal KisDistanceInformation::scalarDistanceApprox() const { return m_d->totalDistance; } qreal KisDistanceInformation::drawingAngleImpl(const QPointF &start, const QPointF &end, bool considerLockedAngle, qreal defaultAngle) const { if (m_d->hasLockedDrawingAngle && considerLockedAngle) { return m_d->lockedDrawingAngle; } // If the start and end positions are the same, we can't compute an angle. In that case, use the // provided default. return KisAlgebra2D::directionBetweenPoints(start, end, defaultAngle); } QPointF KisDistanceInformation::drawingDirectionVectorImpl(const QPointF &start, const QPointF &end, bool considerLockedAngle, qreal defaultAngle) const { if (m_d->hasLockedDrawingAngle && considerLockedAngle) { return QPointF(cos(m_d->lockedDrawingAngle), sin(m_d->lockedDrawingAngle)); } // If the start and end positions are the same, we can't compute a drawing direction. In that // case, use the provided default. if (KisAlgebra2D::fuzzyPointCompare(start, end)) { return QPointF(cos(defaultAngle), sin(defaultAngle)); } const QPointF diff(end - start); return KisAlgebra2D::normalize(diff); } diff --git a/libs/ui/tool/kis_tool_freehand_helper.cpp b/libs/ui/tool/kis_tool_freehand_helper.cpp index 9944283cb6..22ccb2e61a 100644 --- a/libs/ui/tool/kis_tool_freehand_helper.cpp +++ b/libs/ui/tool/kis_tool_freehand_helper.cpp @@ -1,974 +1,974 @@ /* * Copyright (c) 2011 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_tool_freehand_helper.h" #include #include #include #include #include #include "kis_algebra_2d.h" #include "kis_distance_information.h" #include "kis_painting_information_builder.h" #include "kis_recording_adapter.h" #include "kis_image.h" #include "kis_painter.h" #include #include #include "kis_update_time_monitor.h" #include "kis_stabilized_events_sampler.h" #include "KisStabilizerDelayedPaintHelper.h" #include "kis_config.h" #include //#define DEBUG_BEZIER_CURVES // Factor by which to scale the airbrush timer's interval, relative to the actual airbrushing rate. // Setting this less than 1 makes the timer-generated pseudo-events happen faster than the desired // airbrush rate, which can improve responsiveness. const qreal AIRBRUSH_INTERVAL_FACTOR = 0.5; // The amount of time, in milliseconds, to allow between updates of the spacing information. Only // used when spacing updates between dabs are enabled. const qreal SPACING_UPDATE_INTERVAL = 50.0; -// The amount of time, in milliseconds, to allow between updates of the spacing information. Only +// The amount of time, in milliseconds, to allow between updates of the timing information. Only // used when airbrushing. const qreal TIMING_UPDATE_INTERVAL = 50.0; struct KisToolFreehandHelper::Private { KisPaintingInformationBuilder *infoBuilder; KisRecordingAdapter *recordingAdapter; KisStrokesFacade *strokesFacade; KUndo2MagicString transactionText; bool haveTangent; QPointF previousTangent; bool hasPaintAtLeastOnce; QTime strokeTime; QTimer strokeTimeoutTimer; QVector painterInfos; KisResourcesSnapshotSP resources; KisStrokeId strokeId; KisPaintInformation previousPaintInformation; KisPaintInformation olderPaintInformation; KisSmoothingOptionsSP smoothingOptions; // Timer used to generate paint updates periodically even without input events. This is only // used for paintops that depend on timely updates even when the cursor is not moving, e.g. for // airbrushing effects. QTimer airbrushingTimer; QList history; QList distanceHistory; // Keeps track of past cursor positions. This is used to determine the drawing angle when // drawing the brush outline or starting a stroke. KisPaintOpUtils::PositionHistory lastCursorPos; // Stabilizer data QQueue stabilizerDeque; QTimer stabilizerPollTimer; KisStabilizedEventsSampler stabilizedSampler; KisStabilizerDelayedPaintHelper stabilizerDelayedPaintHelper; int canvasRotation; bool canvasMirroredH; qreal effectiveSmoothnessDistance() const; }; KisToolFreehandHelper::KisToolFreehandHelper(KisPaintingInformationBuilder *infoBuilder, const KUndo2MagicString &transactionText, KisRecordingAdapter *recordingAdapter, KisSmoothingOptions *smoothingOptions) : m_d(new Private()) { m_d->infoBuilder = infoBuilder; m_d->recordingAdapter = recordingAdapter; m_d->transactionText = transactionText; m_d->smoothingOptions = KisSmoothingOptionsSP( smoothingOptions ? smoothingOptions : new KisSmoothingOptions()); m_d->canvasRotation = 0; m_d->strokeTimeoutTimer.setSingleShot(true); connect(&m_d->strokeTimeoutTimer, SIGNAL(timeout()), SLOT(finishStroke())); connect(&m_d->airbrushingTimer, SIGNAL(timeout()), SLOT(doAirbrushing())); connect(&m_d->stabilizerPollTimer, SIGNAL(timeout()), SLOT(stabilizerPollAndPaint())); m_d->stabilizerDelayedPaintHelper.setPaintLineCallback( [this](const KisPaintInformation &pi1, const KisPaintInformation &pi2) { paintLine(pi1, pi2); }); m_d->stabilizerDelayedPaintHelper.setUpdateOutlineCallback( [this]() { emit requestExplicitUpdateOutline(); }); } KisToolFreehandHelper::~KisToolFreehandHelper() { delete m_d; } void KisToolFreehandHelper::setSmoothness(KisSmoothingOptionsSP smoothingOptions) { m_d->smoothingOptions = smoothingOptions; } KisSmoothingOptionsSP KisToolFreehandHelper::smoothingOptions() const { return m_d->smoothingOptions; } QPainterPath KisToolFreehandHelper::paintOpOutline(const QPointF &savedCursorPos, const KoPointerEvent *event, const KisPaintOpSettingsSP globalSettings, KisPaintOpSettings::OutlineMode mode) const { KisPaintOpSettingsSP settings = globalSettings; KisPaintInformation info = m_d->infoBuilder->hover(savedCursorPos, event); QPointF prevPoint = m_d->lastCursorPos.pushThroughHistory(savedCursorPos); qreal startAngle = KisAlgebra2D::directionBetweenPoints(prevPoint, savedCursorPos, 0); info.setCanvasRotation(m_d->canvasRotation); info.setCanvasHorizontalMirrorState( m_d->canvasMirroredH ); KisDistanceInformation distanceInfo(prevPoint, 0, startAngle); if (!m_d->painterInfos.isEmpty()) { settings = m_d->resources->currentPaintOpPreset()->settings(); if (m_d->stabilizerDelayedPaintHelper.running() && m_d->stabilizerDelayedPaintHelper.hasLastPaintInformation()) { info = m_d->stabilizerDelayedPaintHelper.lastPaintInformation(); } else { info = m_d->previousPaintInformation; } /** * When LoD mode is active it may happen that the helper has * already started a stroke, but it painted noting, because * all the work is being calculated by the scaled-down LodN * stroke. So at first we try to fetch the data from the lodN * stroke ("buddy") and then check if there is at least * something has been painted with this distance information * object. */ KisDistanceInformation *buddyDistance = m_d->painterInfos.first()->buddyDragDistance(); if (buddyDistance) { /** * Tiny hack alert: here we fetch the distance information * directly from the LodN stroke. Ideally, we should * upscale its data, but here we just override it with our * local copy of the coordinates. */ distanceInfo = *buddyDistance; distanceInfo.overrideLastValues(prevPoint, 0, startAngle); } else if (m_d->painterInfos.first()->dragDistance->isStarted()) { distanceInfo = *m_d->painterInfos.first()->dragDistance; } } KisPaintInformation::DistanceInformationRegistrar registrar = info.registerDistanceInformation(&distanceInfo); QPainterPath outline = settings->brushOutline(info, mode); if (m_d->resources && m_d->smoothingOptions->smoothingType() == KisSmoothingOptions::STABILIZER && m_d->smoothingOptions->useDelayDistance()) { const qreal R = m_d->smoothingOptions->delayDistance() / m_d->resources->effectiveZoom(); outline.addEllipse(info.pos(), R, R); } return outline; } void KisToolFreehandHelper::cursorMoved(const QPointF &cursorPos) { m_d->lastCursorPos.pushThroughHistory(cursorPos); } void KisToolFreehandHelper::initPaint(KoPointerEvent *event, const QPointF &pixelCoords, KoCanvasResourceManager *resourceManager, KisImageWSP image, KisNodeSP currentNode, KisStrokesFacade *strokesFacade, KisNodeSP overrideNode, KisDefaultBoundsBaseSP bounds) { QPointF prevPoint = m_d->lastCursorPos.pushThroughHistory(pixelCoords); m_d->strokeTime.start(); KisPaintInformation pi = m_d->infoBuilder->startStroke(event, elapsedStrokeTime(), resourceManager); qreal startAngle = KisAlgebra2D::directionBetweenPoints(prevPoint, pixelCoords, 0.0); initPaintImpl(startAngle, pi, resourceManager, image, currentNode, strokesFacade, overrideNode, bounds); } bool KisToolFreehandHelper::isRunning() const { return m_d->strokeId; } void KisToolFreehandHelper::initPaintImpl(qreal startAngle, const KisPaintInformation &pi, KoCanvasResourceManager *resourceManager, KisImageWSP image, KisNodeSP currentNode, KisStrokesFacade *strokesFacade, KisNodeSP overrideNode, KisDefaultBoundsBaseSP bounds) { m_d->strokesFacade = strokesFacade; m_d->haveTangent = false; m_d->previousTangent = QPointF(); m_d->hasPaintAtLeastOnce = false; m_d->previousPaintInformation = pi; m_d->resources = new KisResourcesSnapshot(image, currentNode, resourceManager, bounds); if(overrideNode) { m_d->resources->setCurrentNode(overrideNode); } const bool airbrushing = m_d->resources->needsAirbrushing(); const bool useSpacingUpdates = m_d->resources->needsSpacingUpdates(); KisDistanceInitInfo startDistInfo(m_d->previousPaintInformation.pos(), m_d->previousPaintInformation.currentTime(), startAngle, useSpacingUpdates ? SPACING_UPDATE_INTERVAL : LONG_TIME, airbrushing ? TIMING_UPDATE_INTERVAL : LONG_TIME); KisDistanceInformation startDist = startDistInfo.makeDistInfo(); createPainters(m_d->painterInfos, startDist); if(m_d->recordingAdapter) { m_d->recordingAdapter->startStroke(image, m_d->resources, startDistInfo); } KisStrokeStrategy *stroke = new FreehandStrokeStrategy(m_d->resources->needsIndirectPainting(), m_d->resources->indirectPaintingCompositeOp(), m_d->resources, m_d->painterInfos, m_d->transactionText); m_d->strokeId = m_d->strokesFacade->startStroke(stroke); m_d->history.clear(); m_d->distanceHistory.clear(); if(airbrushing) { m_d->airbrushingTimer.setInterval(computeAirbrushTimerInterval()); m_d->airbrushingTimer.start(); } if (m_d->smoothingOptions->smoothingType() == KisSmoothingOptions::STABILIZER) { stabilizerStart(m_d->previousPaintInformation); } // If airbrushing, paint an initial dab immediately. This is a workaround for an issue where // some paintops (Dyna, Particle, Sketch) might never initialize their spacing/timing // information until paintAt is called. if (airbrushing) { paintAt(pi); } } void KisToolFreehandHelper::paintBezierSegment(KisPaintInformation pi1, KisPaintInformation pi2, QPointF tangent1, QPointF tangent2) { if (tangent1.isNull() || tangent2.isNull()) return; const qreal maxSanePoint = 1e6; QPointF controlTarget1; QPointF controlTarget2; // Shows the direction in which control points go QPointF controlDirection1 = pi1.pos() + tangent1; QPointF controlDirection2 = pi2.pos() - tangent2; // Lines in the direction of the control points QLineF line1(pi1.pos(), controlDirection1); QLineF line2(pi2.pos(), controlDirection2); // Lines to check whether the control points lay on the opposite // side of the line QLineF line3(controlDirection1, controlDirection2); QLineF line4(pi1.pos(), pi2.pos()); QPointF intersection; if (line3.intersect(line4, &intersection) == QLineF::BoundedIntersection) { qreal controlLength = line4.length() / 2; line1.setLength(controlLength); line2.setLength(controlLength); controlTarget1 = line1.p2(); controlTarget2 = line2.p2(); } else { QLineF::IntersectType type = line1.intersect(line2, &intersection); if (type == QLineF::NoIntersection || intersection.manhattanLength() > maxSanePoint) { intersection = 0.5 * (pi1.pos() + pi2.pos()); // dbgKrita << "WARINING: there is no intersection point " // << "in the basic smoothing algoriths"; } controlTarget1 = intersection; controlTarget2 = intersection; } // shows how near to the controlTarget the value raises qreal coeff = 0.8; qreal velocity1 = QLineF(QPointF(), tangent1).length(); qreal velocity2 = QLineF(QPointF(), tangent2).length(); if (velocity1 == 0.0 || velocity2 == 0.0) { velocity1 = 1e-6; velocity2 = 1e-6; warnKrita << "WARNING: Basic Smoothing: Velocity is Zero! Please report a bug:" << ppVar(velocity1) << ppVar(velocity2); } qreal similarity = qMin(velocity1/velocity2, velocity2/velocity1); // the controls should not differ more than 50% similarity = qMax(similarity, qreal(0.5)); // when the controls are symmetric, their size should be smaller // to avoid corner-like curves coeff *= 1 - qMax(qreal(0.0), similarity - qreal(0.8)); Q_ASSERT(coeff > 0); QPointF control1; QPointF control2; if (velocity1 > velocity2) { control1 = pi1.pos() * (1.0 - coeff) + coeff * controlTarget1; coeff *= similarity; control2 = pi2.pos() * (1.0 - coeff) + coeff * controlTarget2; } else { control2 = pi2.pos() * (1.0 - coeff) + coeff * controlTarget2; coeff *= similarity; control1 = pi1.pos() * (1.0 - coeff) + coeff * controlTarget1; } paintBezierCurve(pi1, control1, control2, pi2); } qreal KisToolFreehandHelper::Private::effectiveSmoothnessDistance() const { const qreal effectiveSmoothnessDistance = !smoothingOptions->useScalableDistance() ? smoothingOptions->smoothnessDistance() : smoothingOptions->smoothnessDistance() / resources->effectiveZoom(); return effectiveSmoothnessDistance; } void KisToolFreehandHelper::paintEvent(KoPointerEvent *event) { KisPaintInformation info = m_d->infoBuilder->continueStroke(event, elapsedStrokeTime()); info.setCanvasRotation( m_d->canvasRotation ); info.setCanvasHorizontalMirrorState( m_d->canvasMirroredH ); KisUpdateTimeMonitor::instance()->reportMouseMove(info.pos()); paint(info); } void KisToolFreehandHelper::paint(KisPaintInformation &info) { /** * Smooth the coordinates out using the history and the * distance. This is a heavily modified version of an algo used in * Gimp and described in https://bugs.kde.org/show_bug.cgi?id=281267 and * http://www24.atwiki.jp/sigetch_2007/pages/17.html. The main * differences are: * * 1) It uses 'distance' instead of 'velocity', since time * measurements are too unstable in realworld environment * * 2) There is no 'Quality' parameter, since the number of samples * is calculated automatically * * 3) 'Tail Aggressiveness' is used for controling the end of the * stroke * * 4) The formila is a little bit different: 'Distance' parameter * stands for $3 \Sigma$ */ if (m_d->smoothingOptions->smoothingType() == KisSmoothingOptions::WEIGHTED_SMOOTHING && m_d->smoothingOptions->smoothnessDistance() > 0.0) { { // initialize current distance QPointF prevPos; if (!m_d->history.isEmpty()) { const KisPaintInformation &prevPi = m_d->history.last(); prevPos = prevPi.pos(); } else { prevPos = m_d->previousPaintInformation.pos(); } qreal currentDistance = QVector2D(info.pos() - prevPos).length(); m_d->distanceHistory.append(currentDistance); } m_d->history.append(info); qreal x = 0.0; qreal y = 0.0; if (m_d->history.size() > 3) { const qreal sigma = m_d->effectiveSmoothnessDistance() / 3.0; // '3.0' for (3 * sigma) range qreal gaussianWeight = 1 / (sqrt(2 * M_PI) * sigma); qreal gaussianWeight2 = sigma * sigma; qreal distanceSum = 0.0; qreal scaleSum = 0.0; qreal pressure = 0.0; qreal baseRate = 0.0; Q_ASSERT(m_d->history.size() == m_d->distanceHistory.size()); for (int i = m_d->history.size() - 1; i >= 0; i--) { qreal rate = 0.0; const KisPaintInformation nextInfo = m_d->history.at(i); double distance = m_d->distanceHistory.at(i); Q_ASSERT(distance >= 0.0); qreal pressureGrad = 0.0; if (i < m_d->history.size() - 1) { pressureGrad = nextInfo.pressure() - m_d->history.at(i + 1).pressure(); const qreal tailAgressiveness = 40.0 * m_d->smoothingOptions->tailAggressiveness(); if (pressureGrad > 0.0 ) { pressureGrad *= tailAgressiveness * (1.0 - nextInfo.pressure()); distance += pressureGrad * 3.0 * sigma; // (3 * sigma) --- holds > 90% of the region } } if (gaussianWeight2 != 0.0) { distanceSum += distance; rate = gaussianWeight * exp(-distanceSum * distanceSum / (2 * gaussianWeight2)); } if (m_d->history.size() - i == 1) { baseRate = rate; } else if (baseRate / rate > 100) { break; } scaleSum += rate; x += rate * nextInfo.pos().x(); y += rate * nextInfo.pos().y(); if (m_d->smoothingOptions->smoothPressure()) { pressure += rate * nextInfo.pressure(); } } if (scaleSum != 0.0) { x /= scaleSum; y /= scaleSum; if (m_d->smoothingOptions->smoothPressure()) { pressure /= scaleSum; } } if ((x != 0.0 && y != 0.0) || (x == info.pos().x() && y == info.pos().y())) { info.setPos(QPointF(x, y)); if (m_d->smoothingOptions->smoothPressure()) { info.setPressure(pressure); } m_d->history.last() = info; } } } if (m_d->smoothingOptions->smoothingType() == KisSmoothingOptions::SIMPLE_SMOOTHING || m_d->smoothingOptions->smoothingType() == KisSmoothingOptions::WEIGHTED_SMOOTHING) { // Now paint between the coordinates, using the bezier curve interpolation if (!m_d->haveTangent) { m_d->haveTangent = true; m_d->previousTangent = (info.pos() - m_d->previousPaintInformation.pos()) / qMax(qreal(1.0), info.currentTime() - m_d->previousPaintInformation.currentTime()); } else { QPointF newTangent = (info.pos() - m_d->olderPaintInformation.pos()) / qMax(qreal(1.0), info.currentTime() - m_d->olderPaintInformation.currentTime()); if (newTangent.isNull() || m_d->previousTangent.isNull()) { paintLine(m_d->previousPaintInformation, info); } else { paintBezierSegment(m_d->olderPaintInformation, m_d->previousPaintInformation, m_d->previousTangent, newTangent); } m_d->previousTangent = newTangent; } m_d->olderPaintInformation = m_d->previousPaintInformation; m_d->strokeTimeoutTimer.start(100); } else if (m_d->smoothingOptions->smoothingType() == KisSmoothingOptions::NO_SMOOTHING){ paintLine(m_d->previousPaintInformation, info); } if (m_d->smoothingOptions->smoothingType() == KisSmoothingOptions::STABILIZER) { m_d->stabilizedSampler.addEvent(info); } else { m_d->previousPaintInformation = info; } if(m_d->airbrushingTimer.isActive()) { m_d->airbrushingTimer.start(); } } void KisToolFreehandHelper::endPaint() { if (!m_d->hasPaintAtLeastOnce) { paintAt(m_d->previousPaintInformation); } else if (m_d->smoothingOptions->smoothingType() != KisSmoothingOptions::NO_SMOOTHING) { finishStroke(); } m_d->strokeTimeoutTimer.stop(); if(m_d->airbrushingTimer.isActive()) { m_d->airbrushingTimer.stop(); } if (m_d->smoothingOptions->smoothingType() == KisSmoothingOptions::STABILIZER) { stabilizerEnd(); } /** * There might be some timer events still pending, so * we should cancel them. Use this flag for the purpose. * Please note that we are not in MT here, so no mutex * is needed */ m_d->painterInfos.clear(); m_d->strokesFacade->endStroke(m_d->strokeId); m_d->strokeId.clear(); if(m_d->recordingAdapter) { m_d->recordingAdapter->endStroke(); } } void KisToolFreehandHelper::cancelPaint() { if (!m_d->strokeId) return; m_d->strokeTimeoutTimer.stop(); if (m_d->airbrushingTimer.isActive()) { m_d->airbrushingTimer.stop(); } if (m_d->stabilizerPollTimer.isActive()) { m_d->stabilizerPollTimer.stop(); } if (m_d->stabilizerDelayedPaintHelper.running()) { m_d->stabilizerDelayedPaintHelper.cancel(); } // see a comment in endPaint() m_d->painterInfos.clear(); m_d->strokesFacade->cancelStroke(m_d->strokeId); m_d->strokeId.clear(); if(m_d->recordingAdapter) { //FIXME: not implemented //m_d->recordingAdapter->cancelStroke(); } } int KisToolFreehandHelper::elapsedStrokeTime() const { return m_d->strokeTime.elapsed(); } void KisToolFreehandHelper::stabilizerStart(KisPaintInformation firstPaintInfo) { // FIXME: Ugly hack, this is no a "distance" in any way int sampleSize = qRound(m_d->effectiveSmoothnessDistance()); sampleSize = qMax(3, sampleSize); // Fill the deque with the current value repeated until filling the sample m_d->stabilizerDeque.clear(); for (int i = sampleSize; i > 0; i--) { m_d->stabilizerDeque.enqueue(firstPaintInfo); } // Poll and draw regularly KisConfig cfg; int stabilizerSampleSize = cfg.stabilizerSampleSize(); m_d->stabilizerPollTimer.setInterval(stabilizerSampleSize); m_d->stabilizerPollTimer.start(); int delayedPaintInterval = cfg.stabilizerDelayedPaintInterval(); if (delayedPaintInterval < stabilizerSampleSize) { m_d->stabilizerDelayedPaintHelper.start(delayedPaintInterval, firstPaintInfo); } m_d->stabilizedSampler.clear(); m_d->stabilizedSampler.addEvent(firstPaintInfo); } KisPaintInformation KisToolFreehandHelper::getStabilizedPaintInfo(const QQueue &queue, const KisPaintInformation &lastPaintInfo) { KisPaintInformation result(lastPaintInfo.pos(), lastPaintInfo.pressure(), lastPaintInfo.xTilt(), lastPaintInfo.yTilt(), lastPaintInfo.rotation(), lastPaintInfo.tangentialPressure(), lastPaintInfo.perspective(), elapsedStrokeTime(), lastPaintInfo.drawingSpeed()); if (queue.size() > 1) { QQueue::const_iterator it = queue.constBegin(); QQueue::const_iterator end = queue.constEnd(); /** * The first point is going to be overridden by lastPaintInfo, skip it. */ it++; int i = 2; if (m_d->smoothingOptions->stabilizeSensors()) { while (it != end) { qreal k = qreal(i - 1) / i; // coeff for uniform averaging result = KisPaintInformation::mixWithoutTime(k, *it, result); it++; i++; } } else{ while (it != end) { qreal k = qreal(i - 1) / i; // coeff for uniform averaging result = KisPaintInformation::mixOnlyPosition(k, *it, result); it++; i++; } } } return result; } void KisToolFreehandHelper::stabilizerPollAndPaint() { KisStabilizedEventsSampler::iterator it; KisStabilizedEventsSampler::iterator end; std::tie(it, end) = m_d->stabilizedSampler.range(); QVector delayedPaintTodoItems; for (; it != end; ++it) { KisPaintInformation sampledInfo = *it; bool canPaint = true; if (m_d->smoothingOptions->useDelayDistance()) { const qreal R = m_d->smoothingOptions->delayDistance() / m_d->resources->effectiveZoom(); QPointF diff = sampledInfo.pos() - m_d->previousPaintInformation.pos(); qreal dx = sqrt(pow2(diff.x()) + pow2(diff.y())); if (!(dx > R)) { if (m_d->resources->needsAirbrushing()) { sampledInfo.setPos(m_d->previousPaintInformation.pos()); } else { canPaint = false; } } } if (canPaint) { KisPaintInformation newInfo = getStabilizedPaintInfo(m_d->stabilizerDeque, sampledInfo); if (m_d->stabilizerDelayedPaintHelper.running()) { delayedPaintTodoItems.append(newInfo); } else { paintLine(m_d->previousPaintInformation, newInfo); } m_d->previousPaintInformation = newInfo; // Push the new entry through the queue m_d->stabilizerDeque.dequeue(); m_d->stabilizerDeque.enqueue(sampledInfo); } else if (m_d->stabilizerDeque.head().pos() != m_d->previousPaintInformation.pos()) { QQueue::iterator it = m_d->stabilizerDeque.begin(); QQueue::iterator end = m_d->stabilizerDeque.end(); while (it != end) { *it = m_d->previousPaintInformation; ++it; } } } m_d->stabilizedSampler.clear(); if (m_d->stabilizerDelayedPaintHelper.running()) { m_d->stabilizerDelayedPaintHelper.update(delayedPaintTodoItems); } else { emit requestExplicitUpdateOutline(); } } void KisToolFreehandHelper::stabilizerEnd() { // Stop the timer m_d->stabilizerPollTimer.stop(); // Finish the line if (m_d->smoothingOptions->finishStabilizedCurve()) { // Process all the existing events first stabilizerPollAndPaint(); // Draw the finish line with pending events and a time override m_d->stabilizedSampler.addFinishingEvent(m_d->stabilizerDeque.size()); stabilizerPollAndPaint(); } if (m_d->stabilizerDelayedPaintHelper.running()) { m_d->stabilizerDelayedPaintHelper.end(); } } const KisPaintOp* KisToolFreehandHelper::currentPaintOp() const { return !m_d->painterInfos.isEmpty() ? m_d->painterInfos.first()->painter->paintOp() : 0; } void KisToolFreehandHelper::finishStroke() { if (m_d->haveTangent) { m_d->haveTangent = false; QPointF newTangent = (m_d->previousPaintInformation.pos() - m_d->olderPaintInformation.pos()) / (m_d->previousPaintInformation.currentTime() - m_d->olderPaintInformation.currentTime()); paintBezierSegment(m_d->olderPaintInformation, m_d->previousPaintInformation, m_d->previousTangent, newTangent); } } void KisToolFreehandHelper::doAirbrushing() { // Check that the stroke hasn't ended. if (!m_d->painterInfos.isEmpty()) { // Add a new painting update at a point identical to the previous one, except for the time // and speed information. const KisPaintInformation &prevPaint = m_d->previousPaintInformation; KisPaintInformation nextPaint(prevPaint.pos(), prevPaint.pressure(), prevPaint.xTilt(), prevPaint.yTilt(), prevPaint.rotation(), prevPaint.tangentialPressure(), prevPaint.perspective(), elapsedStrokeTime(), 0.0); paint(nextPaint); } } int KisToolFreehandHelper::computeAirbrushTimerInterval() const { qreal realInterval = m_d->resources->airbrushingInterval() * AIRBRUSH_INTERVAL_FACTOR; return qMax(1, qFloor(realInterval)); } void KisToolFreehandHelper::paintAt(int painterInfoId, const KisPaintInformation &pi) { m_d->hasPaintAtLeastOnce = true; m_d->strokesFacade->addJob(m_d->strokeId, new FreehandStrokeStrategy::Data(m_d->resources->currentNode(), painterInfoId, pi)); if(m_d->recordingAdapter) { m_d->recordingAdapter->addPoint(pi); } } void KisToolFreehandHelper::paintLine(int painterInfoId, const KisPaintInformation &pi1, const KisPaintInformation &pi2) { m_d->hasPaintAtLeastOnce = true; m_d->strokesFacade->addJob(m_d->strokeId, new FreehandStrokeStrategy::Data(m_d->resources->currentNode(), painterInfoId, pi1, pi2)); if(m_d->recordingAdapter) { m_d->recordingAdapter->addLine(pi1, pi2); } } void KisToolFreehandHelper::paintBezierCurve(int painterInfoId, const KisPaintInformation &pi1, const QPointF &control1, const QPointF &control2, const KisPaintInformation &pi2) { #ifdef DEBUG_BEZIER_CURVES KisPaintInformation tpi1; KisPaintInformation tpi2; tpi1 = pi1; tpi2 = pi2; tpi1.setPressure(0.3); tpi2.setPressure(0.3); paintLine(tpi1, tpi2); tpi1.setPressure(0.6); tpi2.setPressure(0.3); tpi1.setPos(pi1.pos()); tpi2.setPos(control1); paintLine(tpi1, tpi2); tpi1.setPos(pi2.pos()); tpi2.setPos(control2); paintLine(tpi1, tpi2); #endif m_d->hasPaintAtLeastOnce = true; m_d->strokesFacade->addJob(m_d->strokeId, new FreehandStrokeStrategy::Data(m_d->resources->currentNode(), painterInfoId, pi1, control1, control2, pi2)); if(m_d->recordingAdapter) { m_d->recordingAdapter->addCurve(pi1, control1, control2, pi2); } } void KisToolFreehandHelper::createPainters(QVector &painterInfos, const KisDistanceInformation &startDist) { painterInfos << new PainterInfo(startDist); } void KisToolFreehandHelper::paintAt(const KisPaintInformation &pi) { paintAt(0, pi); } void KisToolFreehandHelper::paintLine(const KisPaintInformation &pi1, const KisPaintInformation &pi2) { paintLine(0, pi1, pi2); } void KisToolFreehandHelper::paintBezierCurve(const KisPaintInformation &pi1, const QPointF &control1, const QPointF &control2, const KisPaintInformation &pi2) { paintBezierCurve(0, pi1, control1, control2, pi2); } int KisToolFreehandHelper::canvasRotation() { return m_d->canvasRotation; } void KisToolFreehandHelper::setCanvasRotation(int rotation) { m_d->canvasRotation = rotation; } bool KisToolFreehandHelper::canvasMirroredH() { return m_d->canvasMirroredH; } void KisToolFreehandHelper::setCanvasHorizontalMirrorState(bool mirrored) { m_d->canvasMirroredH = mirrored; }