Krita Source Code Documentation
Loading...
Searching...
No Matches
kis_distance_information.cpp
Go to the documentation of this file.
1/*
2 * SPDX-FileCopyrightText: 2010 Cyrille Berger <cberger@cberger.net>
3 * SPDX-FileCopyrightText: 2013 Dmitry Kazakov <dimula73@gmail.com>
4 *
5 * SPDX-License-Identifier: GPL-2.0-or-later
6 */
7
12#include "kis_debug.h"
13#include <QtCore/qmath.h>
14#include <QVector2D>
15#include <QTransform>
16#include "kis_algebra_2d.h"
17#include "kis_dom_utils.h"
18
19#include "kis_lod_transform.h"
20
21const qreal MIN_DISTANCE_SPACING = 0.5;
22
23// Smallest allowed interval when timed spacing is enabled, in milliseconds.
24const qreal MIN_TIMED_INTERVAL = 0.5;
25
26// Largest allowed interval when timed spacing is enabled, in milliseconds.
28
29struct Q_DECL_HIDDEN KisDistanceInformation::Private {
31 accumDistance(),
32 accumTime(0.0),
33 spacingUpdateInterval(LONG_TIME),
34 timeSinceSpacingUpdate(0.0),
35 timingUpdateInterval(LONG_TIME),
36 timeSinceTimingUpdate(0.0),
37 lastAngle(0.0),
38 lastDabInfoValid(false),
39 lastPaintInfoValid(false),
40 totalDistance(0.0),
41 currentDabSeqNo(0),
42 levelOfDetail(0)
43 {
44 }
45
46 // Accumulators of time/distance passed since the last painted dab
48 qreal accumTime;
49
52 // Accumulator of time passed since the last spacing update
54
57 // Accumulator of time passed since the last timing update
59
60 // Information about the last position considered (not necessarily a painted dab)
61 QPointF lastPosition;
62 qreal lastAngle;
64
65 // Information about the last painted dab
68
70 boost::optional<qreal> lockedDrawingAngleOptional;
71
74
75 qreal lastMaxPressure = 0.0;
76};
77
78struct Q_DECL_HIDDEN KisDistanceInitInfo::Private {
80 hasLastInfo(false),
81 lastPosition(),
82 lastAngle(0.0),
83 spacingUpdateInterval(LONG_TIME),
84 timingUpdateInterval(LONG_TIME),
85 currentDabSeqNo(0)
86 {
87 }
88
89
90 // Indicates whether lastPosition, and lastAngle are valid or not.
92
93 QPointF lastPosition;
94 qreal lastAngle;
95
98
100};
101
106
107KisDistanceInitInfo::KisDistanceInitInfo(qreal spacingUpdateInterval, qreal timingUpdateInterval, int currentDabSeqNo)
108 : m_d(new Private)
109{
110 m_d->spacingUpdateInterval = spacingUpdateInterval;
111 m_d->timingUpdateInterval = timingUpdateInterval;
112 m_d->currentDabSeqNo = currentDabSeqNo;
113}
114
116 qreal lastAngle, int currentDabSeqNo)
117 : m_d(new Private)
118{
119 m_d->hasLastInfo = true;
120 m_d->lastPosition = lastPosition;
121 m_d->lastAngle = lastAngle;
122 m_d->currentDabSeqNo = currentDabSeqNo;
123}
124
126 qreal lastAngle, qreal spacingUpdateInterval,
127 qreal timingUpdateInterval,
128 int currentDabSeqNo)
129 : m_d(new Private)
130{
131 m_d->hasLastInfo = true;
132 m_d->lastPosition = lastPosition;
133 m_d->lastAngle = lastAngle;
134 m_d->spacingUpdateInterval = spacingUpdateInterval;
135 m_d->timingUpdateInterval = timingUpdateInterval;
136 m_d->currentDabSeqNo = currentDabSeqNo;
137}
138
140 : m_d(new Private(*rhs.m_d))
141{
142}
143
148
150{
151 if (m_d->spacingUpdateInterval != other.m_d->spacingUpdateInterval
152 || m_d->timingUpdateInterval != other.m_d->timingUpdateInterval
153 || m_d->hasLastInfo != other.m_d->hasLastInfo)
154 {
155 return false;
156 }
157 if (m_d->hasLastInfo) {
158 if (m_d->lastPosition != other.m_d->lastPosition
159 || m_d->lastAngle != other.m_d->lastAngle)
160 {
161 return false;
162 }
163 }
164
165 if (m_d->currentDabSeqNo != other.m_d->currentDabSeqNo) {
166 return false;
167 }
168
169 return true;
170}
171
173{
174 *m_d = *rhs.m_d;
175 return *this;
176}
177
179{
180 if (m_d->hasLastInfo) {
181 return KisDistanceInformation(m_d->lastPosition, m_d->lastAngle,
182 m_d->spacingUpdateInterval, m_d->timingUpdateInterval,
183 m_d->currentDabSeqNo);
184 }
185 else {
186 return KisDistanceInformation(m_d->spacingUpdateInterval, m_d->timingUpdateInterval, m_d->currentDabSeqNo);
187 }
188}
189
190void KisDistanceInitInfo::toXML(QDomDocument &doc, QDomElement &elt) const
191{
192 elt.setAttribute("spacingUpdateInterval", QString::number(m_d->spacingUpdateInterval, 'g', 15));
193 elt.setAttribute("timingUpdateInterval", QString::number(m_d->timingUpdateInterval, 'g', 15));
194 elt.setAttribute("currentDabSeqNo", QString::number(m_d->currentDabSeqNo));
195 if (m_d->hasLastInfo) {
196 QDomElement lastInfoElt = doc.createElement("LastInfo");
197 lastInfoElt.setAttribute("lastPosX", QString::number(m_d->lastPosition.x(), 'g', 15));
198 lastInfoElt.setAttribute("lastPosY", QString::number(m_d->lastPosition.y(), 'g', 15));
199 lastInfoElt.setAttribute("lastAngle", QString::number(m_d->lastAngle, 'g', 15));
200 elt.appendChild(lastInfoElt);
201 }
202}
203
205{
206 const qreal spacingUpdateInterval = qreal(KisDomUtils::toDouble(elt.attribute("spacingUpdateInterval",
207 QString::number(LONG_TIME, 'g', 15))));
208 const qreal timingUpdateInterval = qreal(KisDomUtils::toDouble(elt.attribute("timingUpdateInterval",
209 QString::number(LONG_TIME, 'g', 15))));
210 const qreal currentDabSeqNo = KisDomUtils::toInt(elt.attribute("currentDabSeqNo", "0"));
211
212 const QDomElement lastInfoElt = elt.firstChildElement("LastInfo");
213 const bool hasLastInfo = !lastInfoElt.isNull();
214
215 if (hasLastInfo) {
216 const qreal lastPosX = qreal(KisDomUtils::toDouble(lastInfoElt.attribute("lastPosX",
217 "0.0")));
218 const qreal lastPosY = qreal(KisDomUtils::toDouble(lastInfoElt.attribute("lastPosY",
219 "0.0")));
220 const qreal lastAngle = qreal(KisDomUtils::toDouble(lastInfoElt.attribute("lastAngle",
221 "0.0")));
222
223 return KisDistanceInitInfo(QPointF(lastPosX, lastPosY), lastAngle,
226 }
227 else {
230 }
231}
232
237
239 qreal timingUpdateInterval,
240 int currentDabSeqNo)
241 : m_d(new Private)
242{
243 m_d->spacingUpdateInterval = spacingUpdateInterval;
244 m_d->timingUpdateInterval = timingUpdateInterval;
245 m_d->currentDabSeqNo = currentDabSeqNo;
246}
247
249 qreal lastAngle)
250 : m_d(new Private)
251{
252 m_d->lastPosition = lastPosition;
253 m_d->lastAngle = lastAngle;
254
255 m_d->lastDabInfoValid = true;
256}
257
259 qreal lastAngle,
260 qreal spacingUpdateInterval,
261 qreal timingUpdateInterval,
262 int currentDabSeqNo)
263 : KisDistanceInformation(lastPosition, lastAngle)
264{
265 m_d->spacingUpdateInterval = spacingUpdateInterval;
266 m_d->timingUpdateInterval = timingUpdateInterval;
267 m_d->currentDabSeqNo = currentDabSeqNo;
268}
269
271 : m_d(new Private(*rhs.m_d))
272{
273
274}
275
277 : m_d(new Private(*rhs.m_d))
278{
279 KIS_ASSERT_RECOVER_NOOP(!m_d->lastPaintInfoValid &&
280 "The distance information "
281 "should be cloned before the "
282 "actual painting is started");
283
284 m_d->levelOfDetail = levelOfDetail;
285
287 m_d->lastPosition = t.map(m_d->lastPosition);
288}
289
291{
292 *m_d = *rhs.m_d;
293 return *this;
294}
295
296void KisDistanceInformation::overrideLastValues(const QPointF &lastPosition,
297 qreal lastAngle)
298{
299 m_d->lastPosition = lastPosition;
300 m_d->lastAngle = lastAngle;
301
302 m_d->lastDabInfoValid = true;
303}
304
309
311{
312 return m_d->spacing;
313}
314
316{
317 m_d->spacing = spacing;
318 m_d->timeSinceSpacingUpdate = 0.0;
319}
320
322{
323 return m_d->timeSinceSpacingUpdate >= m_d->spacingUpdateInterval;
324}
325
327{
328 return m_d->timing;
329}
330
332{
333 m_d->timing = timing;
334 m_d->timeSinceTimingUpdate = 0.0;
335}
336
338{
339 return m_d->timeSinceTimingUpdate >= m_d->timingUpdateInterval;
340}
341
343{
344 return m_d->lastDabInfoValid;
345}
346
348{
349 return m_d->lastPosition;
350}
351
353{
354 return m_d->lastAngle;
355}
356
358{
359 return m_d->lastPaintInfoValid;
360}
361
363{
364 return m_d->lastPaintInformation;
365}
366
368{
369 return m_d->currentDabSeqNo;
370}
371
373{
374 return m_d->lastMaxPressure;
375}
376
378{
379 return m_d->lastPaintInfoValid;
380}
381
383 const KisSpacingInformation &spacing,
384 const KisTimingInformation &timing)
385{
386 m_d->totalDistance +=
387 KisAlgebra2D::norm(info.pos() - m_d->lastPosition) *
388 KisLodTransform::lodToInvScale(m_d->levelOfDetail);
389
390 m_d->lastPaintInformation = info;
391 m_d->lastPaintInfoValid = true;
392
393 m_d->lastAngle = info.drawingAngle(false);
394 m_d->lastPosition = info.pos();
395 m_d->lastDabInfoValid = true;
396
397 m_d->spacing = spacing;
398 m_d->timing = timing;
399
400 m_d->currentDabSeqNo++;
401
402 m_d->lastMaxPressure = qMax(info.pressure(), m_d->lastMaxPressure);
403}
404
406 const QPointF &end,
407 qreal startTime,
408 qreal endTime)
409{
410 // Compute interpolation factor based on distance.
411 qreal distanceFactor = -1.0;
412 if (m_d->spacing.isDistanceSpacingEnabled()) {
413 distanceFactor = m_d->spacing.isIsotropic() ?
416 }
417
418 // Compute interpolation factor based on time.
419 qreal timeFactor = -1.0;
420 if (m_d->timing.isTimedSpacingEnabled()) {
421 timeFactor = getNextPointPositionTimed(startTime, endTime);
422 }
423
424 // Return the distance-based or time-based factor, whichever is smallest.
425 qreal t = -1.0;
426 if (distanceFactor < 0.0) {
427 t = timeFactor;
428 } else if (timeFactor < 0.0) {
429 t = distanceFactor;
430 } else {
431 t = qMin(distanceFactor, timeFactor);
432 }
433
434 // If we aren't ready to paint a dab, accumulate time for the spacing/timing updates that might
435 // be needed between dabs.
436 if (t < 0.0) {
437 m_d->timeSinceSpacingUpdate += endTime - startTime;
438 m_d->timeSinceTimingUpdate += endTime - startTime;
439 }
440
441 // If we are ready to paint a dab, reset the accumulated time for spacing/timing updates.
442 else {
443 m_d->timeSinceSpacingUpdate = 0.0;
444 m_d->timeSinceTimingUpdate = 0.0;
445 }
446
447 return t;
448}
449
451{
452 return m_d->spacingUpdateInterval;
453}
454
456{
457 return m_d->timingUpdateInterval;
458}
459
461 const QPointF &end)
462{
463 qreal distance = m_d->accumDistance.x();
464 qreal spacing = qMax(MIN_DISTANCE_SPACING, m_d->spacing.distanceSpacing().x());
465
466 if (start == end) {
467 return -1;
468 }
469
470 qreal dragVecLength = QVector2D(end - start).length();
471 qreal nextPointDistance = spacing - distance;
472
473 qreal t;
474
475 // nextPointDistance can sometimes be negative if the spacing info has been modified since the
476 // last interpolation attempt. In that case, have a point painted immediately.
477 if (nextPointDistance <= 0.0) {
479 t = 0.0;
480 }
481 else if (nextPointDistance <= dragVecLength) {
482 t = nextPointDistance / dragVecLength;
484 } else {
485 t = -1;
486 m_d->accumDistance.rx() += dragVecLength;
487 }
488
489 return t;
490}
491
493 const QPointF &end)
494{
495 if (start == end) {
496 return -1;
497 }
498
499 qreal a_rev = 1.0 / qMax(MIN_DISTANCE_SPACING, m_d->spacing.distanceSpacing().x());
500 qreal b_rev = 1.0 / qMax(MIN_DISTANCE_SPACING, m_d->spacing.distanceSpacing().y());
501
502 qreal x = m_d->accumDistance.x();
503 qreal y = m_d->accumDistance.y();
504
505 qreal gamma = pow2(x * a_rev) + pow2(y * b_rev) - 1;
506
507 // If the distance accumulator is already past the spacing ellipse, have a point painted
508 // immediately. This can happen if the spacing info has been modified since the last
509 // interpolation attempt.
510 if (gamma >= 0.0) {
512 return 0.0;
513 }
514
515 static const qreal eps = 2e-3; // < 0.2 deg
516
517 qreal currentRotation = m_d->spacing.rotation();
518 if (m_d->spacing.coordinateSystemFlipped()) {
519 currentRotation = 2 * M_PI - currentRotation;
520 }
521
522 QPointF diff = end - start;
523
524 if (currentRotation > eps) {
525 QTransform rot;
526 // since the ellipse is symmetrical, the sign
527 // of rotation doesn't matter
528 rot.rotateRadians(currentRotation);
529 diff = rot.map(diff);
530 }
531
532 qreal dx = qAbs(diff.x());
533 qreal dy = qAbs(diff.y());
534
535 qreal alpha = pow2(dx * a_rev) + pow2(dy * b_rev);
536 qreal beta = x * dx * a_rev * a_rev + y * dy * b_rev * b_rev;
537 qreal D_4 = pow2(beta) - alpha * gamma;
538
539 qreal t = -1.0;
540
541 if (D_4 >= 0) {
542 qreal k = (-beta + qSqrt(D_4)) / alpha;
543
544 if (k >= 0.0 && k <= 1.0) {
545 t = k;
547 } else {
548 m_d->accumDistance += KisAlgebra2D::abs(diff);
549 }
550 } else {
551 warnKrita << "BUG: No solution for elliptical spacing equation has been found. This shouldn't have happened.";
552 }
553
554 return t;
555}
556
558 qreal endTime)
559{
560 // If start time is not before end time, do not interpolate.
561 if (!(startTime < endTime)) {
562 return -1.0;
563 }
564
565 qreal timedSpacingInterval = qBound(MIN_TIMED_INTERVAL, m_d->timing.timedSpacingInterval(),
567 qreal nextPointInterval = timedSpacingInterval - m_d->accumTime;
568
569 qreal t = -1.0;
570
571 // nextPointInterval can sometimes be negative if the spacing info has been modified since the
572 // last interpolation attempt. In that case, have a point painted immediately.
573 if (nextPointInterval <= 0.0) {
575 t = 0.0;
576 }
577 else if (nextPointInterval <= endTime - startTime) {
579 t = nextPointInterval / (endTime - startTime);
580 }
581 else {
582 m_d->accumTime += endTime - startTime;
583 t = -1.0;
584 }
585
586 return t;
587}
588
590{
591 m_d->accumDistance = QPointF();
592 m_d->accumTime = 0.0;
593}
594
595boost::optional<qreal> KisDistanceInformation::lockedDrawingAngleOptional() const
596{
597 return m_d->lockedDrawingAngleOptional;
598}
599
601{
602 qreal newAngle = info.drawingAngle(false);
603
604 if (m_d->lockedDrawingAngleOptional) {
605 newAngle = *m_d->lockedDrawingAngleOptional;
606 }
607
608 m_d->lockedDrawingAngleOptional = newAngle;
609}
610
611
613{
614 return m_d->totalDistance;
615}
616
qreal distance(const QPointF &p1, const QPointF &p2)
static qreal lodToInvScale(int levelOfDetail)
KisPaintInformation map(KisPaintInformation pi) const
const QPointF & pos() const
qreal pressure() const
The pressure of the value (from 0.0 to 1.0)
qreal drawingAngle(bool considerLockedAngle=false) const
#define KIS_ASSERT_RECOVER_NOOP(cond)
Definition kis_assert.h:97
const qreal eps
#define warnKrita
Definition kis_debug.h:87
const qreal MIN_DISTANCE_SPACING
const qreal MAX_TIMED_INTERVAL
const qreal MIN_TIMED_INTERVAL
T pow2(const T &x)
Definition kis_global.h:166
#define M_PI
Definition kis_global.h:111
const qreal LONG_TIME
Point abs(const Point &pt)
qreal norm(const T &a)
double toDouble(const QString &str, bool *ok=nullptr)
int toInt(const QString &str, bool *ok=nullptr)
boost::optional< qreal > lockedDrawingAngleOptional
const KisTimingInformation & currentTiming() const
KisDistanceInformation & operator=(const KisDistanceInformation &rhs)
qreal getNextPointPositionAnisotropic(const QPointF &start, const QPointF &end)
qreal getNextPointPositionIsotropic(const QPointF &start, const QPointF &end)
KisPaintInformation lastPaintInformation
void updateTiming(const KisTimingInformation &timing)
void updateSpacing(const KisSpacingInformation &spacing)
qreal getNextPointPositionTimed(qreal startTime, qreal endTime)
void lockCurrentDrawingAngle(const KisPaintInformation &info) const
const KisSpacingInformation & currentSpacing() const
void registerPaintedDab(const KisPaintInformation &info, const KisSpacingInformation &spacing, const KisTimingInformation &timing)
void overrideLastValues(const QPointF &lastPosition, qreal lastAngle)
qreal getNextPointPosition(const QPointF &start, const QPointF &end, qreal startTime, qreal endTime)
KisDistanceInitInfo & operator=(const KisDistanceInitInfo &rhs)
void toXML(QDomDocument &doc, QDomElement &elt) const
bool operator==(const KisDistanceInitInfo &other) const
KisDistanceInformation makeDistInfo()
static KisDistanceInitInfo fromXML(const QDomElement &elt)