File indexing completed on 2024-12-22 04:16:51

0001 /*
0002  *  SPDX-FileCopyrightText: 2013,2020 Dmitry Kazakov <dimula73@gmail.com>
0003  *
0004  *  SPDX-License-Identifier: GPL-2.0-or-later
0005  */
0006 
0007 #include "inplace_transform_stroke_strategy.h"
0008 
0009 #include <QMutexLocker>
0010 #include "kundo2commandextradata.h"
0011 
0012 #include "kis_node_progress_proxy.h"
0013 
0014 #include <klocalizedstring.h>
0015 #include <kis_node.h>
0016 #include <kis_group_layer.h>
0017 #include <kis_external_layer_iface.h>
0018 #include <kis_transaction.h>
0019 #include <kis_painter.h>
0020 #include <kis_transform_worker.h>
0021 #include <kis_transform_mask.h>
0022 #include "kis_transform_mask_adapter.h"
0023 #include "kis_transform_utils.h"
0024 #include "kis_abstract_projection_plane.h"
0025 #include "kis_recalculate_transform_mask_job.h"
0026 
0027 #include "kis_projection_leaf.h"
0028 #include "commands_new/KisSimpleModifyTransformMaskCommand.h"
0029 #include "KisAnimAutoKey.h"
0030 
0031 #include "kis_sequential_iterator.h"
0032 #include "kis_selection_mask.h"
0033 #include "kis_image_config.h"
0034 #include "kis_layer_utils.h"
0035 #include <QQueue>
0036 #include <KisDeleteLaterWrapper.h>
0037 #include "transform_transaction_properties.h"
0038 #include "krita_container_utils.h"
0039 #include "commands_new/kis_saved_commands.h"
0040 #include "commands_new/KisLazyCreateTransformMaskKeyframesCommand.h"
0041 #include "kis_command_ids.h"
0042 #include "KisRunnableStrokeJobUtils.h"
0043 #include "commands_new/KisHoldUIUpdatesCommand.h"
0044 #include "KisDecoratedNodeInterface.h"
0045 #include "kis_sync_lod_cache_stroke_strategy.h"
0046 #include "kis_lod_transform.h"
0047 #include <boost/optional.hpp>
0048 #include "kis_selection_mask.h"
0049 #include "kis_undo_stores.h"
0050 #include "kis_transparency_mask.h"
0051 #include "commands_new/KisDisableDirtyRequestsCommand.h"
0052 #include <kis_shape_layer.h>
0053 #include "kis_raster_keyframe_channel.h"
0054 #include "kis_image_animation_interface.h"
0055 #include "KisAnimAutoKey.h"
0056 
0057 
0058 struct InplaceTransformStrokeStrategy::Private
0059 {
0060     // initial conditions passed from the tool
0061     KisUpdatesFacade *updatesFacade;
0062     KisStrokeUndoFacade *undoFacade;
0063     ToolTransformArgs::TransformMode mode;
0064     QString filterId;
0065     bool forceReset;
0066     KisNodeList rootNodes;
0067     KisSelectionSP selection;
0068     KisPaintDeviceSP externalSource;
0069     KisNodeSP imageRoot;
0070     int currentTime = -1; // NOTE: initialized asynchronously in initStrokeCallback!
0071     int previewLevelOfDetail = -1;
0072     bool forceLodMode = true;
0073 
0074     // properties filled by initialization/transformation routines
0075     KisNodeList processedNodes;
0076     ToolTransformArgs initialTransformArgs;
0077     ToolTransformArgs currentTransformArgs;
0078 
0079     const KisSavedMacroCommand *overriddenCommand = 0;
0080 
0081     QList<KisSelectionSP> deactivatedSelections;
0082     QList<KisSelectionMaskSP> deactivatedOverlaySelectionMasks;
0083 
0084     QMutex commandsMutex;
0085 
0086     struct SavedCommand {
0087         CommandGroup commandGroup;
0088         KUndo2CommandSP command;
0089         KisStrokeJobData::Sequentiality sequentiality;
0090     };
0091 
0092     QVector<SavedCommand> commands;
0093 
0094     QMutex devicesCacheMutex;
0095     QHash<KisPaintDevice*, KisPaintDeviceSP> devicesCacheHash;
0096     QHash<KisTransformMask*, KisPaintDeviceSP> transformMaskCacheHash;
0097 
0098     QMutex dirtyRectsMutex;
0099     KisBatchNodeUpdate dirtyRects;
0100     KisBatchNodeUpdate prevDirtyRects;
0101 
0102     KisBatchNodeUpdate dirtyPreviewRects;
0103     KisBatchNodeUpdate prevDirtyPreviewRects;
0104 
0105     inline KisBatchNodeUpdate& effectiveDirtyRects(int levelOfDetail) {
0106         return levelOfDetail > 0 ? dirtyPreviewRects : dirtyRects;
0107     }
0108 
0109     inline KisBatchNodeUpdate& effectivePrevDirtyRects(int levelOfDetail) {
0110         return levelOfDetail > 0 ? prevDirtyPreviewRects : prevDirtyRects;
0111     }
0112 
0113     // data for asynchronous updates
0114     boost::optional<ToolTransformArgs> pendingUpdateArgs;
0115     QElapsedTimer updateTimer;
0116     const int updateInterval = 30;
0117 
0118     // temporary variable to share data between jobs at the initialization phase
0119     QVector<KisDecoratedNodeInterface*> disabledDecoratedNodes;
0120 
0121     /**
0122      * A special cookie-object, which blocks updates in transform mask
0123      * modification commands until the stroke ends. As soon as the stroke
0124      * ends, the object is destroyed and the transform mask modification
0125      * commands start to behave normally.
0126      */
0127     QSharedPointer<boost::none_t> commandUpdatesBlockerCookie;
0128 
0129     bool strokeCompletionHasBeenStarted = false;
0130 
0131     KisBatchNodeUpdateSP updateDataForUndo;
0132     KisBatchNodeUpdate initialUpdatesBeforeClear;
0133 };
0134 
0135 
0136 InplaceTransformStrokeStrategy::InplaceTransformStrokeStrategy(ToolTransformArgs::TransformMode mode,
0137                                                                const QString &filterId,
0138                                                                bool forceReset,
0139                                                                KisNodeList rootNodes,
0140                                                                KisSelectionSP selection,
0141                                                                KisPaintDeviceSP externalSource,
0142                                                                KisStrokeUndoFacade *undoFacade,
0143                                                                KisUpdatesFacade *updatesFacade,
0144                                                                KisNodeSP imageRoot,
0145                                                                bool forceLodMode)
0146     : KisStrokeStrategyUndoCommandBased(kundo2_i18n("Transform"), false, undoFacade),
0147       m_d(new Private())
0148 {
0149 
0150     m_d->mode = mode;
0151     m_d->filterId = filterId;
0152     m_d->forceReset = forceReset;
0153     m_d->rootNodes = rootNodes;
0154     m_d->selection = selection;
0155     m_d->externalSource = externalSource;
0156     m_d->updatesFacade = updatesFacade;
0157     m_d->undoFacade = undoFacade;
0158     m_d->imageRoot = imageRoot;
0159     m_d->forceLodMode = forceLodMode;
0160     m_d->commandUpdatesBlockerCookie.reset(new boost::none_t(boost::none));
0161 
0162     if (selection) {
0163         Q_FOREACH(KisNodeSP node, rootNodes) {
0164             KIS_SAFE_ASSERT_RECOVER_NOOP(!dynamic_cast<KisTransformMask*>(node.data()));
0165         }
0166     }
0167     setMacroId(KisCommandUtils::TransformToolId);
0168 
0169     // TODO: check if can be relaxed
0170     setNeedsExplicitCancel(true);
0171 }
0172 
0173 InplaceTransformStrokeStrategy::~InplaceTransformStrokeStrategy()
0174 {
0175 }
0176 
0177 void InplaceTransformStrokeStrategy::doStrokeCallback(KisStrokeJobData *data)
0178 {
0179     if (UpdateTransformData *upd = dynamic_cast<UpdateTransformData*>(data)) {
0180         if (upd->destination == UpdateTransformData::PAINT_DEVICE) {
0181             m_d->pendingUpdateArgs = upd->args;
0182             tryPostUpdateJob(false);
0183         } else if (m_d->selection) {
0184             // NOTE: selection is hidden during the transformation, so we
0185             //       don't have to do any preview for that. We transform
0186             //       that in one go in the end of the stroke.
0187 
0188             KisTransaction transaction(m_d->selection->pixelSelection());
0189 
0190             KisProcessingVisitor::ProgressHelper helper(m_d->imageRoot.data());
0191             KisTransformUtils::transformDevice(upd->args,
0192                                                m_d->selection->pixelSelection(), &helper);
0193 
0194             runAndSaveCommand(KUndo2CommandSP(transaction.endAndTake()),
0195                               KisStrokeJobData::CONCURRENT, KisStrokeJobData::NORMAL);
0196         }
0197 
0198     } else if (BarrierUpdateData *barrierData = dynamic_cast<BarrierUpdateData *>(data)) {
0199 
0200         doCanvasUpdate(barrierData->forceUpdate);
0201 
0202     } else if (KisAsynchronousStrokeUpdateHelper::UpdateData *updateData =
0203                dynamic_cast<KisAsynchronousStrokeUpdateHelper::UpdateData*>(data)) {
0204 
0205         tryPostUpdateJob(updateData->forceUpdate);
0206 
0207     } else {
0208         KisStrokeStrategyUndoCommandBased::doStrokeCallback(data);
0209     }
0210 
0211 }
0212 
0213 void InplaceTransformStrokeStrategy::tryPostUpdateJob(bool forceUpdate)
0214 {
0215     if (!m_d->pendingUpdateArgs) return;
0216 
0217     if (forceUpdate ||
0218         (m_d->updateTimer.elapsed() > m_d->updateInterval &&
0219          !m_d->updatesFacade->hasUpdatesRunning())) {
0220 
0221         addMutatedJob(new BarrierUpdateData(forceUpdate));
0222     }
0223 }
0224 
0225 void InplaceTransformStrokeStrategy::doCanvasUpdate(bool forceUpdate)
0226 {
0227     if (!m_d->pendingUpdateArgs) return;
0228 
0229     if (!forceUpdate &&
0230             (m_d->updateTimer.elapsed() < m_d->updateInterval ||
0231              m_d->updatesFacade->hasUpdatesRunning())) {
0232 
0233         return;
0234     }
0235 
0236     QVector<KisStrokeJobData *> jobs;
0237 
0238     ToolTransformArgs args = *m_d->pendingUpdateArgs;
0239     m_d->pendingUpdateArgs = boost::none;
0240 
0241     reapplyTransform(args, jobs, m_d->previewLevelOfDetail, false);
0242 
0243     KritaUtils::addJobBarrier(jobs, [this, args]() {
0244         m_d->currentTransformArgs = args;
0245         m_d->updateTimer.restart();
0246         // sanity check that no job has been squeezed in between
0247         KIS_SAFE_ASSERT_RECOVER_RETURN(!m_d->pendingUpdateArgs);
0248     });
0249 
0250     addMutatedJobs(jobs);
0251 }
0252 
0253 int InplaceTransformStrokeStrategy::calculatePreferredLevelOfDetail(const QRect &srcRect)
0254 {
0255     KisLodPreferences lodPreferences = this->currentLodPreferences();
0256     if (!lodPreferences.lodSupported() ||
0257         !(lodPreferences.lodPreferred() || m_d->forceLodMode)) return -1;
0258 
0259     const int maxSize = 2000;
0260     const int maxDimension = KisAlgebra2D::maxDimension(srcRect);
0261 
0262     const qreal zoom = qMax(1.0, qreal(maxDimension) / maxSize);
0263 
0264     const int calculatedLod = qCeil(std::log2(zoom));
0265 
0266     return qMax(calculatedLod, lodPreferences.desiredLevelOfDetail());
0267 
0268 }
0269 
0270 
0271 void InplaceTransformStrokeStrategy::postProcessToplevelCommand(KUndo2Command *command)
0272 {
0273     KisTransformUtils::postProcessToplevelCommand(command,
0274                                                   m_d->currentTransformArgs,
0275                                                   m_d->rootNodes,
0276                                                   m_d->processedNodes,
0277                                                   m_d->currentTime,
0278                                                   m_d->overriddenCommand);
0279 
0280     KisStrokeStrategyUndoCommandBased::postProcessToplevelCommand(command);
0281 }
0282 
0283 
0284 
0285 void InplaceTransformStrokeStrategy::initStrokeCallback()
0286 {
0287     KisStrokeStrategyUndoCommandBased::initStrokeCallback();
0288 
0289     m_d->currentTime = KisTransformUtils::fetchCurrentImageTime(m_d->rootNodes);
0290 
0291     QVector<KisStrokeJobData *> extraInitJobs;
0292 
0293     if (m_d->selection) {
0294         m_d->selection->setVisible(false);
0295         m_d->deactivatedSelections.append(m_d->selection);
0296     }
0297 
0298     Q_FOREACH(KisNodeSP node, m_d->rootNodes) {
0299         KisSelectionMaskSP overlaySelectionMask =
0300                 dynamic_cast<KisSelectionMask*>(node->graphListener()->graphOverlayNode());
0301         if (overlaySelectionMask && node != KisNodeSP(overlaySelectionMask)) {
0302             overlaySelectionMask->setDecorationsVisible(false);
0303             m_d->deactivatedOverlaySelectionMasks.append(overlaySelectionMask);
0304         }
0305     }
0306 
0307     if (m_d->rootNodes.size() == 1) {
0308         KisNodeSP rootNode = m_d->rootNodes[0];
0309         rootNode = KisTransformUtils::tryOverrideRootToTransformMask(rootNode);
0310 
0311         if (rootNode->inherits("KisTransformMask") && rootNode->projectionLeaf()->isDroppedNode()) {
0312             rootNode.clear();
0313             m_d->processedNodes.clear();
0314 
0315             TransformTransactionProperties transaction(QRect(), &m_d->initialTransformArgs, m_d->rootNodes, m_d->processedNodes);
0316             Q_EMIT sigTransactionGenerated(transaction, m_d->initialTransformArgs, this);
0317             return;
0318         }
0319     }
0320 
0321     // When placing an external source image, we never work recursively on any layer masks
0322     m_d->processedNodes = KisTransformUtils::fetchNodesList(m_d->mode, m_d->rootNodes, m_d->externalSource, m_d->selection);
0323 
0324     bool argsAreInitialized = false;
0325     QVector<KisStrokeJobData *> lastCommandUndoJobs;
0326 
0327     // When externalSource is set, it means that we are initializing a new
0328     // stroke following a newActivationWithExternalSource, thus we never try
0329     // to reuse an existing transformation from the undo queue. However, when
0330     // externalSource is not set, tryFetchArgsFromCommandAndUndo may still
0331     // recover a previous stroke that referenced an external source.
0332     if (!m_d->forceReset && !m_d->externalSource) {
0333         if (KisTransformUtils::tryFetchArgsFromCommandAndUndo(&m_d->initialTransformArgs,
0334                                                               m_d->mode,
0335                                                               m_d->rootNodes,
0336                                                               m_d->processedNodes,
0337                                                               m_d->undoFacade,
0338                                                               m_d->currentTime,
0339                                                               &lastCommandUndoJobs,
0340                                                               &m_d->overriddenCommand)) {
0341             argsAreInitialized = true;
0342         } else if (KisTransformUtils::tryInitArgsFromNode(m_d->rootNodes, &m_d->initialTransformArgs)) {
0343             argsAreInitialized = true;
0344         }
0345     }
0346 
0347     KritaUtils::addJobBarrier(extraInitJobs, [this]() {
0348         Q_FOREACH (KisNodeSP node, m_d->processedNodes) {
0349             m_d->prevDirtyRects.addUpdate(node, node->projectionPlane()->tightUserVisibleBounds());
0350         }
0351 
0352         m_d->initialUpdatesBeforeClear = m_d->prevDirtyRects.compressed();
0353         m_d->updateDataForUndo.reset(new KisBatchNodeUpdate(m_d->initialUpdatesBeforeClear));
0354 
0355         executeAndAddCommand(new KisUpdateCommandEx(m_d->updateDataForUndo, m_d->updatesFacade, KisUpdateCommandEx::INITIALIZING, m_d->commandUpdatesBlockerCookie), Clear, KisStrokeJobData::BARRIER);
0356         executeAndAddCommand(new KisDisableDirtyRequestsCommand(m_d->updatesFacade, KisDisableDirtyRequestsCommand::INITIALIZING), Clear, KisStrokeJobData::BARRIER);
0357     });
0358 
0359     extraInitJobs << lastCommandUndoJobs;
0360 
0361     if (!lastCommandUndoJobs.isEmpty()) {
0362         KritaUtils::addJobBarrier(extraInitJobs, [this]() {
0363             /**
0364              * In case we are doing a continued action, we need to
0365              * set initial update to the "very initial" state that
0366              * was present before the previous stroke. So here we
0367              * just override the rect calculated before
0368              */
0369             KisBatchNodeUpdate updates;
0370 
0371             Q_FOREACH (KisNodeSP node, m_d->processedNodes) {
0372                 updates.addUpdate(node, node->projectionPlane()->tightUserVisibleBounds());
0373             }
0374 
0375             m_d->initialUpdatesBeforeClear = updates.compressed();
0376             *m_d->updateDataForUndo = m_d->initialUpdatesBeforeClear;
0377 
0378             /**
0379              * We need to make sure that the nodes will be successfully be
0380              * transformed back in case the stroke will be finished before
0381              * sigTransactionGenerated() signal is delivered.
0382              */
0383             m_d->pendingUpdateArgs = m_d->initialTransformArgs;
0384         });
0385     }
0386 
0387     KritaUtils::addJobSequential(extraInitJobs, [this]() {
0388         // When dealing with animated transform mask layers, create keyframe and save the command for undo.
0389         // NOTE: for transform masks we create a keyframe no matter what the user
0390         //       settigs are
0391         Q_FOREACH (KisNodeSP node, m_d->processedNodes) {
0392             if (KisTransformMask* transformMask = dynamic_cast<KisTransformMask*>(node.data())) {
0393                 if (KisLazyCreateTransformMaskKeyframesCommand::maskHasAnimation(transformMask)) {
0394                     runAndSaveCommand(toQShared(new KisLazyCreateTransformMaskKeyframesCommand(transformMask)), KisStrokeJobData::BARRIER, KisStrokeJobData::NORMAL);
0395                 }
0396             } else if (KisAutoKey::activeMode() > KisAutoKey::NONE &&
0397                        node->hasEditablePaintDevice()){
0398 
0399                 KUndo2Command *autoKeyframeCommand =
0400                         KisAutoKey::tryAutoCreateDuplicatedFrame(node->paintDevice(),
0401                                                                  KisAutoKey::SupportsLod);
0402                 if (autoKeyframeCommand) {
0403                     runAndSaveCommand(toQShared(autoKeyframeCommand), KisStrokeJobData::BARRIER, KisStrokeJobData::NORMAL);
0404                 }
0405             }
0406         }
0407     });
0408 
0409     KritaUtils::addJobSequential(extraInitJobs, [this]() {
0410         /**
0411           * We must request shape layers to rerender areas outside image bounds
0412           */
0413         Q_FOREACH(KisNodeSP node, m_d->rootNodes) {
0414             KisLayerUtils::forceAllHiddenOriginalsUpdate(node);
0415         }
0416     });
0417 
0418     KritaUtils::addJobBarrier(extraInitJobs, [this]() {
0419         /**
0420           * We must ensure that the currently selected subtree
0421           * has finished all its updates.
0422           */
0423         Q_FOREACH(KisNodeSP node, m_d->rootNodes) {
0424             KisLayerUtils::forceAllDelayedNodesUpdate(node);
0425         }
0426     });
0427 
0428     /// Disable all decorated nodes to generate outline
0429     /// and preview correctly. We will enable them back
0430     /// as soon as preview generation is finished.
0431     KritaUtils::addJobBarrier(extraInitJobs, [this]() {
0432         Q_FOREACH (KisNodeSP node, m_d->processedNodes) {
0433             KisDecoratedNodeInterface *decoratedNode = dynamic_cast<KisDecoratedNodeInterface*>(node.data());
0434             if (decoratedNode && decoratedNode->decorationsVisible()) {
0435                 decoratedNode->setDecorationsVisible(false);
0436                 m_d->disabledDecoratedNodes << decoratedNode;
0437             }
0438         }
0439     });
0440 
0441     KritaUtils::addJobBarrier(extraInitJobs,
0442                               [this,
0443                               argsAreInitialized]() mutable {
0444         QRect srcRect;
0445 
0446         KisPaintDeviceSP externalSource =
0447             m_d->externalSource ? m_d->externalSource :
0448             (argsAreInitialized && m_d->initialTransformArgs.externalSource()) ?
0449                 m_d->initialTransformArgs.externalSource() : 0;
0450 
0451         if (externalSource) {
0452             // Start the transformation around the visible pixels of the external image
0453             srcRect = externalSource->exactBounds();
0454         } else if (m_d->selection) {
0455             srcRect = m_d->selection->selectedExactRect();
0456         } else {
0457             srcRect = QRect();
0458             Q_FOREACH (KisNodeSP node, m_d->processedNodes) {
0459                 // group layers may have a projection of layers
0460                 // that are locked and will not be transformed
0461                 if (node->inherits("KisGroupLayer")) continue;
0462 
0463                 if (const KisTransformMask *mask = dynamic_cast<const KisTransformMask*>(node.data())) {
0464                     srcRect |= mask->sourceDataBounds();
0465                 } else if (const KisSelectionMask *mask = dynamic_cast<const KisSelectionMask*>(node.data())) {
0466                     srcRect |= mask->selection()->selectedExactRect();
0467                 } else if (const KisTransparencyMask *mask = dynamic_cast<const KisTransparencyMask*>(node.data())) {
0468                     srcRect |= mask->selection()->selectedExactRect();
0469                 } else {
0470                     /// We shouldn't include masks or layer styles into the handles rect,
0471                     /// in the end, we process the paint device only
0472                     srcRect |= node->paintDevice() ? node->paintDevice()->exactBounds() : node->exactBounds();
0473                 }
0474             }
0475         }
0476 
0477         TransformTransactionProperties transaction(srcRect, &m_d->initialTransformArgs, m_d->rootNodes, m_d->processedNodes);
0478         if (!argsAreInitialized) {
0479             m_d->initialTransformArgs = KisTransformUtils::resetArgsForMode(m_d->mode, m_d->filterId, transaction, m_d->externalSource);
0480         }
0481         m_d->externalSource.clear();
0482 
0483         const QRect imageBoundsRect = m_d->imageRoot->projection()->defaultBounds()->bounds();
0484         m_d->previewLevelOfDetail = calculatePreferredLevelOfDetail(srcRect & imageBoundsRect);
0485 
0486         if (m_d->previewLevelOfDetail > 0) {
0487             for (auto it = m_d->prevDirtyRects.begin(); it != m_d->prevDirtyRects.end(); ++it) {
0488                 KisLodTransform t(m_d->previewLevelOfDetail);
0489                 m_d->prevDirtyPreviewRects.addUpdate(it->first, t.map(it->second));
0490             }
0491         }
0492 
0493         Q_EMIT sigTransactionGenerated(transaction, m_d->initialTransformArgs, this);
0494     });
0495 
0496     /// recover back visibility of decorated nodes
0497     KritaUtils::addJobBarrier(extraInitJobs, [this]() {
0498         Q_FOREACH (KisDecoratedNodeInterface *decoratedNode, m_d->disabledDecoratedNodes) {
0499             decoratedNode->setDecorationsVisible(true);
0500         }
0501         m_d->disabledDecoratedNodes.clear();
0502     });
0503 
0504     Q_FOREACH (KisNodeSP node, m_d->processedNodes) {
0505         KritaUtils::addJobSequential(extraInitJobs, [this, node]() mutable {
0506             createCacheAndClearNode(node);
0507         });
0508     }
0509 
0510     KritaUtils::addJobBarrier(extraInitJobs, [this]() {
0511         QMutexLocker l(&m_d->dirtyRectsMutex);
0512 
0513         executeAndAddCommand(new KisDisableDirtyRequestsCommand(m_d->updatesFacade, KisDisableDirtyRequestsCommand::FINALIZING), Clear, KisStrokeJobData::BARRIER);
0514 
0515         m_d->updateTimer.start();
0516     });
0517 
0518     if (!lastCommandUndoJobs.isEmpty()) {
0519         KIS_SAFE_ASSERT_RECOVER_NOOP(m_d->overriddenCommand);
0520 
0521         for (auto it = extraInitJobs.begin(); it != extraInitJobs.end(); ++it) {
0522             (*it)->setCancellable(false);
0523         }
0524     }
0525 
0526     KritaUtils::addJobBarrier(extraInitJobs, [this]() {
0527         if (m_d->previewLevelOfDetail > 0) {
0528             QVector<KisStrokeJobData*> lodSyncJobs;
0529 
0530             KisSyncLodCacheStrokeStrategy::createJobsData(lodSyncJobs,
0531                                                           m_d->imageRoot,
0532                                                           m_d->updatesFacade,
0533                                                           m_d->previewLevelOfDetail,
0534                                                           m_d->devicesCacheHash.values() +
0535                                                           m_d->transformMaskCacheHash.values());
0536 
0537             for (auto it = lodSyncJobs.begin(); it != lodSyncJobs.end(); ++it) {
0538                 (*it)->setLevelOfDetailOverride(m_d->previewLevelOfDetail);
0539             }
0540 
0541             addMutatedJobs(lodSyncJobs);
0542         }
0543     });
0544 
0545     addMutatedJobs(extraInitJobs);
0546 }
0547 
0548 void InplaceTransformStrokeStrategy::finishStrokeCallback()
0549 {
0550     QVector<KisStrokeJobData *> mutatedJobs;
0551 
0552     finishAction(mutatedJobs);
0553 
0554     if (!mutatedJobs.isEmpty()) {
0555         addMutatedJobs(mutatedJobs);
0556     }
0557 }
0558 
0559 void InplaceTransformStrokeStrategy::cancelStrokeCallback()
0560 {
0561     QVector<KisStrokeJobData *> mutatedJobs;
0562 
0563     cancelAction(mutatedJobs);
0564 
0565     if (!mutatedJobs.isEmpty()) {
0566         addMutatedJobs(mutatedJobs);
0567     }
0568 }
0569 
0570 InplaceTransformStrokeStrategy::BarrierUpdateData::BarrierUpdateData(bool _forceUpdate)
0571     : KisAsynchronousStrokeUpdateHelper::UpdateData(_forceUpdate, BARRIER, NORMAL)
0572 {
0573 }
0574 
0575 KisStrokeJobData *InplaceTransformStrokeStrategy::BarrierUpdateData::createLodClone(int levelOfDetail)
0576 {
0577     return new BarrierUpdateData(*this, levelOfDetail);
0578 }
0579 
0580 InplaceTransformStrokeStrategy::BarrierUpdateData::BarrierUpdateData(const InplaceTransformStrokeStrategy::BarrierUpdateData &rhs, int levelOfDetail)
0581     : KisAsynchronousStrokeUpdateHelper::UpdateData (rhs, levelOfDetail)
0582 {
0583 }
0584 
0585 void InplaceTransformStrokeStrategy::executeAndAddCommand(KUndo2Command *cmd, InplaceTransformStrokeStrategy::CommandGroup group, KisStrokeJobData::Sequentiality seq)
0586 {
0587     QMutexLocker l(&m_d->commandsMutex);
0588     KUndo2CommandSP sharedCommand = toQShared(cmd);
0589     executeCommand(sharedCommand, false);
0590     m_d->commands.append({group, sharedCommand, seq});
0591 }
0592 
0593 void InplaceTransformStrokeStrategy::notifyAllCommandsDone()
0594 {
0595     for (auto it = m_d->commands.begin(); it != m_d->commands.end(); ++it) {
0596         if (it->commandGroup == Clear) {
0597             notifyCommandDone(it->command, it->sequentiality, KisStrokeJobData::NORMAL);
0598         }
0599     }
0600 
0601     notifyCommandDone(toQShared(new KUndo2Command()), KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::NORMAL);
0602 
0603     for (auto it = m_d->commands.begin(); it != m_d->commands.end(); ++it) {
0604         if (it->commandGroup == Transform) {
0605             notifyCommandDone(it->command, it->sequentiality, KisStrokeJobData::NORMAL);
0606         }
0607     }
0608 }
0609 
0610 void InplaceTransformStrokeStrategy::undoAllCommands()
0611 {
0612     for (auto it = std::make_reverse_iterator(m_d->commands.end());
0613          it != std::make_reverse_iterator(m_d->commands.begin());
0614          ++it) {
0615 
0616         executeCommand(it->command, true);
0617     }
0618 
0619     m_d->commands.clear();
0620 }
0621 
0622 void InplaceTransformStrokeStrategy::undoTransformCommands(int levelOfDetail)
0623 {
0624     for (auto it = std::make_reverse_iterator(m_d->commands.end());
0625          it != std::make_reverse_iterator(m_d->commands.begin());) {
0626 
0627         if ((levelOfDetail > 0 &&
0628              (it->commandGroup == TransformLod || it->commandGroup == TransformLodTemporary)) ||
0629             (levelOfDetail <= 0 &&
0630              (it->commandGroup == Transform || it->commandGroup == TransformTemporary))) {
0631 
0632             executeCommand(it->command, true);
0633             it = std::make_reverse_iterator(m_d->commands.erase(std::next(it).base()));
0634         } else {
0635             ++it;
0636         }
0637     }
0638 }
0639 
0640 void InplaceTransformStrokeStrategy::fetchAllUpdateRequests(int levelOfDetail, KisBatchNodeUpdateSP updateData)
0641 {
0642     KisBatchNodeUpdate &dirtyRects = m_d->effectiveDirtyRects(levelOfDetail);
0643     KisBatchNodeUpdate &prevDirtyRects = m_d->effectivePrevDirtyRects(levelOfDetail);
0644 
0645     *updateData = (prevDirtyRects | dirtyRects).compressed();
0646 
0647     KisBatchNodeUpdate savedUndoRects = dirtyRects;
0648 
0649     if (levelOfDetail > 0) {
0650 
0651         for (auto it = savedUndoRects.begin(); it != savedUndoRects.end(); ++it) {
0652             it->second = KisLodTransform::upscaledRect(it->second, levelOfDetail);
0653         }
0654     }
0655 
0656     *m_d->updateDataForUndo = (m_d->initialUpdatesBeforeClear | savedUndoRects).compressed();
0657     prevDirtyRects.clear();
0658     dirtyRects.swap(prevDirtyRects);
0659 }
0660 
0661 void InplaceTransformStrokeStrategy::transformNode(KisNodeSP node, const ToolTransformArgs &config, int levelOfDetail)
0662 {
0663     KisPaintDeviceSP device = node->paintDevice();
0664 
0665     CommandGroup commandGroup =
0666         levelOfDetail > 0 ? TransformLod : Transform;
0667 
0668     if (KisExternalLayer *extLayer =
0669         dynamic_cast<KisExternalLayer*>(node.data())) {
0670 
0671             if (config.mode() == ToolTransformArgs::FREE_TRANSFORM ||
0672                     (config.mode() == ToolTransformArgs::PERSPECTIVE_4POINT &&
0673                      extLayer->supportsPerspectiveTransform())) {
0674 
0675                 if (levelOfDetail <= 0) {
0676                     const QRect oldDirtyRect = extLayer->projectionPlane()->tightUserVisibleBounds() | extLayer->theoreticalBoundingRect();
0677 
0678                     QVector3D transformedCenter;
0679                     KisTransformWorker w = KisTransformUtils::createTransformWorker(config, 0, 0, &transformedCenter);
0680                     QTransform t = w.transform();
0681                     KUndo2Command *cmd = extLayer->transform(t);
0682 
0683                     executeAndAddCommand(cmd, Transform, KisStrokeJobData::CONCURRENT);
0684 
0685                     /// we should make sure that the asynchronous shape regeneration
0686                     /// has completed before we issue the updates a bit later
0687                     if (KisShapeLayer *shapeLayer = dynamic_cast<KisShapeLayer*>(extLayer)) {
0688                         shapeLayer->forceUpdateTimedNode();
0689                     }
0690 
0691                     /**
0692                      * Shape layer's projection may not be yet ready right
0693                      * after transformation, because it need to do that in
0694                      * the GUI thread, so we should approximate that.
0695                      */
0696                     const QRect theoreticalNewDirtyRect =
0697                         kisGrowRect(t.mapRect(oldDirtyRect), 1);
0698 
0699                     addDirtyRect(node,
0700                                  oldDirtyRect |
0701                                  theoreticalNewDirtyRect |
0702                                  extLayer->projectionPlane()->tightUserVisibleBounds() |
0703                                  extLayer->theoreticalBoundingRect(), 0);
0704                     return;
0705                 } else {
0706                     device = node->projection();
0707                     commandGroup = TransformLodTemporary;
0708                 }
0709             }
0710 
0711     } else {
0712         device = node->paintDevice();
0713     }
0714 
0715     if (device) {
0716         KisPaintDeviceSP cachedPortion;
0717 
0718         {
0719             QMutexLocker l(&m_d->devicesCacheMutex);
0720             cachedPortion = m_d->devicesCacheHash[device.data()];
0721         }
0722 
0723         KIS_SAFE_ASSERT_RECOVER_RETURN(cachedPortion);
0724 
0725         KisTransaction transaction(device);
0726 
0727         KisProcessingVisitor::ProgressHelper helper(node);
0728         KisTransformUtils::transformAndMergeDevice(config, cachedPortion,
0729                                                    device, &helper);
0730 
0731         executeAndAddCommand(transaction.endAndTake(), commandGroup, KisStrokeJobData::CONCURRENT);
0732         addDirtyRect(node, cachedPortion->extent() | node->projectionPlane()->tightUserVisibleBounds(), levelOfDetail);
0733 
0734     } else if (KisTransformMask *transformMask =
0735                dynamic_cast<KisTransformMask*>(node.data())) {
0736 
0737         const QRect oldDirtyRect = transformMask->extent();
0738 
0739         if (levelOfDetail > 0) {
0740             KisPaintDeviceSP cachedPortion;
0741 
0742             {
0743                 QMutexLocker l(&m_d->devicesCacheMutex);
0744                 cachedPortion = m_d->transformMaskCacheHash[transformMask];
0745             }
0746 
0747             KIS_SAFE_ASSERT_RECOVER_RETURN(cachedPortion);
0748 
0749             KisPaintDeviceSP dst = new KisPaintDevice(cachedPortion->colorSpace());
0750             dst->prepareClone(cachedPortion);
0751 
0752             KisProcessingVisitor::ProgressHelper helper(node);
0753             KisTransformUtils::transformAndMergeDevice(config, cachedPortion,
0754                                                        dst, &helper);
0755 
0756             transformMask->overrideStaticCacheDevice(dst);
0757         }
0758 
0759         {
0760             KUndo2Command *cmd = new KisSimpleModifyTransformMaskCommand(transformMask,
0761                                                                          KisTransformMaskParamsInterfaceSP(
0762                                                                              new KisTransformMaskAdapter(config)),
0763                                                                          m_d->commandUpdatesBlockerCookie);
0764             executeAndAddCommand(cmd, commandGroup, KisStrokeJobData::CONCURRENT);
0765             addDirtyRect(node, oldDirtyRect | transformMask->extent(), levelOfDetail);
0766         }
0767 
0768     }
0769 }
0770 
0771 void InplaceTransformStrokeStrategy::createCacheAndClearNode(KisNodeSP node)
0772 {
0773     KisPaintDeviceSP device;
0774     CommandGroup commandGroup = Clear;
0775 
0776     if (KisExternalLayer *extLayer =
0777                    dynamic_cast<KisExternalLayer*>(node.data())) {
0778 
0779         if (m_d->mode == ToolTransformArgs::FREE_TRANSFORM ||
0780             (m_d->mode == ToolTransformArgs::PERSPECTIVE_4POINT &&
0781              extLayer->supportsPerspectiveTransform())) {
0782 
0783             device = node->projection();
0784             commandGroup = ClearTemporary;
0785         }
0786     } else if (KisTransformMask *mask = dynamic_cast<KisTransformMask*>(node.data())) {
0787         KIS_SAFE_ASSERT_RECOVER_NOOP(!m_d->selection);
0788 
0789         // NOTE: this action should be either sequential or barrier
0790         QMutexLocker l(&m_d->devicesCacheMutex);
0791         if (!m_d->transformMaskCacheHash.contains(mask)) {
0792 
0793             KisPaintDeviceSP dev = mask->buildSourcePreviewDevice();
0794             m_d->transformMaskCacheHash.insert(mask, new KisPaintDevice(*dev));
0795 
0796             return;
0797         }
0798 
0799     } else {
0800         device = node->paintDevice();
0801     }
0802 
0803     if (device) {
0804 
0805         {
0806             QMutexLocker l(&m_d->devicesCacheMutex);
0807 
0808             if (!m_d->devicesCacheHash.contains(device.data())) {
0809                 KisPaintDeviceSP cache;
0810 
0811                 // The image that will be transformed is linked to the original
0812                 // layer. We copy existing pixels or use an external source.
0813                 if (m_d->initialTransformArgs.externalSource()) {
0814                     cache = device->createCompositionSourceDevice(m_d->initialTransformArgs.externalSource());
0815                 } else if (m_d->selection) {
0816                     QRect srcRect = m_d->selection->selectedExactRect();
0817 
0818                     cache = device->createCompositionSourceDevice();
0819                     KisPainter gc(cache);
0820                     gc.setSelection(m_d->selection);
0821                     gc.bitBlt(srcRect.topLeft(), device, srcRect);
0822                 } else {
0823                     cache = device->createCompositionSourceDevice(device);
0824                 }
0825 
0826                 m_d->devicesCacheHash.insert(device.data(), cache);
0827             }
0828         }
0829 
0830         // Don't clear the selection or layer when the source is external
0831         if (m_d->initialTransformArgs.externalSource()) return;
0832 
0833         KisTransaction transaction(device);
0834         if (m_d->selection) {
0835             device->clearSelection(m_d->selection);
0836         } else {
0837             QRect oldExtent = device->extent();
0838             device->clear();
0839             device->setDirty(oldExtent);
0840         }
0841 
0842         executeAndAddCommand(transaction.endAndTake(), commandGroup, KisStrokeJobData::CONCURRENT);
0843     }
0844 }
0845 
0846 void InplaceTransformStrokeStrategy::reapplyTransform(ToolTransformArgs args,
0847                                                       QVector<KisStrokeJobData *> &mutatedJobs,
0848                                                       int levelOfDetail,
0849                                                       bool useHoldUI)
0850 {
0851     if (levelOfDetail > 0) {
0852         args.scale3dSrcAndDst(KisLodTransform::lodToScale(levelOfDetail));
0853     }
0854 
0855     KisBatchNodeUpdateSP updateData(new KisBatchNodeUpdate());
0856 
0857     CommandGroup commandGroup =
0858         levelOfDetail > 0 ? TransformLod : Transform;
0859 
0860     KritaUtils::addJobBarrier(mutatedJobs, levelOfDetail,
0861                               [this, args, levelOfDetail, updateData, useHoldUI, commandGroup]() {
0862 
0863         // it has its own dirty requests blocking inside
0864         undoTransformCommands(levelOfDetail);
0865 
0866         if (useHoldUI) {
0867             executeAndAddCommand(new KisHoldUIUpdatesCommand(m_d->updatesFacade, KisCommandUtils::FlipFlopCommand::INITIALIZING), commandGroup, KisStrokeJobData::BARRIER);
0868         }
0869 
0870         executeAndAddCommand(new KisDisableDirtyRequestsCommand(m_d->updatesFacade, KisUpdateCommandEx::INITIALIZING), commandGroup, KisStrokeJobData::BARRIER);
0871     });
0872 
0873     Q_FOREACH (KisNodeSP node, m_d->processedNodes) {
0874         KritaUtils::addJobConcurrent(mutatedJobs, levelOfDetail,
0875                                      [this, node, args, levelOfDetail]() {
0876             transformNode(node, args, levelOfDetail);
0877         });
0878     }
0879 
0880     KritaUtils::addJobBarrier(mutatedJobs, levelOfDetail,
0881                               [this, levelOfDetail, updateData, useHoldUI, commandGroup]() {
0882 
0883         fetchAllUpdateRequests(levelOfDetail, updateData);
0884 
0885         executeAndAddCommand(new KisDisableDirtyRequestsCommand(m_d->updatesFacade, KisUpdateCommandEx::FINALIZING), commandGroup, KisStrokeJobData::BARRIER);
0886         executeAndAddCommand(new KisUpdateCommandEx(m_d->updateDataForUndo, m_d->updatesFacade, KisUpdateCommandEx::FINALIZING, m_d->commandUpdatesBlockerCookie), commandGroup, KisStrokeJobData::BARRIER);
0887 
0888         if (useHoldUI) {
0889             executeAndAddCommand(new KisHoldUIUpdatesCommand(m_d->updatesFacade, KisCommandUtils::FlipFlopCommand::FINALIZING), commandGroup, KisStrokeJobData::BARRIER);
0890         }
0891 
0892         /**
0893          * We also emit the updates manually, because KisUpdateCommandEx is
0894          * still blocked by m_d->commandUpdatesBlockerCookie (for easy undo
0895          * purposes)
0896          */
0897         for (auto it = updateData->begin(); it != updateData->end(); ++it) {
0898             KisTransformMask *transformMask = dynamic_cast<KisTransformMask*>(it->first.data());
0899 
0900             if (transformMask &&
0901                     ((levelOfDetail <= 0 && !transformMask->transformParams()->isAffine()) ||
0902                      (levelOfDetail <= 0 && m_d->previewLevelOfDetail > 0))) {
0903 
0904                 transformMask->threadSafeForceStaticImageUpdate(it->second);
0905             } else {
0906                 m_d->updatesFacade->refreshGraphAsync(it->first, it->second);
0907             }
0908 
0909         }
0910     });
0911 }
0912 
0913 void InplaceTransformStrokeStrategy::finalizeStrokeImpl(QVector<KisStrokeJobData *> &mutatedJobs, bool saveCommands)
0914 {
0915     KritaUtils::addJobBarrier(mutatedJobs, [this]() {
0916         Q_FOREACH (KisSelectionSP selection, m_d->deactivatedSelections) {
0917             selection->setVisible(true);
0918         }
0919 
0920         Q_FOREACH(KisSelectionMaskSP deactivatedOverlaySelectionMask, m_d->deactivatedOverlaySelectionMasks) {
0921             deactivatedOverlaySelectionMask->selection()->setVisible(true);
0922             deactivatedOverlaySelectionMask->setDirty();
0923         }
0924 
0925         m_d->commandUpdatesBlockerCookie.reset();
0926     });
0927 
0928 
0929     if (saveCommands) {
0930         KritaUtils::addJobBarrier(mutatedJobs, [this]() {
0931             notifyAllCommandsDone();
0932             m_d->commands.clear();
0933         });
0934     }
0935 }
0936 
0937 void InplaceTransformStrokeStrategy::finishAction(QVector<KisStrokeJobData *> &mutatedJobs)
0938 {
0939     /**
0940      * * Forward to cancelling should happen before the guard for
0941      *   finalizingActionsStarted.
0942      *
0943      * * Transform masks may switch mode and become identity, that
0944      *   shouldn't be cancelled.
0945      */
0946     if (m_d->currentTransformArgs.isUnchanging() &&
0947         m_d->transformMaskCacheHash.isEmpty() &&
0948         !m_d->overriddenCommand) {
0949 
0950         cancelAction(mutatedJobs);
0951         return;
0952     }
0953 
0954     if (m_d->previewLevelOfDetail > 0) {
0955         /**
0956          * Update jobs from level of detail updates may cause dirtying
0957          * of the transform mask's static cache device. Therefore we must
0958          * ensure that final update of the mask happens strictly after
0959          * them.
0960          */
0961         KritaUtils::addJobBarrier(mutatedJobs, [this]() { Q_UNUSED(this) });
0962 
0963         if (!m_d->transformMaskCacheHash.isEmpty()) {
0964             KritaUtils::addJobSequential(mutatedJobs, [this]() {
0965                 Q_FOREACH (KisTransformMask *mask, m_d->transformMaskCacheHash.keys()) {
0966                     mask->overrideStaticCacheDevice(0);
0967                 }
0968 
0969                 /**
0970                  * Transform masks don't have internal state switch for LoD mode,
0971                  * therefore all the preview transformations must be cancelled
0972                  * before applying the final command
0973                  */
0974                 undoTransformCommands(m_d->previewLevelOfDetail);
0975             });
0976         }
0977 
0978         reapplyTransform(m_d->currentTransformArgs, mutatedJobs, 0, true);
0979     } else {
0980         if (m_d->pendingUpdateArgs) {
0981             mutatedJobs << new BarrierUpdateData(true);
0982         }
0983     }
0984 
0985     mutatedJobs << new UpdateTransformData(m_d->currentTransformArgs,
0986                                            UpdateTransformData::SELECTION);
0987 
0988     // the rest of the transform finishing work cannot be cancelled...
0989     KritaUtils::addJobBarrier(mutatedJobs, [this]() {
0990         m_d->strokeCompletionHasBeenStarted = true;
0991 
0992         QVector<KisStrokeJobData *> nonCancellableFinishJobs;
0993 
0994         finalizeStrokeImpl(nonCancellableFinishJobs, true);
0995 
0996         KritaUtils::addJobBarrier(nonCancellableFinishJobs, [this]() {
0997             KisStrokeStrategyUndoCommandBased::finishStrokeCallback();
0998         });
0999 
1000         for (auto it = nonCancellableFinishJobs.begin(); it != nonCancellableFinishJobs.end(); ++it) {
1001             (*it)->setCancellable(false);
1002         }
1003 
1004         this->addMutatedJobs(nonCancellableFinishJobs);
1005 
1006     });
1007 }
1008 
1009 void InplaceTransformStrokeStrategy::cancelAction(QVector<KisStrokeJobData *> &mutatedJobs)
1010 {
1011     /**
1012      * It is too late to cancel anything, the transformation has been completed
1013      * and its commands have been pushed into the undo adapter, so there is no
1014      * way to stop that.
1015      */
1016     if (m_d->strokeCompletionHasBeenStarted) return;
1017 
1018 
1019     KIS_SAFE_ASSERT_RECOVER_NOOP(m_d->transformMaskCacheHash.isEmpty() ||
1020                                  (m_d->transformMaskCacheHash.size() == 1 && m_d->processedNodes.size() == 1));
1021 
1022     const bool isChangingTransformMask = !m_d->transformMaskCacheHash.isEmpty();
1023 
1024     if (m_d->initialTransformArgs.isIdentity()) {
1025         KritaUtils::addJobSequential(mutatedJobs, [this]() {
1026             Q_FOREACH (KisTransformMask *mask, m_d->transformMaskCacheHash.keys()) {
1027                 mask->overrideStaticCacheDevice(0);
1028             }
1029         });
1030 
1031 
1032         KritaUtils::addJobBarrier(mutatedJobs, [this]() {
1033             m_d->commandUpdatesBlockerCookie.reset();
1034             undoTransformCommands(0);
1035             undoAllCommands();
1036         });
1037         finalizeStrokeImpl(mutatedJobs, false);
1038 
1039         KritaUtils::addJobSequential(mutatedJobs, [this]() {
1040             Q_FOREACH (KisTransformMask *mask, m_d->transformMaskCacheHash.keys()) {
1041                 mask->threadSafeForceStaticImageUpdate();
1042             }
1043         });
1044 
1045         KritaUtils::addJobBarrier(mutatedJobs, [this]() {
1046             KisStrokeStrategyUndoCommandBased::cancelStrokeCallback();
1047         });
1048     } else {
1049         KIS_SAFE_ASSERT_RECOVER_NOOP(isChangingTransformMask || m_d->overriddenCommand);
1050 
1051         KritaUtils::addJobSequential(mutatedJobs, [this]() {
1052             Q_FOREACH (KisTransformMask *mask, m_d->transformMaskCacheHash.keys()) {
1053                 mask->overrideStaticCacheDevice(0);
1054             }
1055         });
1056 
1057         reapplyTransform(m_d->initialTransformArgs, mutatedJobs, 0, true);
1058 
1059         mutatedJobs << new UpdateTransformData(m_d->initialTransformArgs,
1060                                                UpdateTransformData::SELECTION);
1061 
1062         finalizeStrokeImpl(mutatedJobs, bool(m_d->overriddenCommand));
1063 
1064         KritaUtils::addJobSequential(mutatedJobs, [this]() {
1065             Q_FOREACH (KisTransformMask *mask, m_d->transformMaskCacheHash.keys()) {
1066                 mask->threadSafeForceStaticImageUpdate();
1067             }
1068         });
1069 
1070         if (m_d->overriddenCommand) {
1071             KritaUtils::addJobBarrier(mutatedJobs, [this]() {
1072                 m_d->currentTransformArgs = m_d->initialTransformArgs;
1073                 KisStrokeStrategyUndoCommandBased::finishStrokeCallback();
1074             });
1075         } else {
1076             KritaUtils::addJobBarrier(mutatedJobs, [this]() {
1077                 KisStrokeStrategyUndoCommandBased::cancelStrokeCallback();
1078             });
1079         }
1080     }
1081 }
1082 
1083 void InplaceTransformStrokeStrategy::addDirtyRect(KisNodeSP node, const QRect &rect, int levelOfDetail) {
1084     QMutexLocker l(&m_d->dirtyRectsMutex);
1085     m_d->effectiveDirtyRects(levelOfDetail).addUpdate(node, rect);
1086 }