Krita Source Code Documentation
Loading...
Searching...
No Matches
inplace_transform_stroke_strategy.cpp
Go to the documentation of this file.
1/*
2 * SPDX-FileCopyrightText: 2013,2020 Dmitry Kazakov <dimula73@gmail.com>
3 *
4 * SPDX-License-Identifier: GPL-2.0-or-later
5 */
6
8
9#include <QMutex>
10#include <QMutexLocker>
11#include <QElapsedTimer>
13
15
16#include <klocalizedstring.h>
17#include <kis_node.h>
18#include <kis_group_layer.h>
20#include <kis_transaction.h>
21#include <kis_painter.h>
23#include <kis_transform_mask.h>
25#include "kis_transform_utils.h"
26#include "kis_convex_hull.h"
29
30#include "kis_projection_leaf.h"
32#include "KisAnimAutoKey.h"
33
35#include "kis_selection_mask.h"
36#include "kis_image_config.h"
37#include "kis_layer_utils.h"
38#include <QQueue>
44#include "kis_command_ids.h"
49#include "kis_lod_transform.h"
50#include <boost/optional.hpp>
51#include "kis_selection_mask.h"
52#include "kis_undo_stores.h"
54#include "kis_filter_mask.h"
56#include <kis_shape_layer.h>
59#include "KisAnimAutoKey.h"
60#include "krita_utils.h"
61
62
64{
65 // initial conditions passed from the tool
69 QString filterId;
75 int currentTime = -1; // NOTE: initialized asynchronously in initStrokeCallback!
77 bool forceLodMode = true;
78
79 // properties filled by initialization/transformation routines
83
85
88
90
96
98
100 QHash<KisPaintDevice*, KisPaintDeviceSP> devicesCacheHash;
101 QHash<KisTransformMask*, KisPaintDeviceSP> transformMaskCacheHash;
102
106
109
110 inline KisBatchNodeUpdate& effectiveDirtyRects(int levelOfDetail) {
111 return levelOfDetail > 0 ? dirtyPreviewRects : dirtyRects;
112 }
113
114 inline KisBatchNodeUpdate& effectivePrevDirtyRects(int levelOfDetail) {
115 return levelOfDetail > 0 ? prevDirtyPreviewRects : prevDirtyRects;
116 }
117
118 // data for asynchronous updates
119 boost::optional<ToolTransformArgs> pendingUpdateArgs;
120 QElapsedTimer updateTimer;
121 const int updateInterval = 30;
122
123 // temporary variable to share data between jobs at the initialization phase
125
133
135
138
140};
141
142
144 const QString &filterId,
145 bool forceReset,
146 KisNodeList rootNodes,
147 KisSelectionSP selection,
148 KisPaintDeviceSP externalSource,
149 KisStrokeUndoFacade *undoFacade,
150 KisUpdatesFacade *updatesFacade,
151 KisNodeSP imageRoot,
152 bool forceLodMode)
153 : KisStrokeStrategyUndoCommandBased(kundo2_i18n("Transform"), false, undoFacade),
154 m_d(new Private())
155{
156
157 m_d->mode = mode;
158 m_d->filterId = filterId;
159 m_d->forceReset = forceReset;
160 m_d->rootNodes = rootNodes;
161 m_d->selection = selection;
162 m_d->externalSource = externalSource;
163 m_d->updatesFacade = updatesFacade;
164 m_d->undoFacade = undoFacade;
165 m_d->imageRoot = imageRoot;
166 m_d->forceLodMode = forceLodMode;
167 m_d->commandUpdatesBlockerCookie.reset(new boost::none_t(boost::none));
168
169 if (selection) {
170 Q_FOREACH(KisNodeSP node, rootNodes) {
171 KIS_SAFE_ASSERT_RECOVER_NOOP(!dynamic_cast<KisTransformMask*>(node.data()));
172 }
173 }
175
176 // TODO: check if can be relaxed
178}
179
183
185{
186 if (UpdateTransformData *upd = dynamic_cast<UpdateTransformData*>(data)) {
187 if (upd->destination == UpdateTransformData::PAINT_DEVICE) {
188 m_d->pendingUpdateArgs = upd->args;
189 tryPostUpdateJob(false);
190 } else if (m_d->selection) {
191 // NOTE: selection is hidden during the transformation, so we
192 // don't have to do any preview for that. We transform
193 // that in one go in the end of the stroke.
194
195 KisTransaction transaction(m_d->selection->pixelSelection());
196
197 KisProcessingVisitor::ProgressHelper helper(m_d->imageRoot.data());
199 m_d->selection->pixelSelection(), &helper);
200
203 }
204
205 } else if (BarrierUpdateData *barrierData = dynamic_cast<BarrierUpdateData *>(data)) {
206
207 doCanvasUpdate(barrierData->forceUpdate);
208
209 } else if (KisAsynchronousStrokeUpdateHelper::UpdateData *updateData =
211
212 tryPostUpdateJob(updateData->forceUpdate);
213
214 } else if (dynamic_cast<CalculateConvexHullData*>(data)) {
215
216 if (!m_d->convexHullHasBeenCalculated) {
217 m_d->convexHullHasBeenCalculated = true;
218 QPolygon hull = calculateConvexHull();
219 if (!hull.isEmpty()) {
220 Q_EMIT sigConvexHullCalculated(hull, this);
221 }
222 }
223
224 } else {
226 }
227
228}
229
231{
232 if (!m_d->pendingUpdateArgs) return;
233
234 if (forceUpdate ||
235 (m_d->updateTimer.elapsed() > m_d->updateInterval &&
236 !m_d->updatesFacade->hasUpdatesRunning())) {
237
238 addMutatedJob(new BarrierUpdateData(forceUpdate));
239 }
240}
241
243{
244 if (!m_d->pendingUpdateArgs) return;
245
246 if (!forceUpdate &&
247 (m_d->updateTimer.elapsed() < m_d->updateInterval ||
248 m_d->updatesFacade->hasUpdatesRunning())) {
249
250 return;
251 }
252
254
255 ToolTransformArgs args = *m_d->pendingUpdateArgs;
256 m_d->pendingUpdateArgs = boost::none;
257
258 reapplyTransform(args, jobs, m_d->previewLevelOfDetail, false);
259
260 KritaUtils::addJobBarrier(jobs, [this, args]() {
261 m_d->currentTransformArgs = args;
262 m_d->updateTimer.restart();
263 // sanity check that no job has been squeezed in between
264 KIS_SAFE_ASSERT_RECOVER_RETURN(!m_d->pendingUpdateArgs);
265 });
266
267 addMutatedJobs(jobs);
268}
269
271{
272 KisLodPreferences lodPreferences = this->currentLodPreferences();
273 if (!lodPreferences.lodSupported() ||
274 !(lodPreferences.lodPreferred() || m_d->forceLodMode)) return -1;
275
276 const int maxSize = 2000;
277 const int maxDimension = KisAlgebra2D::maxDimension(srcRect);
278
279 const qreal zoom = qMax(1.0, qreal(maxDimension) / maxSize);
280
281 const int calculatedLod = qCeil(std::log2(zoom));
282
283 return qMax(calculatedLod, lodPreferences.desiredLevelOfDetail());
284
285}
286
287
289{
291 m_d->currentTransformArgs,
292 m_d->rootNodes,
293 m_d->processedNodes,
294 m_d->currentTime,
295 m_d->overriddenCommand);
296
298}
299
301{
302 // Best effort attempt to calculate the convex hull, mimicking the
303 // approach that computes srcRect in initStrokeCallback below
304 KisPaintDeviceSP externalSource =
305 m_d->externalSource ? m_d->externalSource :
306 m_d->initialTransformArgs.externalSource() ?
307 m_d->initialTransformArgs.externalSource() : 0;
308
309 QVector<QPoint> points;
310 if (externalSource) {
311 points = KisConvexHull::findConvexHull(externalSource);
312 } else if (m_d->selection) {
313 points = KisConvexHull::findConvexHull(m_d->selection->pixelSelection());
314 } else {
315 int numContributions = 0;
316 Q_FOREACH (KisNodeSP node, m_d->processedNodes) {
317 if (node->inherits("KisGroupLayer")) continue;
318
319 if (dynamic_cast<const KisTransformMask*>(node.data())) {
320 return QPolygon(); // Produce no convex hull if a KisTransformMask is present
321 } else {
322 KisPaintDeviceSP device;
323 // Get the original device as per createCacheAndClearNode below
324 if (KisExternalLayer *extLayer = dynamic_cast<KisExternalLayer*>(node.data())) {
325 device = extLayer->projection();
326 } else {
327 device = node->paintDevice();
328 }
329 if (device) {
330 // Use the original device to get the cached device containing the original image data
331 KisPaintDeviceSP toUse;
332 {
333 QMutexLocker l(&m_d->devicesCacheMutex);
334 if (m_d->devicesCacheHash.contains(device.data())) {
335 toUse = m_d->devicesCacheHash[device.data()];
336 } else {
337 toUse = device;
338 }
339 }
340 /* This sometimes does not agree with the original exactBounds
341 because of colorspace changes between the original device
342 and cached. E.g. When the defaultPixel changes as follows it
343 triggers different behavior in calculateExactBounds:
344 KoColor ("ALPHA", "Alpha":0) => KoColor ("GRAYA", "Gray":0, "Alpha":255)
345 */
346 const bool isConvertedSelection =
347 node->paintDevice() &&
349 *toUse->colorSpace() == *node->paintDevice()->compositionSourceColorSpace();
350
351
352 QPolygon polygon = isConvertedSelection ?
355
356 points += polygon;
357
358 numContributions += 1;
359 } else {
360 // When can this happen? Should it continue instead?
361 ENTER_FUNCTION() << "Bailing out, device was null" << ppVar(node);
362 return QPolygon();
363 }
364 }
365 }
366 if (numContributions > 1) {
367 points = KisConvexHull::findConvexHull(points);
368 }
369 }
370 return QPolygon(points);
371}
372
373
375{
377
378 m_d->currentTime = KisTransformUtils::fetchCurrentImageTime(m_d->rootNodes);
379
380 QVector<KisStrokeJobData *> extraInitJobs;
381
382 if (m_d->selection) {
383 m_d->selection->setVisible(false);
384 m_d->deactivatedSelections.append(m_d->selection);
385 }
386
387 Q_FOREACH(KisNodeSP node, m_d->rootNodes) {
388 KisSelectionMaskSP overlaySelectionMask =
389 dynamic_cast<KisSelectionMask*>(node->graphListener()->graphOverlayNode());
390 if (overlaySelectionMask && node != KisNodeSP(overlaySelectionMask)) {
391 overlaySelectionMask->setDecorationsVisible(false);
392 m_d->deactivatedOverlaySelectionMasks.append(overlaySelectionMask);
393 }
394 }
395
396 if (m_d->rootNodes.size() == 1) {
397 KisNodeSP rootNode = m_d->rootNodes[0];
399
400 if (rootNode->inherits("KisTransformMask") && rootNode->projectionLeaf()->isDroppedNode()) {
401 rootNode.clear();
402 m_d->processedNodes.clear();
403
404 TransformTransactionProperties transaction(QRect(), &m_d->initialTransformArgs, m_d->rootNodes, m_d->processedNodes);
405 Q_EMIT sigTransactionGenerated(transaction, m_d->initialTransformArgs, this);
406 return;
407 }
408 }
409
410 // When placing an external source image, we never work recursively on any layer masks
411 m_d->processedNodes = KisTransformUtils::fetchNodesList(m_d->mode, m_d->rootNodes, m_d->externalSource, m_d->selection);
412
413 bool argsAreInitialized = false;
414 QVector<KisStrokeJobData *> lastCommandUndoJobs;
415
416 // When externalSource is set, it means that we are initializing a new
417 // stroke following a newActivationWithExternalSource, thus we never try
418 // to reuse an existing transformation from the undo queue. However, when
419 // externalSource is not set, tryFetchArgsFromCommandAndUndo may still
420 // recover a previous stroke that referenced an external source.
421 if (!m_d->forceReset && !m_d->externalSource) {
423 m_d->mode,
424 m_d->rootNodes,
425 m_d->processedNodes,
426 m_d->undoFacade,
427 m_d->currentTime,
428 &lastCommandUndoJobs,
429 &m_d->overriddenCommand)) {
430 argsAreInitialized = true;
431 } else if (KisTransformUtils::tryInitArgsFromNode(m_d->rootNodes, &m_d->initialTransformArgs)) {
432 argsAreInitialized = true;
433 }
434 }
435
436 KritaUtils::addJobBarrier(extraInitJobs, [this]() {
437 Q_FOREACH (KisNodeSP node, m_d->processedNodes) {
438 m_d->prevDirtyRects.addUpdate(node, node->projectionPlane()->tightUserVisibleBounds());
439 }
440
441 m_d->initialUpdatesBeforeClear = m_d->prevDirtyRects.compressed();
442 m_d->updateDataForUndo.reset(new KisBatchNodeUpdate(m_d->initialUpdatesBeforeClear));
443
444 executeAndAddCommand(new KisUpdateCommandEx(m_d->updateDataForUndo, m_d->updatesFacade, KisUpdateCommandEx::INITIALIZING, m_d->commandUpdatesBlockerCookie), Clear, KisStrokeJobData::BARRIER);
446 });
447
448 extraInitJobs << lastCommandUndoJobs;
449
450 if (!lastCommandUndoJobs.isEmpty()) {
451 KritaUtils::addJobBarrier(extraInitJobs, [this]() {
458 KisBatchNodeUpdate updates;
459
460 Q_FOREACH (KisNodeSP node, m_d->processedNodes) {
461 updates.addUpdate(node, node->projectionPlane()->tightUserVisibleBounds());
462 }
463
464 m_d->initialUpdatesBeforeClear = updates.compressed();
465 *m_d->updateDataForUndo = m_d->initialUpdatesBeforeClear;
466
472 m_d->pendingUpdateArgs = m_d->initialTransformArgs;
473 });
474 }
475
476 KritaUtils::addJobSequential(extraInitJobs, [this]() {
477 // When dealing with animated transform mask layers, create keyframe and save the command for undo.
478 // NOTE: for transform masks we create a keyframe no matter what the user
479 // settings are
480 Q_FOREACH (KisNodeSP node, m_d->processedNodes) {
481 if (KisTransformMask* transformMask = dynamic_cast<KisTransformMask*>(node.data())) {
484 }
486 node->hasEditablePaintDevice()){
487
488 KUndo2Command *autoKeyframeCommand =
491 if (autoKeyframeCommand) {
493 }
494 }
495 }
496 });
497
498 KritaUtils::addJobSequential(extraInitJobs, [this]() {
502 Q_FOREACH(KisNodeSP node, m_d->rootNodes) {
504 }
505 });
506
507 KritaUtils::addJobBarrier(extraInitJobs, [this]() {
512 Q_FOREACH(KisNodeSP node, m_d->rootNodes) {
514 }
515 });
516
520 KritaUtils::addJobBarrier(extraInitJobs, [this]() {
521 Q_FOREACH (KisNodeSP node, m_d->processedNodes) {
522 KisDecoratedNodeInterface *decoratedNode = dynamic_cast<KisDecoratedNodeInterface*>(node.data());
523 if (decoratedNode && decoratedNode->decorationsVisible()) {
524 decoratedNode->setDecorationsVisible(false);
525 m_d->disabledDecoratedNodes << decoratedNode;
526 }
527 }
528 });
529
530 KritaUtils::addJobBarrier(extraInitJobs,
531 [this,
532 argsAreInitialized]() mutable {
533 QRect srcRect;
534
535 KisPaintDeviceSP externalSource =
536 m_d->externalSource ? m_d->externalSource :
537 (argsAreInitialized && m_d->initialTransformArgs.externalSource()) ?
538 m_d->initialTransformArgs.externalSource() : 0;
539
540 if (externalSource) {
541 // Start the transformation around the visible pixels of the external image
542 srcRect = externalSource->exactBounds();
543 } else if (m_d->selection) {
544 srcRect = m_d->selection->selectedExactRect();
545 } else {
546 srcRect = QRect();
547 Q_FOREACH (KisNodeSP node, m_d->processedNodes) {
548 // group layers may have a projection of layers
549 // that are locked and will not be transformed
550 if (node->inherits("KisGroupLayer")) continue;
551
552 if (const KisTransformMask *mask = dynamic_cast<const KisTransformMask*>(node.data())) {
553 srcRect |= mask->sourceDataBounds();
554 } else if (const KisSelectionMask *mask = dynamic_cast<const KisSelectionMask*>(node.data())) {
555 srcRect |= mask->selection()->selectedExactRect();
556 } else if (const KisTransparencyMask *mask = dynamic_cast<const KisTransparencyMask*>(node.data())) {
557 srcRect |= mask->selection()->selectedExactRect();
558 } else if (const KisFilterMask *mask = dynamic_cast<const KisFilterMask*>(node.data())) {
563 if (mask->paintDevice()) {
564 srcRect |= mask->paintDevice()->nonDefaultPixelArea();
565 }
566 } else {
569 srcRect |= node->paintDevice() ? node->paintDevice()->exactBounds() : node->exactBounds();
570 }
571 }
572 }
573
574 TransformTransactionProperties transaction(srcRect, &m_d->initialTransformArgs, m_d->rootNodes, m_d->processedNodes);
575 if (!argsAreInitialized) {
576 m_d->initialTransformArgs = KisTransformUtils::resetArgsForMode(m_d->mode, m_d->filterId, transaction, m_d->externalSource);
577 }
578 m_d->externalSource.clear();
579
580 const QRect imageBoundsRect = m_d->imageRoot->projection()->defaultBounds()->bounds();
581 m_d->previewLevelOfDetail = calculatePreferredLevelOfDetail(srcRect & imageBoundsRect);
582
583 if (m_d->previewLevelOfDetail > 0) {
584 for (auto it = m_d->prevDirtyRects.begin(); it != m_d->prevDirtyRects.end(); ++it) {
585 KisLodTransform t(m_d->previewLevelOfDetail);
586 m_d->prevDirtyPreviewRects.addUpdate(it->first, t.map(it->second));
587 }
588 }
589
590 if (transaction.currentConfig()->boundsRotation() != 0.0) {
591 m_d->convexHullHasBeenCalculated = true;
592 transaction.setConvexHull(calculateConvexHull());
593 transaction.setConvexHullHasBeenRequested(true);
594 }
595
596 Q_EMIT sigTransactionGenerated(transaction, m_d->initialTransformArgs, this);
597 });
598
600 KritaUtils::addJobBarrier(extraInitJobs, [this]() {
601 Q_FOREACH (KisDecoratedNodeInterface *decoratedNode, m_d->disabledDecoratedNodes) {
602 decoratedNode->setDecorationsVisible(true);
603 }
604 m_d->disabledDecoratedNodes.clear();
605 });
606
607 Q_FOREACH (KisNodeSP node, m_d->processedNodes) {
608 KritaUtils::addJobSequential(extraInitJobs, [this, node]() mutable {
610 });
611 }
612
613 KritaUtils::addJobBarrier(extraInitJobs, [this]() {
614 QMutexLocker l(&m_d->dirtyRectsMutex);
615
617
618 m_d->updateTimer.start();
619 });
620
621 if (!lastCommandUndoJobs.isEmpty()) {
622 KIS_SAFE_ASSERT_RECOVER_NOOP(m_d->overriddenCommand);
623
624 for (auto it = extraInitJobs.begin(); it != extraInitJobs.end(); ++it) {
625 (*it)->setCancellable(false);
626 }
627 }
628
629 KritaUtils::addJobBarrier(extraInitJobs, [this]() {
630 if (m_d->previewLevelOfDetail > 0) {
631 QVector<KisStrokeJobData*> lodSyncJobs;
632
633 KisSyncLodCacheStrokeStrategy::createJobsData(lodSyncJobs,
634 m_d->imageRoot,
635 m_d->updatesFacade,
636 m_d->previewLevelOfDetail,
637 m_d->devicesCacheHash.values() +
638 m_d->transformMaskCacheHash.values());
639
640 for (auto it = lodSyncJobs.begin(); it != lodSyncJobs.end(); ++it) {
641 (*it)->setLevelOfDetailOverride(m_d->previewLevelOfDetail);
642 }
643
644 addMutatedJobs(lodSyncJobs);
645 }
646 });
647
648 addMutatedJobs(extraInitJobs);
649}
650
652{
653 QVector<KisStrokeJobData *> mutatedJobs;
654
655 finishAction(mutatedJobs);
656
657 if (!mutatedJobs.isEmpty()) {
658 addMutatedJobs(mutatedJobs);
659 }
660}
661
663{
664 QVector<KisStrokeJobData *> mutatedJobs;
665
666 cancelAction(mutatedJobs);
667
668 if (!mutatedJobs.isEmpty()) {
669 addMutatedJobs(mutatedJobs);
670 }
671}
672
674 : KisAsynchronousStrokeUpdateHelper::UpdateData(_forceUpdate, BARRIER, NORMAL)
675{
676}
677
679{
680 return new BarrierUpdateData(*this, levelOfDetail);
681}
682
687
689{
690 QMutexLocker l(&m_d->commandsMutex);
691 KUndo2CommandSP sharedCommand = toQShared(cmd);
692 executeCommand(sharedCommand, false);
693 m_d->commands.append({group, sharedCommand, seq});
694}
695
697{
698 for (auto it = m_d->commands.begin(); it != m_d->commands.end(); ++it) {
699 if (it->commandGroup == Clear) {
700 notifyCommandDone(it->command, it->sequentiality, KisStrokeJobData::NORMAL);
701 }
702 }
703
705
706 for (auto it = m_d->commands.begin(); it != m_d->commands.end(); ++it) {
707 if (it->commandGroup == Transform) {
708 notifyCommandDone(it->command, it->sequentiality, KisStrokeJobData::NORMAL);
709 }
710 }
711}
712
714{
715 for (auto it = std::make_reverse_iterator(m_d->commands.end());
716 it != std::make_reverse_iterator(m_d->commands.begin());
717 ++it) {
718
719 executeCommand(it->command, true);
720 }
721
722 m_d->commands.clear();
723}
724
726{
727 for (auto it = std::make_reverse_iterator(m_d->commands.end());
728 it != std::make_reverse_iterator(m_d->commands.begin());) {
729
730 if ((levelOfDetail > 0 &&
731 (it->commandGroup == TransformLod || it->commandGroup == TransformLodTemporary)) ||
732 (levelOfDetail <= 0 &&
733 (it->commandGroup == Transform || it->commandGroup == TransformTemporary))) {
734
735 executeCommand(it->command, true);
736 it = std::make_reverse_iterator(m_d->commands.erase(std::next(it).base()));
737 } else {
738 ++it;
739 }
740 }
741}
742
744{
745 KisBatchNodeUpdate &dirtyRects = m_d->effectiveDirtyRects(levelOfDetail);
746 KisBatchNodeUpdate &prevDirtyRects = m_d->effectivePrevDirtyRects(levelOfDetail);
747
748 *updateData = (prevDirtyRects | dirtyRects).compressed();
749
750 KisBatchNodeUpdate savedUndoRects = dirtyRects;
751
752 if (levelOfDetail > 0) {
753
754 for (auto it = savedUndoRects.begin(); it != savedUndoRects.end(); ++it) {
755 it->second = KisLodTransform::upscaledRect(it->second, levelOfDetail);
756 }
757 }
758
759 *m_d->updateDataForUndo = (m_d->initialUpdatesBeforeClear | savedUndoRects).compressed();
760 prevDirtyRects.clear();
761 dirtyRects.swap(prevDirtyRects);
762}
763
765{
766 KisPaintDeviceSP device = node->paintDevice();
767
768 CommandGroup commandGroup =
769 levelOfDetail > 0 ? TransformLod : Transform;
770
771 if (KisExternalLayer *extLayer =
772 dynamic_cast<KisExternalLayer*>(node.data())) {
773
774 if (config.mode() == ToolTransformArgs::FREE_TRANSFORM ||
776 extLayer->supportsPerspectiveTransform())) {
777
778 if (levelOfDetail <= 0) {
779 const QRect oldDirtyRect = extLayer->projectionPlane()->tightUserVisibleBounds() | extLayer->theoreticalBoundingRect();
780
782 QTransform t = w.transform();
783 KUndo2Command *cmd = extLayer->transform(t);
784
786
789 if (KisShapeLayer *shapeLayer = dynamic_cast<KisShapeLayer*>(extLayer)) {
790 shapeLayer->forceUpdateTimedNode();
791 }
792
798 const QRect theoreticalNewDirtyRect =
799 kisGrowRect(t.mapRect(oldDirtyRect), 1);
800
801 addDirtyRect(node,
802 oldDirtyRect |
803 theoreticalNewDirtyRect |
804 extLayer->projectionPlane()->tightUserVisibleBounds() |
805 extLayer->theoreticalBoundingRect(), 0);
806 return;
807 } else {
808 device = node->projection();
809 commandGroup = TransformLodTemporary;
810 }
811 }
812
813 } else {
814 device = node->paintDevice();
815 }
816
817 if (device) {
818 KisPaintDeviceSP cachedPortion;
819
820 {
821 QMutexLocker l(&m_d->devicesCacheMutex);
822 cachedPortion = m_d->devicesCacheHash[device.data()];
823 }
824
825 KIS_SAFE_ASSERT_RECOVER_RETURN(cachedPortion);
826
827 KisTransaction transaction(device);
828
830 KisTransformUtils::transformAndMergeDevice(config, cachedPortion,
831 device, &helper);
832
834 addDirtyRect(node, cachedPortion->extent() | node->projectionPlane()->tightUserVisibleBounds(), levelOfDetail);
835
836 } else if (KisTransformMask *transformMask =
837 dynamic_cast<KisTransformMask*>(node.data())) {
838
839 const QRect oldDirtyRect = transformMask->extent();
840
841 if (levelOfDetail > 0) {
842 KisPaintDeviceSP cachedPortion;
843
844 {
845 QMutexLocker l(&m_d->devicesCacheMutex);
846 cachedPortion = m_d->transformMaskCacheHash[transformMask];
847 }
848
849 KIS_SAFE_ASSERT_RECOVER_RETURN(cachedPortion);
850
851 KisPaintDeviceSP dst = new KisPaintDevice(cachedPortion->colorSpace());
852 dst->prepareClone(cachedPortion);
853
855 KisTransformUtils::transformAndMergeDevice(config, cachedPortion,
856 dst, &helper);
857
858 transformMask->overrideStaticCacheDevice(dst);
859 }
860
861 {
862 KUndo2Command *cmd = new KisSimpleModifyTransformMaskCommand(transformMask,
864 new KisTransformMaskAdapter(config)),
865 m_d->commandUpdatesBlockerCookie);
867 addDirtyRect(node, oldDirtyRect | transformMask->extent(), levelOfDetail);
868 }
869
870 }
871}
872
874{
875 KisPaintDeviceSP device;
876 CommandGroup commandGroup = Clear;
877
878 if (KisExternalLayer *extLayer =
879 dynamic_cast<KisExternalLayer*>(node.data())) {
880
883 extLayer->supportsPerspectiveTransform())) {
884
885 device = node->projection();
886 commandGroup = ClearTemporary;
887 }
888 } else if (KisTransformMask *mask = dynamic_cast<KisTransformMask*>(node.data())) {
890
891 // NOTE: this action should be either sequential or barrier
892 QMutexLocker l(&m_d->devicesCacheMutex);
893 if (!m_d->transformMaskCacheHash.contains(mask)) {
894
895 KisPaintDeviceSP dev = mask->buildSourcePreviewDevice();
896 m_d->transformMaskCacheHash.insert(mask, new KisPaintDevice(*dev));
897
898 return;
899 }
900
901 } else {
902 device = node->paintDevice();
903 }
904
905 if (device) {
906
907 {
908 QMutexLocker l(&m_d->devicesCacheMutex);
909
910 if (!m_d->devicesCacheHash.contains(device.data())) {
911 KisPaintDeviceSP cache;
912
913 // The image that will be transformed is linked to the original
914 // layer. We copy existing pixels or use an external source.
915 if (m_d->initialTransformArgs.externalSource()) {
916 cache = device->createCompositionSourceDevice(m_d->initialTransformArgs.externalSource());
917 } else if (m_d->selection) {
918 QRect srcRect = m_d->selection->selectedExactRect();
919
920 cache = device->createCompositionSourceDevice();
921 KisPainter gc(cache);
922 gc.setSelection(m_d->selection);
923 gc.bitBlt(srcRect.topLeft(), device, srcRect);
924 } else {
925 cache = device->createCompositionSourceDevice(device);
926 }
927
928 m_d->devicesCacheHash.insert(device.data(), cache);
929 }
930 }
931
932 // Don't clear the selection or layer when the source is external
933 if (m_d->initialTransformArgs.externalSource()) return;
934
935 KisTransaction transaction(device);
936 if (m_d->selection) {
937 device->clearSelection(m_d->selection);
938 } else {
939 QRect oldExtent = device->extent();
940 device->clear();
941 device->setDirty(oldExtent);
942 }
943
945 }
946}
947
949 QVector<KisStrokeJobData *> &mutatedJobs,
950 int levelOfDetail,
951 bool useHoldUI)
952{
953 if (levelOfDetail > 0) {
955 }
956
958
959 CommandGroup commandGroup =
960 levelOfDetail > 0 ? TransformLod : Transform;
961
962 KritaUtils::addJobBarrier(mutatedJobs, levelOfDetail,
963 [this, args, levelOfDetail, updateData, useHoldUI, commandGroup]() {
964
965 // it has its own dirty requests blocking inside
966 undoTransformCommands(levelOfDetail);
967
968 if (useHoldUI) {
970 }
971
973 });
974
975 Q_FOREACH (KisNodeSP node, m_d->processedNodes) {
976 KritaUtils::addJobConcurrent(mutatedJobs, levelOfDetail,
977 [this, node, args, levelOfDetail]() {
978 transformNode(node, args, levelOfDetail);
979 });
980 }
981
982 KritaUtils::addJobBarrier(mutatedJobs, levelOfDetail,
983 [this, levelOfDetail, updateData, useHoldUI, commandGroup]() {
984
985 fetchAllUpdateRequests(levelOfDetail, updateData);
986
988 executeAndAddCommand(new KisUpdateCommandEx(m_d->updateDataForUndo, m_d->updatesFacade, KisUpdateCommandEx::FINALIZING, m_d->commandUpdatesBlockerCookie), commandGroup, KisStrokeJobData::BARRIER);
989
990 if (useHoldUI) {
992 }
993
999 for (auto it = updateData->begin(); it != updateData->end(); ++it) {
1000 KisTransformMask *transformMask = dynamic_cast<KisTransformMask*>(it->first.data());
1001
1002 if (transformMask &&
1003 ((levelOfDetail <= 0 && !transformMask->transformParams()->isAffine()) ||
1004 (levelOfDetail <= 0 && m_d->previewLevelOfDetail > 0))) {
1005
1006 transformMask->threadSafeForceStaticImageUpdate(it->second);
1007 } else {
1008 m_d->updatesFacade->refreshGraphAsync(it->first, it->second);
1009 }
1010
1011 }
1012 });
1013}
1014
1016{
1017 KritaUtils::addJobBarrier(mutatedJobs, [this]() {
1018 Q_FOREACH (KisSelectionSP selection, m_d->deactivatedSelections) {
1019 selection->setVisible(true);
1020 }
1021
1022 Q_FOREACH(KisSelectionMaskSP deactivatedOverlaySelectionMask, m_d->deactivatedOverlaySelectionMasks) {
1023 deactivatedOverlaySelectionMask->selection()->setVisible(true);
1024 deactivatedOverlaySelectionMask->setDirty();
1025 }
1026
1027 m_d->commandUpdatesBlockerCookie.reset();
1028 });
1029
1030
1031 if (saveCommands) {
1032 KritaUtils::addJobBarrier(mutatedJobs, [this]() {
1034 m_d->commands.clear();
1035 });
1036 }
1037}
1038
1040{
1041 const QVector<QRect> finalDirtyRects =
1044
1045 Q_FOREACH (const QRect &rc, finalDirtyRects) {
1046 KritaUtils::addJobConcurrent(mutatedJobs, [rc, updatesFacade] () {
1047 updatesFacade->notifyUIUpdateCompleted(rc);
1048 });
1049 }
1050}
1051
1053{
1061 if (m_d->currentTransformArgs.isUnchanging() &&
1062 m_d->transformMaskCacheHash.isEmpty() &&
1063 !m_d->overriddenCommand) {
1064
1065 cancelAction(mutatedJobs);
1066 return;
1067 }
1068
1069 if (m_d->previewLevelOfDetail > 0) {
1076 KritaUtils::addJobBarrier(mutatedJobs, [this]() { Q_UNUSED(this) });
1077
1078 if (!m_d->transformMaskCacheHash.isEmpty()) {
1079 KritaUtils::addJobSequential(mutatedJobs, [this]() {
1080 Q_FOREACH (KisTransformMask *mask, m_d->transformMaskCacheHash.keys()) {
1082 }
1083
1089 undoTransformCommands(m_d->previewLevelOfDetail);
1090 });
1091 }
1092
1093 reapplyTransform(m_d->currentTransformArgs, mutatedJobs, 0, true);
1094
1100 KritaUtils::addJobBarrier(mutatedJobs, [this]() { Q_UNUSED(this) });
1101 repopulateUI(mutatedJobs, m_d->updatesFacade, m_d->updatesFacade->bounds());
1102
1103 } else {
1104 if (m_d->pendingUpdateArgs) {
1105 mutatedJobs << new BarrierUpdateData(true);
1106 }
1107 }
1108
1109 mutatedJobs << new UpdateTransformData(m_d->currentTransformArgs,
1111
1112 // the rest of the transform finishing work cannot be cancelled...
1113 KritaUtils::addJobBarrier(mutatedJobs, [this]() {
1114 m_d->strokeCompletionHasBeenStarted = true;
1115
1116 QVector<KisStrokeJobData *> nonCancellableFinishJobs;
1117
1118 finalizeStrokeImpl(nonCancellableFinishJobs, true);
1119
1120 KritaUtils::addJobBarrier(nonCancellableFinishJobs, [this]() {
1122 });
1123
1124 for (auto it = nonCancellableFinishJobs.begin(); it != nonCancellableFinishJobs.end(); ++it) {
1125 (*it)->setCancellable(false);
1126 }
1127
1128 this->addMutatedJobs(nonCancellableFinishJobs);
1129
1130 });
1131}
1132
1134{
1140 if (m_d->strokeCompletionHasBeenStarted) return;
1141
1142
1143 KIS_SAFE_ASSERT_RECOVER_NOOP(m_d->transformMaskCacheHash.isEmpty() ||
1144 (m_d->transformMaskCacheHash.size() == 1 && m_d->processedNodes.size() == 1));
1145
1146 const bool isChangingTransformMask = !m_d->transformMaskCacheHash.isEmpty();
1147
1148 if (m_d->initialTransformArgs.isIdentity()) {
1149 KritaUtils::addJobSequential(mutatedJobs, [this]() {
1150 Q_FOREACH (KisTransformMask *mask, m_d->transformMaskCacheHash.keys()) {
1152 }
1153 });
1154
1155
1156 KritaUtils::addJobBarrier(mutatedJobs, [this]() {
1157 m_d->commandUpdatesBlockerCookie.reset();
1160 });
1161
1162 if (m_d->previewLevelOfDetail > 0) {
1168 KritaUtils::addJobBarrier(mutatedJobs, [this]() { Q_UNUSED(this) });
1169 repopulateUI(mutatedJobs, m_d->updatesFacade, m_d->updatesFacade->bounds());
1170 }
1171
1172 finalizeStrokeImpl(mutatedJobs, false);
1173
1174 KritaUtils::addJobSequential(mutatedJobs, [this]() {
1175 Q_FOREACH (KisTransformMask *mask, m_d->transformMaskCacheHash.keys()) {
1177 }
1178 });
1179
1180 KritaUtils::addJobBarrier(mutatedJobs, [this]() {
1182 });
1183 } else {
1184 KIS_SAFE_ASSERT_RECOVER_NOOP(isChangingTransformMask || m_d->overriddenCommand);
1185
1186 KritaUtils::addJobSequential(mutatedJobs, [this]() {
1187 Q_FOREACH (KisTransformMask *mask, m_d->transformMaskCacheHash.keys()) {
1189 }
1190 });
1191
1192 reapplyTransform(m_d->initialTransformArgs, mutatedJobs, 0, true);
1193
1194 if (m_d->previewLevelOfDetail > 0) {
1200 KritaUtils::addJobBarrier(mutatedJobs, [this]() { Q_UNUSED(this) });
1201 repopulateUI(mutatedJobs, m_d->updatesFacade, m_d->updatesFacade->bounds());
1202 }
1203
1204 mutatedJobs << new UpdateTransformData(m_d->initialTransformArgs,
1206
1207 finalizeStrokeImpl(mutatedJobs, bool(m_d->overriddenCommand));
1208
1209 KritaUtils::addJobSequential(mutatedJobs, [this]() {
1210 Q_FOREACH (KisTransformMask *mask, m_d->transformMaskCacheHash.keys()) {
1212 }
1213 });
1214
1215 if (m_d->overriddenCommand) {
1216 KritaUtils::addJobBarrier(mutatedJobs, [this]() {
1217 m_d->currentTransformArgs = m_d->initialTransformArgs;
1219 });
1220 } else {
1221 KritaUtils::addJobBarrier(mutatedJobs, [this]() {
1223 });
1224 }
1225 }
1226}
1227
1228void InplaceTransformStrokeStrategy::addDirtyRect(KisNodeSP node, const QRect &rect, int levelOfDetail) {
1229 QMutexLocker l(&m_d->dirtyRectsMutex);
1230 m_d->effectiveDirtyRects(levelOfDetail).addUpdate(node, rect);
1231}
const KoID AlphaColorModelID("A", ki18n("Alpha mask"))
void finalizeStrokeImpl(QVector< KisStrokeJobData * > &mutatedJobs, bool saveCommands)
void finishAction(QVector< KisStrokeJobData * > &mutatedJobs)
void addDirtyRect(KisNodeSP node, const QRect &rect, int levelOfDetail)
void sigConvexHullCalculated(QPolygon convexHull, void *cookie)
static void repopulateUI(QVector< KisStrokeJobData * > &mutatedJobs, KisUpdatesFacade *updatesFacade, const QRect &dirtyRect)
InplaceTransformStrokeStrategy(ToolTransformArgs::TransformMode mode, const QString &filterId, bool forceReset, KisNodeList rootNodes, KisSelectionSP selection, KisPaintDeviceSP externalSource, KisStrokeUndoFacade *undoFacade, KisUpdatesFacade *updatesFacade, KisNodeSP imageRoot, bool forceLodMode)
void transformNode(KisNodeSP node, const ToolTransformArgs &config, int levelOfDetail)
void fetchAllUpdateRequests(int levelOfDetail, KisBatchNodeUpdateSP updateData)
void cancelAction(QVector< KisStrokeJobData * > &mutatedJobs)
void doStrokeCallback(KisStrokeJobData *data) override
void sigTransactionGenerated(TransformTransactionProperties transaction, ToolTransformArgs args, void *cookie)
void postProcessToplevelCommand(KUndo2Command *command) override
Applies some modifications (e.g. assigning extra data) to the toplevel command.
void reapplyTransform(ToolTransformArgs args, QVector< KisStrokeJobData * > &mutatedJobs, int levelOfDetail, bool useHoldUI)
void executeAndAddCommand(KUndo2Command *cmd, CommandGroup group, KisStrokeJobData::Sequentiality seq)
KisBatchNodeUpdate compressed() const
void addUpdate(KisNodeSP node, const QRect &rc)
virtual bool decorationsVisible() const =0
virtual void setDecorationsVisible(bool value, bool update)=0
static QRect upscaledRect(const QRect &srcRect, int lod)
static qreal lodToScale(int levelOfDetail)
KisPaintInformation map(KisPaintInformation pi) const
void setDirty(const QRect &rc)
virtual void clear()
KisPaintDeviceSP createCompositionSourceDevice() const
virtual const KoColorSpace * compositionSourceColorSpace() const
QRect exactBounds() const
QRect extent() const
const KoColorSpace * colorSpace() const
void clearSelection(KisSelectionSP selection)
void prepareClone(KisPaintDeviceSP src)
void setSelection(KisSelectionSP selection)
void bitBlt(qint32 dstX, qint32 dstY, const KisPaintDeviceSP srcDev, qint32 srcX, qint32 srcY, qint32 srcWidth, qint32 srcHeight)
virtual void postProcessToplevelCommand(KUndo2Command *command)
Applies some modifications (e.g. assigning extra data) to the toplevel command.
void notifyCommandDone(KUndo2CommandSP command, KisStrokeJobData::Sequentiality sequentiality, KisStrokeJobData::Exclusivity exclusivity)
void runAndSaveCommand(KUndo2CommandSP command, KisStrokeJobData::Sequentiality sequentiality, KisStrokeJobData::Exclusivity exclusivity)
void doStrokeCallback(KisStrokeJobData *data) override
void executeCommand(KUndo2CommandSP command, bool undo)
KisLodPreferences currentLodPreferences() const
void addMutatedJobs(const QVector< KisStrokeJobData * > list)
void addMutatedJob(KisStrokeJobData *data)
void setNeedsExplicitCancel(bool value)
KUndo2Command * endAndTake()
static void transformDevice(const ToolTransformArgs &config, KisPaintDeviceSP device, KisProcessingVisitor::ProgressHelper *helper)
static KisTransformWorker createTransformWorker(const ToolTransformArgs &config, KisPaintDeviceSP device, KoUpdaterPtr updater)
static ToolTransformArgs resetArgsForMode(ToolTransformArgs::TransformMode mode, const QString &filterId, const TransformTransactionProperties &transaction, KisPaintDeviceSP externalSource)
static int fetchCurrentImageTime(KisNodeList rootNodes)
static void postProcessToplevelCommand(KUndo2Command *command, const ToolTransformArgs &args, KisNodeList rootNodes, KisNodeList processedNodes, int currentTime, const KisSavedMacroCommand *overriddenCommand)
static QList< KisNodeSP > fetchNodesList(ToolTransformArgs::TransformMode mode, KisNodeList rootNodes, bool isExternalSourcePresent, KisSelectionSP selection)
static KisNodeSP tryOverrideRootToTransformMask(KisNodeSP root)
static void transformAndMergeDevice(const ToolTransformArgs &config, KisPaintDeviceSP src, KisPaintDeviceSP dst, KisProcessingVisitor::ProgressHelper *helper)
static bool tryInitArgsFromNode(KisNodeList rootNodes, ToolTransformArgs *args)
static bool tryFetchArgsFromCommandAndUndo(ToolTransformArgs *outArgs, ToolTransformArgs::TransformMode mode, KisNodeList currentNodes, KisNodeList selectedNodes, KisStrokeUndoFacade *undoFacade, int currentTime, QVector< KisStrokeJobData * > *undoJobs, const KisSavedMacroCommand **overriddenCommand)
virtual void notifyUIUpdateCompleted(const QRect &rc)=0
virtual KoID colorModelId() const =0
void scale3dSrcAndDst(qreal scale)
double boundsRotation() const
TransformMode mode() const
#define KIS_SAFE_ASSERT_RECOVER_RETURN(cond)
Definition kis_assert.h:128
#define KIS_SAFE_ASSERT_RECOVER_NOOP(cond)
Definition kis_assert.h:130
#define ENTER_FUNCTION()
Definition kis_debug.h:178
#define ppVar(var)
Definition kis_debug.h:155
T kisGrowRect(const T &rect, U offset)
Definition kis_global.h:186
QSharedPointer< T > toQShared(T *ptr)
QSharedPointer< KisTransformMaskParamsInterface > KisTransformMaskParamsInterfaceSP
QSharedPointer< KUndo2Command > KUndo2CommandSP
Definition kis_types.h:262
KisSharedPtr< KisNode > KisNodeSP
Definition kis_types.h:86
KUndo2MagicString kundo2_i18n(const char *text)
auto maxDimension(Size size) -> decltype(size.width())
KUndo2Command * tryAutoCreateDuplicatedFrame(KisPaintDeviceSP device, AutoCreateKeyframeFlags flags)
create a new duplicated keyframe if auto-keyframe mode is on
QPolygon findConvexHullSelectionLike(KisPaintDeviceSP device)
QPolygon findConvexHull(const QVector< QPoint > &points)
void forceAllHiddenOriginalsUpdate(KisNodeSP root)
void forceAllDelayedNodesUpdate(KisNodeSP root)
void addJobConcurrent(QVector< Job * > &jobs, Func func)
void addJobSequential(QVector< Job * > &jobs, Func func)
void addJobBarrier(QVector< Job * > &jobs, Func func)
QVector< QRect > splitRectIntoPatchesTight(const QRect &rc, const QSize &patchSize)
QSize optimalPatchSize()
KisStrokeJobData * createLodClone(int levelOfDetail) override
KisBatchNodeUpdate & effectiveDirtyRects(int levelOfDetail)
KisBatchNodeUpdate & effectivePrevDirtyRects(int levelOfDetail)
QHash< KisTransformMask *, KisPaintDeviceSP > transformMaskCacheHash
QHash< KisPaintDevice *, KisPaintDeviceSP > devicesCacheHash
QVector< KisDecoratedNodeInterface * > disabledDecoratedNodes
virtual KisPaintDeviceSP projection() const =0
virtual QRect exactBounds() const
virtual KisPaintDeviceSP paintDevice() const =0
bool hasEditablePaintDevice() const
int desiredLevelOfDetail() const
bool lodSupported() const
bool lodPreferred() const
KisSelectionSP selection
Definition kis_mask.cc:44
virtual KisNode * graphOverlayNode() const
virtual KisAbstractProjectionPlaneSP projectionPlane() const
Definition kis_node.cpp:240
KisProjectionLeafSP projectionLeaf
Definition kis_node.cpp:93
KisNodeGraphListener * graphListener
Definition kis_node.cpp:87
void setDirty(const QVector< QRect > &rects) override
void setDecorationsVisible(bool value, bool update) override
void setVisible(bool visible)
void threadSafeForceStaticImageUpdate(const QRect &extraUpdateRect)
void overrideStaticCacheDevice(KisPaintDeviceSP device)