File indexing completed on 2024-12-22 04:13:00
0001 /* 0002 * SPDX-FileCopyrightText: 2011 Dmitry Kazakov <dimula73@gmail.com> 0003 * 0004 * SPDX-License-Identifier: GPL-2.0-or-later 0005 */ 0006 0007 #include "kis_tool_freehand_helper.h" 0008 0009 #include <QTimer> 0010 #include <QElapsedTimer> 0011 #include <QQueue> 0012 0013 #include <klocalizedstring.h> 0014 0015 #include <KoPointerEvent.h> 0016 #include <KoCanvasResourceProvider.h> 0017 0018 #include "kis_algebra_2d.h" 0019 #include "kis_distance_information.h" 0020 #include "kis_painting_information_builder.h" 0021 #include "kis_image.h" 0022 #include "kis_painter.h" 0023 #include <brushengine/kis_paintop_preset.h> 0024 #include <brushengine/kis_paintop_utils.h> 0025 0026 #include "kis_update_time_monitor.h" 0027 #include "kis_stabilized_events_sampler.h" 0028 #include "KisStabilizerDelayedPaintHelper.h" 0029 #include "kis_config.h" 0030 0031 #include "kis_random_source.h" 0032 #include "KisPerStrokeRandomSource.h" 0033 0034 #include "strokes/freehand_stroke.h" 0035 #include "strokes/KisFreehandStrokeInfo.h" 0036 #include "KisAsynchronousStrokeUpdateHelper.h" 0037 #include "kis_canvas_resource_provider.h" 0038 #include <KisOptimizedBrushOutline.h> 0039 0040 #include <math.h> 0041 0042 //#define DEBUG_BEZIER_CURVES 0043 0044 // Factor by which to scale the airbrush timer's interval, relative to the actual airbrushing rate. 0045 // Setting this less than 1 makes the timer-generated pseudo-events happen faster than the desired 0046 // airbrush rate, which can improve responsiveness. 0047 const qreal AIRBRUSH_INTERVAL_FACTOR = 0.5; 0048 0049 // The amount of time, in milliseconds, to allow between updates of the spacing information. Only 0050 // used when spacing updates between dabs are enabled. 0051 const qreal SPACING_UPDATE_INTERVAL = 50.0; 0052 0053 // The amount of time, in milliseconds, to allow between updates of the timing information. Only 0054 // used when airbrushing. 0055 const qreal TIMING_UPDATE_INTERVAL = 50.0; 0056 0057 struct KisToolFreehandHelper::Private 0058 { 0059 KoCanvasResourceProvider *resourceManager; 0060 KisPaintingInformationBuilder *infoBuilder; 0061 KisStrokesFacade *strokesFacade; 0062 KisAsynchronousStrokeUpdateHelper asyncUpdateHelper; 0063 0064 KUndo2MagicString transactionText; 0065 0066 bool haveTangent; 0067 QPointF previousTangent; 0068 0069 bool hasPaintAtLeastOnce; 0070 0071 QElapsedTimer strokeTime; 0072 QTimer strokeTimeoutTimer; 0073 0074 QVector<KisFreehandStrokeInfo*> strokeInfos; 0075 KisResourcesSnapshotSP resources; 0076 KisStrokeId strokeId; 0077 0078 KisPaintInformation previousPaintInformation; 0079 KisPaintInformation olderPaintInformation; 0080 0081 KisSmoothingOptionsSP smoothingOptions; 0082 0083 // fake random sources for hovering outline *only* 0084 KisRandomSourceSP fakeDabRandomSource; 0085 KisPerStrokeRandomSourceSP fakeStrokeRandomSource; 0086 0087 // Timer used to generate paint updates periodically even without input events. This is only 0088 // used for paintops that depend on timely updates even when the cursor is not moving, e.g. for 0089 // airbrushing effects. 0090 QTimer airbrushingTimer; 0091 0092 QList<KisPaintInformation> history; 0093 QList<qreal> distanceHistory; 0094 0095 // Keeps track of past cursor positions. This is used to determine the drawing angle when 0096 // drawing the brush outline or starting a stroke. 0097 KisPaintOpUtils::PositionHistory lastCursorPos; 0098 0099 // Stabilizer data 0100 bool usingStabilizer; 0101 QQueue<KisPaintInformation> stabilizerDeque; 0102 QTimer stabilizerPollTimer; 0103 KisStabilizedEventsSampler stabilizedSampler; 0104 KisStabilizerDelayedPaintHelper stabilizerDelayedPaintHelper; 0105 0106 qreal effectiveSmoothnessDistance() const; 0107 }; 0108 0109 0110 KisToolFreehandHelper::KisToolFreehandHelper(KisPaintingInformationBuilder *infoBuilder, 0111 KoCanvasResourceProvider *resourceManager, 0112 const KUndo2MagicString &transactionText, 0113 KisSmoothingOptions *smoothingOptions) 0114 : m_d(new Private()) 0115 { 0116 m_d->resourceManager = resourceManager; 0117 m_d->infoBuilder = infoBuilder; 0118 m_d->transactionText = transactionText; 0119 m_d->smoothingOptions = KisSmoothingOptionsSP( 0120 smoothingOptions ? smoothingOptions : new KisSmoothingOptions()); 0121 0122 m_d->fakeDabRandomSource = new KisRandomSource(); 0123 m_d->fakeStrokeRandomSource = new KisPerStrokeRandomSource(); 0124 0125 m_d->strokeTimeoutTimer.setSingleShot(true); 0126 connect(&m_d->strokeTimeoutTimer, SIGNAL(timeout()), SLOT(finishStroke())); 0127 connect(&m_d->airbrushingTimer, SIGNAL(timeout()), SLOT(doAirbrushing())); 0128 connect(&m_d->stabilizerPollTimer, SIGNAL(timeout()), SLOT(stabilizerPollAndPaint())); 0129 connect(m_d->smoothingOptions.data(), SIGNAL(sigSmoothingTypeChanged()), SLOT(slotSmoothingTypeChanged())); 0130 0131 m_d->stabilizerDelayedPaintHelper.setPaintLineCallback( 0132 [this](const KisPaintInformation &pi1, const KisPaintInformation &pi2) { 0133 paintLine(pi1, pi2); 0134 }); 0135 m_d->stabilizerDelayedPaintHelper.setUpdateOutlineCallback( 0136 [this]() { 0137 emit requestExplicitUpdateOutline(); 0138 }); 0139 } 0140 0141 KisToolFreehandHelper::~KisToolFreehandHelper() 0142 { 0143 delete m_d; 0144 } 0145 0146 void KisToolFreehandHelper::setSmoothness(KisSmoothingOptionsSP smoothingOptions) 0147 { 0148 m_d->smoothingOptions = smoothingOptions; 0149 } 0150 0151 KisSmoothingOptionsSP KisToolFreehandHelper::smoothingOptions() const 0152 { 0153 return m_d->smoothingOptions; 0154 } 0155 0156 KisOptimizedBrushOutline KisToolFreehandHelper::paintOpOutline(const QPointF &savedCursorPos, 0157 const KoPointerEvent *event, 0158 const KisPaintOpSettingsSP globalSettings, 0159 KisPaintOpSettings::OutlineMode mode) const 0160 { 0161 KisPaintOpSettingsSP settings = globalSettings; 0162 QPointF prevPoint = m_d->lastCursorPos.pushThroughHistory(savedCursorPos, currentZoom()); 0163 qreal startAngle = KisAlgebra2D::directionBetweenPoints(prevPoint, savedCursorPos, 0); 0164 KisDistanceInformation distanceInfo(prevPoint, startAngle); 0165 0166 KisPaintInformation info; 0167 0168 if (!m_d->strokeInfos.isEmpty()) { 0169 settings = m_d->resources->currentPaintOpPreset()->settings(); 0170 if (m_d->stabilizerDelayedPaintHelper.running() && 0171 m_d->stabilizerDelayedPaintHelper.hasLastPaintInformation()) { 0172 info = m_d->stabilizerDelayedPaintHelper.lastPaintInformation(); 0173 } else { 0174 info = m_d->previousPaintInformation; 0175 } 0176 0177 /** 0178 * When LoD mode is active it may happen that the helper has 0179 * already started a stroke, but it painted noting, because 0180 * all the work is being calculated by the scaled-down LodN 0181 * stroke. So at first we try to fetch the data from the lodN 0182 * stroke ("buddy") and then check if there is at least 0183 * something has been painted with this distance information 0184 * object. 0185 */ 0186 KisDistanceInformation *buddyDistance = 0187 m_d->strokeInfos.first()->buddyDragDistance(); 0188 0189 if (buddyDistance) { 0190 /** 0191 * Tiny hack alert: here we fetch the distance information 0192 * directly from the LodN stroke. Ideally, we should 0193 * upscale its data, but here we just override it with our 0194 * local copy of the coordinates. 0195 */ 0196 distanceInfo = *buddyDistance; 0197 distanceInfo.overrideLastValues(prevPoint, startAngle); 0198 0199 } else if (m_d->strokeInfos.first()->dragDistance->isStarted()) { 0200 distanceInfo = *m_d->strokeInfos.first()->dragDistance; 0201 } 0202 } else { 0203 info = m_d->infoBuilder->hover(savedCursorPos, event, !m_d->strokeInfos.isEmpty()); 0204 } 0205 0206 KisPaintInformation::DistanceInformationRegistrar registrar = 0207 info.registerDistanceInformation(&distanceInfo); 0208 0209 info.setRandomSource(m_d->fakeDabRandomSource); 0210 info.setPerStrokeRandomSource(m_d->fakeStrokeRandomSource); 0211 0212 KisOptimizedBrushOutline outline = settings->brushOutline(info, mode, currentPhysicalZoom()); 0213 0214 if (m_d->resources && 0215 m_d->smoothingOptions->smoothingType() == KisSmoothingOptions::STABILIZER && 0216 m_d->smoothingOptions->useDelayDistance()) { 0217 0218 const qreal R = m_d->smoothingOptions->delayDistance() / 0219 m_d->resources->effectiveZoom(); 0220 0221 outline.addEllipse(info.pos(), R, R); 0222 } 0223 0224 return outline; 0225 } 0226 0227 void KisToolFreehandHelper::cursorMoved(const QPointF &cursorPos) 0228 { 0229 m_d->lastCursorPos.pushThroughHistory(cursorPos, currentZoom()); 0230 } 0231 0232 void KisToolFreehandHelper::initPaint(KoPointerEvent *event, 0233 const QPointF &pixelCoords, 0234 KisImageWSP image, KisNodeSP currentNode, 0235 KisStrokesFacade *strokesFacade, 0236 KisNodeSP overrideNode, 0237 KisDefaultBoundsBaseSP bounds) 0238 { 0239 QPointF prevPoint = m_d->lastCursorPos.pushThroughHistory(pixelCoords, currentZoom()); 0240 m_d->strokeTime.start(); 0241 KisPaintInformation pi = 0242 m_d->infoBuilder->startStroke(event, elapsedStrokeTime(), m_d->resourceManager); 0243 qreal startAngle = KisAlgebra2D::directionBetweenPoints(prevPoint, pixelCoords, 0.0); 0244 0245 initPaintImpl(startAngle, 0246 pi, 0247 m_d->resourceManager, 0248 image, 0249 currentNode, 0250 strokesFacade, 0251 overrideNode, 0252 bounds); 0253 } 0254 0255 bool KisToolFreehandHelper::isRunning() const 0256 { 0257 return m_d->strokeId; 0258 } 0259 0260 void KisToolFreehandHelper::initPaintImpl(qreal startAngle, 0261 const KisPaintInformation &pi, 0262 KoCanvasResourceProvider *resourceManager, 0263 KisImageWSP image, 0264 KisNodeSP currentNode, 0265 KisStrokesFacade *strokesFacade, 0266 KisNodeSP overrideNode, 0267 KisDefaultBoundsBaseSP bounds) 0268 { 0269 m_d->strokesFacade = strokesFacade; 0270 0271 m_d->haveTangent = false; 0272 m_d->previousTangent = QPointF(); 0273 0274 m_d->hasPaintAtLeastOnce = false; 0275 0276 m_d->previousPaintInformation = pi; 0277 0278 m_d->resources = new KisResourcesSnapshot(image, 0279 currentNode, 0280 resourceManager, 0281 bounds); 0282 if(overrideNode) { 0283 m_d->resources->setCurrentNode(overrideNode); 0284 } 0285 0286 const bool airbrushing = m_d->resources->needsAirbrushing(); 0287 const bool useSpacingUpdates = m_d->resources->needsSpacingUpdates(); 0288 0289 KisDistanceInitInfo startDistInfo(m_d->previousPaintInformation.pos(), 0290 startAngle, 0291 useSpacingUpdates ? SPACING_UPDATE_INTERVAL : LONG_TIME, 0292 airbrushing ? TIMING_UPDATE_INTERVAL : LONG_TIME, 0293 0); 0294 KisDistanceInformation startDist = startDistInfo.makeDistInfo(); 0295 0296 createPainters(m_d->strokeInfos, 0297 startDist); 0298 0299 KisStrokeStrategy *stroke = 0300 new FreehandStrokeStrategy(m_d->resources, 0301 m_d->strokeInfos, 0302 m_d->transactionText, 0303 FreehandStrokeStrategy::SupportsContinuedInterstrokeData | 0304 FreehandStrokeStrategy::SupportsTimedMergeId); 0305 0306 m_d->strokeId = m_d->strokesFacade->startStroke(stroke); 0307 0308 m_d->history.clear(); 0309 m_d->distanceHistory.clear(); 0310 0311 if (airbrushing) { 0312 m_d->airbrushingTimer.setInterval(computeAirbrushTimerInterval()); 0313 m_d->airbrushingTimer.start(); 0314 } else if (m_d->resources->presetNeedsAsynchronousUpdates()) { 0315 m_d->asyncUpdateHelper.startUpdateStream(m_d->strokesFacade, m_d->strokeId); 0316 } 0317 0318 if (m_d->smoothingOptions->smoothingType() == KisSmoothingOptions::STABILIZER) { 0319 stabilizerStart(m_d->previousPaintInformation); 0320 } 0321 0322 // If airbrushing, paint an initial dab immediately. This is a workaround for an issue where 0323 // some paintops (Dyna, Particle, Sketch) might never initialize their spacing/timing 0324 // information until paintAt is called. 0325 if (airbrushing) { 0326 paintAt(pi); 0327 } 0328 } 0329 0330 KoCanvasResourceProvider *KisToolFreehandHelper::resourceManager() const 0331 { 0332 return m_d->resourceManager; 0333 } 0334 0335 void KisToolFreehandHelper::paintBezierSegment(KisPaintInformation pi1, KisPaintInformation pi2, 0336 QPointF tangent1, QPointF tangent2) 0337 { 0338 if (tangent1.isNull() || tangent2.isNull()) return; 0339 0340 const qreal maxSanePoint = 1e6; 0341 0342 QPointF controlTarget1; 0343 QPointF controlTarget2; 0344 0345 // Shows the direction in which control points go 0346 QPointF controlDirection1 = pi1.pos() + tangent1; 0347 QPointF controlDirection2 = pi2.pos() - tangent2; 0348 0349 // Lines in the direction of the control points 0350 QLineF line1(pi1.pos(), controlDirection1); 0351 QLineF line2(pi2.pos(), controlDirection2); 0352 0353 // Lines to check whether the control points lay on the opposite 0354 // side of the line 0355 QLineF line3(controlDirection1, controlDirection2); 0356 QLineF line4(pi1.pos(), pi2.pos()); 0357 0358 QPointF intersection; 0359 if (line3.intersect(line4, &intersection) == QLineF::BoundedIntersection) { 0360 qreal controlLength = line4.length() / 2; 0361 0362 line1.setLength(controlLength); 0363 line2.setLength(controlLength); 0364 0365 controlTarget1 = line1.p2(); 0366 controlTarget2 = line2.p2(); 0367 } else { 0368 QLineF::IntersectType type = line1.intersect(line2, &intersection); 0369 0370 if (type == QLineF::NoIntersection || 0371 intersection.manhattanLength() > maxSanePoint) { 0372 0373 intersection = 0.5 * (pi1.pos() + pi2.pos()); 0374 // dbgKrita << "WARNING: there is no intersection point " 0375 // << "in the basic smoothing algorithms"; 0376 } 0377 0378 controlTarget1 = intersection; 0379 controlTarget2 = intersection; 0380 } 0381 0382 // shows how near to the controlTarget the value raises 0383 qreal coeff = 0.8; 0384 0385 qreal velocity1 = QLineF(QPointF(), tangent1).length(); 0386 qreal velocity2 = QLineF(QPointF(), tangent2).length(); 0387 0388 if (velocity1 == 0.0 || velocity2 == 0.0) { 0389 velocity1 = 1e-6; 0390 velocity2 = 1e-6; 0391 warnKrita << "WARNING: Basic Smoothing: Velocity is Zero! Please report a bug:" << ppVar(velocity1) << ppVar(velocity2); 0392 } 0393 0394 qreal similarity = qMin(velocity1/velocity2, velocity2/velocity1); 0395 0396 // the controls should not differ more than 50% 0397 similarity = qMax(similarity, qreal(0.5)); 0398 0399 // when the controls are symmetric, their size should be smaller 0400 // to avoid corner-like curves 0401 coeff *= 1 - qMax(qreal(0.0), similarity - qreal(0.8)); 0402 0403 Q_ASSERT(coeff > 0); 0404 0405 0406 QPointF control1; 0407 QPointF control2; 0408 0409 if (velocity1 > velocity2) { 0410 control1 = pi1.pos() * (1.0 - coeff) + coeff * controlTarget1; 0411 coeff *= similarity; 0412 control2 = pi2.pos() * (1.0 - coeff) + coeff * controlTarget2; 0413 } else { 0414 control2 = pi2.pos() * (1.0 - coeff) + coeff * controlTarget2; 0415 coeff *= similarity; 0416 control1 = pi1.pos() * (1.0 - coeff) + coeff * controlTarget1; 0417 } 0418 0419 paintBezierCurve(pi1, 0420 control1, 0421 control2, 0422 pi2); 0423 } 0424 0425 qreal KisToolFreehandHelper::Private::effectiveSmoothnessDistance() const 0426 { 0427 qreal zoomingCoeff = 1.0; 0428 0429 /// stabilizer has inverted meaning of the "scalable distance", because 0430 /// it measures "samples", but not "distance" 0431 0432 if ((smoothingOptions->smoothingType() == KisSmoothingOptions::STABILIZER) ^ 0433 smoothingOptions->useScalableDistance()) { 0434 0435 zoomingCoeff = 1.0 / resources->effectiveZoom(); 0436 } 0437 0438 return smoothingOptions->smoothnessDistance() * zoomingCoeff; 0439 } 0440 0441 void KisToolFreehandHelper::paintEvent(KoPointerEvent *event) 0442 { 0443 KisPaintInformation info = 0444 m_d->infoBuilder->continueStroke(event, 0445 elapsedStrokeTime()); 0446 KisUpdateTimeMonitor::instance()->reportMouseMove(info.pos()); 0447 0448 paint(info); 0449 } 0450 0451 void KisToolFreehandHelper::paint(KisPaintInformation &info) 0452 { 0453 /** 0454 * Smooth the coordinates out using the history and the 0455 * distance. This is a heavily modified version of an algo used in 0456 * Gimp and described in https://bugs.kde.org/show_bug.cgi?id=281267 and 0457 * https://w.atwiki.jp/sigetch_2007/pages/17.html. The main 0458 * differences are: 0459 * 0460 * 1) It uses 'distance' instead of 'velocity', since time 0461 * measurements are too unstable in realworld environment 0462 * 0463 * 2) There is no 'Quality' parameter, since the number of samples 0464 * is calculated automatically 0465 * 0466 * 3) 'Tail Aggressiveness' is used for controlling the end of the 0467 * stroke 0468 * 0469 * 4) The formula is a little bit different: 'Distance' parameter 0470 * stands for $3 \Sigma$ 0471 */ 0472 if (m_d->smoothingOptions->smoothingType() == KisSmoothingOptions::WEIGHTED_SMOOTHING 0473 && m_d->smoothingOptions->smoothnessDistance() > 0.0) { 0474 0475 { // initialize current distance 0476 QPointF prevPos; 0477 0478 if (!m_d->history.isEmpty()) { 0479 const KisPaintInformation &prevPi = m_d->history.last(); 0480 prevPos = prevPi.pos(); 0481 } else { 0482 prevPos = m_d->previousPaintInformation.pos(); 0483 } 0484 0485 qreal currentDistance = QVector2D(info.pos() - prevPos).length(); 0486 m_d->distanceHistory.append(currentDistance); 0487 } 0488 0489 m_d->history.append(info); 0490 0491 qreal x = 0.0; 0492 qreal y = 0.0; 0493 0494 if (m_d->history.size() > 3) { 0495 const qreal sigma = m_d->effectiveSmoothnessDistance() / 3.0; // '3.0' for (3 * sigma) range 0496 0497 qreal gaussianWeight = 1 / (sqrt(2 * M_PI) * sigma); 0498 qreal gaussianWeight2 = sigma * sigma; 0499 qreal distanceSum = 0.0; 0500 qreal scaleSum = 0.0; 0501 qreal pressure = 0.0; 0502 qreal baseRate = 0.0; 0503 0504 Q_ASSERT(m_d->history.size() == m_d->distanceHistory.size()); 0505 0506 for (int i = m_d->history.size() - 1; i >= 0; i--) { 0507 qreal rate = 0.0; 0508 0509 const KisPaintInformation nextInfo = m_d->history.at(i); 0510 double distance = m_d->distanceHistory.at(i); 0511 Q_ASSERT(distance >= 0.0); 0512 0513 qreal pressureGrad = 0.0; 0514 if (i < m_d->history.size() - 1) { 0515 pressureGrad = nextInfo.pressure() - m_d->history.at(i + 1).pressure(); 0516 0517 const qreal tailAggressiveness = 40.0 * m_d->smoothingOptions->tailAggressiveness(); 0518 0519 if (pressureGrad > 0.0 ) { 0520 pressureGrad *= tailAggressiveness * (1.0 - nextInfo.pressure()); 0521 distance += pressureGrad * 3.0 * sigma; // (3 * sigma) --- holds > 90% of the region 0522 } 0523 } 0524 0525 if (gaussianWeight2 != 0.0) { 0526 distanceSum += distance; 0527 rate = gaussianWeight * exp(-distanceSum * distanceSum / (2 * gaussianWeight2)); 0528 } 0529 0530 if (m_d->history.size() - i == 1) { 0531 baseRate = rate; 0532 } else if (baseRate / rate > 100) { 0533 break; 0534 } 0535 0536 scaleSum += rate; 0537 x += rate * nextInfo.pos().x(); 0538 y += rate * nextInfo.pos().y(); 0539 0540 if (m_d->smoothingOptions->smoothPressure()) { 0541 pressure += rate * nextInfo.pressure(); 0542 } 0543 } 0544 0545 if (scaleSum != 0.0) { 0546 x /= scaleSum; 0547 y /= scaleSum; 0548 0549 if (m_d->smoothingOptions->smoothPressure()) { 0550 pressure /= scaleSum; 0551 } 0552 } 0553 0554 if ((x != 0.0 && y != 0.0) || (x == info.pos().x() && y == info.pos().y())) { 0555 info.setPos(QPointF(x, y)); 0556 if (m_d->smoothingOptions->smoothPressure()) { 0557 info.setPressure(pressure); 0558 } 0559 m_d->history.last() = info; 0560 } 0561 } 0562 } 0563 0564 if (m_d->smoothingOptions->smoothingType() == KisSmoothingOptions::SIMPLE_SMOOTHING 0565 || m_d->smoothingOptions->smoothingType() == KisSmoothingOptions::WEIGHTED_SMOOTHING) 0566 { 0567 // Now paint between the coordinates, using the bezier curve interpolation 0568 if (!m_d->haveTangent) { 0569 m_d->haveTangent = true; 0570 m_d->previousTangent = 0571 (info.pos() - m_d->previousPaintInformation.pos()) / 0572 qMax(qreal(1.0), info.currentTime() - m_d->previousPaintInformation.currentTime()); 0573 } else { 0574 QPointF newTangent = (info.pos() - m_d->olderPaintInformation.pos()) / 0575 qMax(qreal(1.0), info.currentTime() - m_d->olderPaintInformation.currentTime()); 0576 0577 if (newTangent.isNull() || m_d->previousTangent.isNull()) 0578 { 0579 paintLine(m_d->previousPaintInformation, info); 0580 } else { 0581 paintBezierSegment(m_d->olderPaintInformation, m_d->previousPaintInformation, 0582 m_d->previousTangent, newTangent); 0583 } 0584 0585 m_d->previousTangent = newTangent; 0586 } 0587 m_d->olderPaintInformation = m_d->previousPaintInformation; 0588 0589 // Enable stroke timeout only when not airbrushing. 0590 if (!m_d->airbrushingTimer.isActive()) { 0591 m_d->strokeTimeoutTimer.start(100); 0592 } 0593 } 0594 else if (m_d->smoothingOptions->smoothingType() == KisSmoothingOptions::NO_SMOOTHING){ 0595 paintLine(m_d->previousPaintInformation, info); 0596 } 0597 0598 if (m_d->smoothingOptions->smoothingType() == KisSmoothingOptions::STABILIZER) { 0599 m_d->stabilizedSampler.addEvent(info); 0600 if (m_d->stabilizerDelayedPaintHelper.running()) { 0601 // Paint here so we don't have to rely on the timer 0602 // This is just a tricky source for a relatively stable 7ms "timer" 0603 m_d->stabilizerDelayedPaintHelper.paintSome(); 0604 } 0605 } else { 0606 m_d->previousPaintInformation = info; 0607 } 0608 0609 if(m_d->airbrushingTimer.isActive()) { 0610 m_d->airbrushingTimer.start(); 0611 } 0612 } 0613 0614 void KisToolFreehandHelper::endPaint() 0615 { 0616 if (!m_d->hasPaintAtLeastOnce) { 0617 paintAt(m_d->previousPaintInformation); 0618 } else if (m_d->smoothingOptions->smoothingType() != KisSmoothingOptions::NO_SMOOTHING) { 0619 finishStroke(); 0620 } 0621 m_d->strokeTimeoutTimer.stop(); 0622 0623 if(m_d->airbrushingTimer.isActive()) { 0624 m_d->airbrushingTimer.stop(); 0625 } 0626 0627 if (m_d->smoothingOptions->smoothingType() == KisSmoothingOptions::STABILIZER) { 0628 stabilizerEnd(); 0629 } 0630 0631 if (m_d->asyncUpdateHelper.isActive()) { 0632 m_d->asyncUpdateHelper.endUpdateStream(); 0633 } 0634 0635 /** 0636 * There might be some timer events still pending, so 0637 * we should cancel them. Use this flag for the purpose. 0638 * Please note that we are not in MT here, so no mutex 0639 * is needed 0640 */ 0641 m_d->strokeInfos.clear(); 0642 0643 // last update to complete rendering if there is still something pending 0644 m_d->strokesFacade->addJob(m_d->strokeId, 0645 new KisAsynchronousStrokeUpdateHelper::UpdateData(true)); 0646 0647 m_d->strokesFacade->endStroke(m_d->strokeId); 0648 m_d->strokeId.clear(); 0649 m_d->infoBuilder->reset(); 0650 } 0651 0652 void KisToolFreehandHelper::cancelPaint() 0653 { 0654 if (!m_d->strokeId) return; 0655 0656 m_d->strokeTimeoutTimer.stop(); 0657 0658 if (m_d->airbrushingTimer.isActive()) { 0659 m_d->airbrushingTimer.stop(); 0660 } 0661 0662 if (m_d->asyncUpdateHelper.isActive()) { 0663 m_d->asyncUpdateHelper.cancelUpdateStream(); 0664 } 0665 0666 if (m_d->stabilizerPollTimer.isActive()) { 0667 m_d->stabilizerPollTimer.stop(); 0668 } 0669 0670 if (m_d->stabilizerDelayedPaintHelper.running()) { 0671 m_d->stabilizerDelayedPaintHelper.cancel(); 0672 } 0673 0674 // see a comment in endPaint() 0675 m_d->strokeInfos.clear(); 0676 0677 m_d->strokesFacade->cancelStroke(m_d->strokeId); 0678 m_d->strokeId.clear(); 0679 0680 } 0681 0682 int KisToolFreehandHelper::elapsedStrokeTime() const 0683 { 0684 return m_d->strokeTime.elapsed(); 0685 } 0686 0687 void KisToolFreehandHelper::stabilizerStart(KisPaintInformation firstPaintInfo) 0688 { 0689 m_d->usingStabilizer = true; 0690 // FIXME: Ugly hack, this is no a "distance" in any way 0691 int sampleSize = qRound(m_d->effectiveSmoothnessDistance()); 0692 sampleSize = qMax(3, sampleSize); 0693 0694 // Fill the deque with the current value repeated until filling the sample 0695 m_d->stabilizerDeque.clear(); 0696 for (int i = sampleSize; i > 0; i--) { 0697 m_d->stabilizerDeque.enqueue(firstPaintInfo); 0698 } 0699 0700 // Poll and draw regularly 0701 KisConfig cfg(true); 0702 int stabilizerSampleSize = cfg.stabilizerSampleSize(); 0703 m_d->stabilizerPollTimer.setInterval(stabilizerSampleSize); 0704 m_d->stabilizerPollTimer.start(); 0705 0706 bool delayedPaintEnabled = cfg.stabilizerDelayedPaint(); 0707 if (delayedPaintEnabled) { 0708 m_d->stabilizerDelayedPaintHelper.start(firstPaintInfo); 0709 } 0710 0711 m_d->stabilizedSampler.clear(); 0712 m_d->stabilizedSampler.addEvent(firstPaintInfo); 0713 } 0714 0715 KisPaintInformation 0716 KisToolFreehandHelper::getStabilizedPaintInfo(const QQueue<KisPaintInformation> &queue, 0717 const KisPaintInformation &lastPaintInfo) 0718 { 0719 KisPaintInformation result(lastPaintInfo.pos(), 0720 lastPaintInfo.pressure(), 0721 lastPaintInfo.xTilt(), 0722 lastPaintInfo.yTilt(), 0723 lastPaintInfo.rotation(), 0724 lastPaintInfo.tangentialPressure(), 0725 lastPaintInfo.perspective(), 0726 elapsedStrokeTime(), 0727 lastPaintInfo.drawingSpeed()); 0728 0729 result.setCanvasRotation(lastPaintInfo.canvasRotation()); 0730 result.setCanvasMirroredH(lastPaintInfo.canvasMirroredH()); 0731 result.setCanvasMirroredV(lastPaintInfo.canvasMirroredV()); 0732 0733 if (queue.size() > 1) { 0734 QQueue<KisPaintInformation>::const_iterator it = queue.constBegin(); 0735 QQueue<KisPaintInformation>::const_iterator end = queue.constEnd(); 0736 0737 /** 0738 * The first point is going to be overridden by lastPaintInfo, skip it. 0739 */ 0740 it++; 0741 int i = 2; 0742 0743 if (m_d->smoothingOptions->stabilizeSensors()) { 0744 while (it != end) { 0745 qreal k = qreal(i - 1) / i; // coeff for uniform averaging 0746 result.KisPaintInformation::mixOtherWithoutTime(k, *it); 0747 it++; 0748 i++; 0749 } 0750 } else{ 0751 while (it != end) { 0752 qreal k = qreal(i - 1) / i; // coeff for uniform averaging 0753 result.KisPaintInformation::mixOtherOnlyPosition(k, *it); 0754 it++; 0755 i++; 0756 } 0757 } 0758 } 0759 0760 return result; 0761 } 0762 0763 void KisToolFreehandHelper::stabilizerPollAndPaint() 0764 { 0765 KisStabilizedEventsSampler::iterator it; 0766 KisStabilizedEventsSampler::iterator end; 0767 std::tie(it, end) = m_d->stabilizedSampler.range(); 0768 QVector<KisPaintInformation> delayedPaintTodoItems; 0769 0770 for (; it != end; ++it) { 0771 KisPaintInformation sampledInfo = *it; 0772 0773 bool canPaint = true; 0774 0775 if (m_d->smoothingOptions->useDelayDistance()) { 0776 const qreal R = m_d->smoothingOptions->delayDistance() / 0777 m_d->resources->effectiveZoom(); 0778 0779 QPointF diff = sampledInfo.pos() - m_d->previousPaintInformation.pos(); 0780 qreal dx = sqrt(pow2(diff.x()) + pow2(diff.y())); 0781 0782 if (!(dx > R)) { 0783 if (m_d->resources->needsAirbrushing()) { 0784 sampledInfo.setPos(m_d->previousPaintInformation.pos()); 0785 } 0786 else { 0787 canPaint = false; 0788 } 0789 } 0790 } 0791 0792 if (canPaint) { 0793 KisPaintInformation newInfo = getStabilizedPaintInfo(m_d->stabilizerDeque, sampledInfo); 0794 0795 if (m_d->stabilizerDelayedPaintHelper.running()) { 0796 delayedPaintTodoItems.append(newInfo); 0797 } else { 0798 paintLine(m_d->previousPaintInformation, newInfo); 0799 } 0800 m_d->previousPaintInformation = newInfo; 0801 0802 // Push the new entry through the queue 0803 m_d->stabilizerDeque.dequeue(); 0804 m_d->stabilizerDeque.enqueue(sampledInfo); 0805 } else if (m_d->stabilizerDeque.head().pos() != m_d->previousPaintInformation.pos()) { 0806 0807 QQueue<KisPaintInformation>::iterator it = m_d->stabilizerDeque.begin(); 0808 QQueue<KisPaintInformation>::iterator end = m_d->stabilizerDeque.end(); 0809 0810 while (it != end) { 0811 *it = m_d->previousPaintInformation; 0812 ++it; 0813 } 0814 } 0815 } 0816 0817 m_d->stabilizedSampler.clear(); 0818 0819 if (m_d->stabilizerDelayedPaintHelper.running()) { 0820 m_d->stabilizerDelayedPaintHelper.update(delayedPaintTodoItems); 0821 } else { 0822 emit requestExplicitUpdateOutline(); 0823 } 0824 } 0825 0826 void KisToolFreehandHelper::stabilizerEnd() 0827 { 0828 // Stop the timer 0829 m_d->stabilizerPollTimer.stop(); 0830 0831 // Finish the line 0832 if (m_d->smoothingOptions->finishStabilizedCurve()) { 0833 // Process all the existing events first 0834 stabilizerPollAndPaint(); 0835 0836 // Draw the finish line with pending events and a time override 0837 m_d->stabilizedSampler.addFinishingEvent(m_d->stabilizerDeque.size()); 0838 stabilizerPollAndPaint(); 0839 } 0840 0841 if (m_d->stabilizerDelayedPaintHelper.running()) { 0842 m_d->stabilizerDelayedPaintHelper.end(); 0843 } 0844 m_d->usingStabilizer = false; 0845 } 0846 0847 void KisToolFreehandHelper::slotSmoothingTypeChanged() 0848 { 0849 if (!isRunning()) { 0850 return; 0851 } 0852 KisSmoothingOptions::SmoothingType currentSmoothingType = 0853 m_d->smoothingOptions->smoothingType(); 0854 if (m_d->usingStabilizer 0855 && (currentSmoothingType != KisSmoothingOptions::STABILIZER)) { 0856 stabilizerEnd(); 0857 } else if (!m_d->usingStabilizer 0858 && (currentSmoothingType == KisSmoothingOptions::STABILIZER)) { 0859 stabilizerStart(m_d->previousPaintInformation); 0860 } 0861 } 0862 0863 void KisToolFreehandHelper::finishStroke() 0864 { 0865 if (m_d->haveTangent) { 0866 m_d->haveTangent = false; 0867 0868 QPointF newTangent = (m_d->previousPaintInformation.pos() - m_d->olderPaintInformation.pos()) / 0869 (m_d->previousPaintInformation.currentTime() - m_d->olderPaintInformation.currentTime()); 0870 0871 paintBezierSegment(m_d->olderPaintInformation, 0872 m_d->previousPaintInformation, 0873 m_d->previousTangent, 0874 newTangent); 0875 } 0876 } 0877 0878 void KisToolFreehandHelper::doAirbrushing() 0879 { 0880 // Check that the stroke hasn't ended. 0881 if (!m_d->strokeInfos.isEmpty()) { 0882 0883 // Add a new painting update at a point identical to the previous one, except for the time 0884 // and speed information. 0885 const KisPaintInformation &prevPaint = m_d->previousPaintInformation; 0886 KisPaintInformation nextPaint(prevPaint.pos(), 0887 prevPaint.pressure(), 0888 prevPaint.xTilt(), 0889 prevPaint.yTilt(), 0890 prevPaint.rotation(), 0891 prevPaint.tangentialPressure(), 0892 prevPaint.perspective(), 0893 elapsedStrokeTime(), 0894 0.0); 0895 nextPaint.setCanvasRotation(prevPaint.canvasRotation()); 0896 nextPaint.setCanvasMirroredH(prevPaint.canvasMirroredH()); 0897 nextPaint.setCanvasMirroredV(prevPaint.canvasMirroredV()); 0898 paint(nextPaint); 0899 } 0900 } 0901 0902 int KisToolFreehandHelper::computeAirbrushTimerInterval() const 0903 { 0904 qreal realInterval = m_d->resources->airbrushingInterval() * AIRBRUSH_INTERVAL_FACTOR; 0905 return qMax(1, qFloor(realInterval)); 0906 } 0907 0908 qreal KisToolFreehandHelper::currentZoom() const 0909 { 0910 return m_d->resourceManager ? m_d->resourceManager->resource(KoCanvasResource::EffectiveZoom).toReal() : 1.0; 0911 } 0912 0913 qreal KisToolFreehandHelper::currentPhysicalZoom() const 0914 { 0915 return m_d->resourceManager ? m_d->resourceManager->resource(KoCanvasResource::EffectivePhysicalZoom).toReal() : 1.0; 0916 } 0917 0918 void KisToolFreehandHelper::paintAt(int strokeInfoId, 0919 const KisPaintInformation &pi) 0920 { 0921 m_d->hasPaintAtLeastOnce = true; 0922 m_d->strokesFacade->addJob(m_d->strokeId, 0923 new FreehandStrokeStrategy::Data(strokeInfoId, pi)); 0924 0925 } 0926 0927 void KisToolFreehandHelper::paintLine(int strokeInfoId, 0928 const KisPaintInformation &pi1, 0929 const KisPaintInformation &pi2) 0930 { 0931 m_d->hasPaintAtLeastOnce = true; 0932 m_d->strokesFacade->addJob(m_d->strokeId, 0933 new FreehandStrokeStrategy::Data(strokeInfoId, pi1, pi2)); 0934 0935 } 0936 0937 void KisToolFreehandHelper::paintBezierCurve(int strokeInfoId, 0938 const KisPaintInformation &pi1, 0939 const QPointF &control1, 0940 const QPointF &control2, 0941 const KisPaintInformation &pi2) 0942 { 0943 #ifdef DEBUG_BEZIER_CURVES 0944 KisPaintInformation tpi1; 0945 KisPaintInformation tpi2; 0946 0947 tpi1 = pi1; 0948 tpi2 = pi2; 0949 0950 tpi1.setPressure(0.3); 0951 tpi2.setPressure(0.3); 0952 0953 paintLine(tpi1, tpi2); 0954 0955 tpi1.setPressure(0.6); 0956 tpi2.setPressure(0.3); 0957 0958 tpi1.setPos(pi1.pos()); 0959 tpi2.setPos(control1); 0960 paintLine(tpi1, tpi2); 0961 0962 tpi1.setPos(pi2.pos()); 0963 tpi2.setPos(control2); 0964 paintLine(tpi1, tpi2); 0965 #endif 0966 0967 m_d->hasPaintAtLeastOnce = true; 0968 m_d->strokesFacade->addJob(m_d->strokeId, 0969 new FreehandStrokeStrategy::Data(strokeInfoId, 0970 pi1, control1, control2, pi2)); 0971 0972 } 0973 0974 void KisToolFreehandHelper::createPainters(QVector<KisFreehandStrokeInfo*> &strokeInfos, 0975 const KisDistanceInformation &startDist) 0976 { 0977 strokeInfos << new KisFreehandStrokeInfo(startDist); 0978 } 0979 0980 void KisToolFreehandHelper::paintAt(const KisPaintInformation &pi) 0981 { 0982 paintAt(0, pi); 0983 } 0984 0985 void KisToolFreehandHelper::paintLine(const KisPaintInformation &pi1, 0986 const KisPaintInformation &pi2) 0987 { 0988 paintLine(0, pi1, pi2); 0989 } 0990 0991 void KisToolFreehandHelper::paintBezierCurve(const KisPaintInformation &pi1, 0992 const QPointF &control1, 0993 const QPointF &control2, 0994 const KisPaintInformation &pi2) 0995 { 0996 paintBezierCurve(0, pi1, control1, control2, pi2); 0997 }