File indexing completed on 2024-05-19 04:26:23

0001 /*
0002  * SPDX-FileCopyrightText: 2007 Boudewijn Rempt <boud@valdyas.org>
0003  *
0004  * SPDX-License-Identifier: GPL-2.0-or-later
0005  */
0006 
0007 #include "kis_node.h"
0008 
0009 #include <QList>
0010 #include <QReadWriteLock>
0011 #include <QReadLocker>
0012 #include <QWriteLocker>
0013 #include <QPainterPath>
0014 #include <QRect>
0015 #include <QCoreApplication>
0016 
0017 #include <KoProperties.h>
0018 
0019 #include "kis_global.h"
0020 #include "kis_node_graph_listener.h"
0021 #include "kis_node_visitor.h"
0022 #include "kis_processing_visitor.h"
0023 #include "kis_node_progress_proxy.h"
0024 #include "kis_busy_progress_indicator.h"
0025 #include "KisFrameChangeUpdateRecipe.h"
0026 
0027 #include "kis_clone_layer.h"
0028 
0029 #include "kis_time_span.h"
0030 
0031 #include "kis_safe_read_list.h"
0032 typedef KisSafeReadList<KisNodeSP> KisSafeReadNodeList;
0033 
0034 #include "kis_abstract_projection_plane.h"
0035 #include "kis_projection_leaf.h"
0036 #include "kis_undo_adapter.h"
0037 #include "kis_keyframe_channel.h"
0038 #include "kis_image.h"
0039 #include "kis_layer_utils.h"
0040 #include "KisRegion.h"
0041 #include <KisStaticInitializer.h>
0042 
0043 /**
0044  * The link between KisProjection and KisImageUpdater
0045  * uses queued signals with an argument of KisNodeSP type,
0046  * so we should register it beforehand
0047  */
0048 KIS_DECLARE_STATIC_INITIALIZER {
0049     qRegisterMetaType<KisNodeSP>("KisNodeSP");
0050     qRegisterMetaType<KisNodeList>("KisNodeList");
0051 }
0052 
0053 
0054 /**
0055  * Note about "thread safety" of KisNode
0056  *
0057  * 1) One can *read* any information about node and node graph in any
0058  *    number of threads concurrently. This operation is safe because
0059  *    of the usage of KisSafeReadNodeList and will run concurrently
0060  *    (lock-free).
0061  *
0062  * 2) One can *write* any information into the node or node graph in a
0063  *    single thread only! Changing the graph concurrently is *not*
0064  *    sane and therefore not supported.
0065  *
0066  * 3) One can *read and write* information about the node graph
0067  *    concurrently, given that there is only *one* writer thread and
0068  *    any number of reader threads. Please note that in this case the
0069  *    node's code is just guaranteed *not to crash*, which is ensured
0070  *    by nodeSubgraphLock. You need to ensure the sanity of the data
0071  *    read by the reader threads yourself!
0072  */
0073 
0074 struct Q_DECL_HIDDEN KisNode::Private
0075 {
0076 public:
0077     Private(KisNode *node)
0078             : graphListener(0)
0079             , nodeProgressProxy(0)
0080             , busyProgressIndicator(0)
0081             , projectionLeaf(new KisProjectionLeaf(node))
0082     {
0083     }
0084 
0085     KisNodeWSP parent;
0086     KisNodeGraphListener *graphListener;
0087     KisSafeReadNodeList nodes;
0088     KisNodeProgressProxy *nodeProgressProxy;
0089     KisBusyProgressIndicator *busyProgressIndicator;
0090     QReadWriteLock nodeSubgraphLock;
0091 
0092     KisProjectionLeafSP projectionLeaf;
0093 
0094     const KisNode* findSymmetricClone(const KisNode *srcRoot,
0095                                       const KisNode *dstRoot,
0096                                       const KisNode *srcTarget);
0097     void processDuplicatedClones(const KisNode *srcDuplicationRoot,
0098                                  const KisNode *dstDuplicationRoot,
0099                                  KisNode *node);
0100 
0101     std::optional<KisFrameChangeUpdateRecipe> frameRemovalUpdateRecipe;
0102     KisFrameChangeUpdateRecipe handleKeyframeChannelUpdateImpl(const KisKeyframeChannel *channel, int time);
0103 };
0104 
0105 /**
0106  * Finds the layer in \p dstRoot subtree, which has the same path as
0107  * \p srcTarget has in \p srcRoot
0108  */
0109 const KisNode* KisNode::Private::findSymmetricClone(const KisNode *srcRoot,
0110                                                     const KisNode *dstRoot,
0111                                                     const KisNode *srcTarget)
0112 {
0113     if (srcRoot == srcTarget) return dstRoot;
0114 
0115     KisSafeReadNodeList::const_iterator srcIter = srcRoot->m_d->nodes.constBegin();
0116     KisSafeReadNodeList::const_iterator dstIter = dstRoot->m_d->nodes.constBegin();
0117 
0118     for (; srcIter != srcRoot->m_d->nodes.constEnd(); srcIter++, dstIter++) {
0119 
0120         KIS_ASSERT_RECOVER_RETURN_VALUE((srcIter != srcRoot->m_d->nodes.constEnd()) ==
0121                                         (dstIter != dstRoot->m_d->nodes.constEnd()), 0);
0122 
0123         const KisNode *node = findSymmetricClone(srcIter->data(), dstIter->data(), srcTarget);
0124         if (node) return node;
0125 
0126     }
0127 
0128     return 0;
0129 }
0130 
0131 /**
0132  * This function walks through a subtrees of old and new layers and
0133  * searches for clone layers. For each clone layer it checks whether
0134  * its copyFrom() lays inside the old subtree, and if it is so resets
0135  * it to the corresponding layer in the new subtree.
0136  *
0137  * That is needed when the user duplicates a group layer with all its
0138  * layer subtree. In such a case all the "internal" clones must stay
0139  * "internal" and not point to the layers of the older group.
0140  */
0141 void KisNode::Private::processDuplicatedClones(const KisNode *srcDuplicationRoot,
0142                                                const KisNode *dstDuplicationRoot,
0143                                                KisNode *node)
0144 {
0145     if (KisCloneLayer *clone = dynamic_cast<KisCloneLayer*>(node)) {
0146         KIS_ASSERT_RECOVER_RETURN(clone->copyFrom());
0147         const KisNode *newCopyFrom = findSymmetricClone(srcDuplicationRoot,
0148                                                         dstDuplicationRoot,
0149                                                         clone->copyFrom());
0150 
0151         if (newCopyFrom) {
0152             KisLayer *newCopyFromLayer = qobject_cast<KisLayer*>(const_cast<KisNode*>(newCopyFrom));
0153             KIS_ASSERT_RECOVER_RETURN(newCopyFromLayer);
0154 
0155             clone->setCopyFrom(newCopyFromLayer);
0156         }
0157     }
0158 
0159     KisSafeReadNodeList::const_iterator iter;
0160     FOREACH_SAFE(iter, node->m_d->nodes) {
0161         KisNode *child = const_cast<KisNode*>((*iter).data());
0162         processDuplicatedClones(srcDuplicationRoot, dstDuplicationRoot, child);
0163     }
0164 }
0165 
0166 KisNode::KisNode(KisImageWSP image)
0167         : KisBaseNode(image),
0168           m_d(new Private(this))
0169 {
0170     m_d->parent = 0;
0171     m_d->graphListener = 0;
0172     moveToThread(qApp->thread());
0173 }
0174 
0175 KisNode::KisNode(const KisNode & rhs)
0176         : KisBaseNode(rhs)
0177         , m_d(new Private(this))
0178 {
0179     m_d->parent = 0;
0180     m_d->graphListener = 0;
0181     moveToThread(qApp->thread());
0182 
0183     // NOTE: the nodes are not supposed to be added/removed while
0184     // creation of another node, so we do *no* locking here!
0185 
0186     KisSafeReadNodeList::const_iterator iter;
0187     FOREACH_SAFE(iter, rhs.m_d->nodes) {
0188         KisNodeSP child = (*iter)->clone();
0189         child->createNodeProgressProxy();
0190         m_d->nodes.append(child);
0191         child->setParent(this);
0192     }
0193 
0194     m_d->processDuplicatedClones(&rhs, this, this);
0195 }
0196 
0197 KisNode::~KisNode()
0198 {
0199     if (m_d->busyProgressIndicator) {
0200         m_d->busyProgressIndicator->prepareDestroying();
0201         m_d->busyProgressIndicator->deleteLater();
0202     }
0203 
0204     if (m_d->nodeProgressProxy) {
0205         m_d->nodeProgressProxy->prepareDestroying();
0206         m_d->nodeProgressProxy->deleteLater();
0207     }
0208 
0209     {
0210         QWriteLocker l(&m_d->nodeSubgraphLock);
0211         m_d->nodes.clear();
0212     }
0213 
0214     delete m_d;
0215 }
0216 
0217 QRect KisNode::needRect(const QRect &rect, PositionToFilthy pos) const
0218 {
0219     Q_UNUSED(pos);
0220     return rect;
0221 }
0222 
0223 QRect KisNode::changeRect(const QRect &rect, PositionToFilthy pos) const
0224 {
0225     Q_UNUSED(pos);
0226     return rect;
0227 }
0228 
0229 QRect KisNode::accessRect(const QRect &rect, PositionToFilthy pos) const
0230 {
0231     Q_UNUSED(pos);
0232     return rect;
0233 }
0234 
0235 void KisNode::childNodeChanged(KisNodeSP /*changedChildNode*/)
0236 {
0237 }
0238 
0239 KisAbstractProjectionPlaneSP KisNode::projectionPlane() const
0240 {
0241     KIS_ASSERT_RECOVER_NOOP(0 && "KisNode::projectionPlane() is not defined!");
0242     static KisAbstractProjectionPlaneSP plane =
0243         toQShared(new KisDumbProjectionPlane());
0244 
0245     return plane;
0246 }
0247 
0248 KisProjectionLeafSP KisNode::projectionLeaf() const
0249 {
0250     return m_d->projectionLeaf;
0251 }
0252 
0253 void KisNode::setImage(KisImageWSP newImage)
0254 {
0255     KisBaseNode::setImage(newImage);
0256 
0257     KisNodeSP node = firstChild();
0258     while (node) {
0259         KisLayerUtils::recursiveApplyNodes(node,
0260                                            [newImage] (KisNodeSP node) {
0261                                                node->setImage(newImage);
0262                                            });
0263 
0264         node = node->nextSibling();
0265     }
0266 }
0267 
0268 bool KisNode::accept(KisNodeVisitor &v)
0269 {
0270     return v.visit(this);
0271 }
0272 
0273 void KisNode::accept(KisProcessingVisitor &visitor, KisUndoAdapter *undoAdapter)
0274 {
0275     visitor.visit(this, undoAdapter);
0276 }
0277 
0278 int KisNode::graphSequenceNumber() const
0279 {
0280     return m_d->graphListener ? m_d->graphListener->graphSequenceNumber() : -1;
0281 }
0282 
0283 KisNodeGraphListener *KisNode::graphListener() const
0284 {
0285     return m_d->graphListener;
0286 }
0287 
0288 void KisNode::setGraphListener(KisNodeGraphListener *graphListener)
0289 {
0290     m_d->graphListener = graphListener;
0291 
0292     QReadLocker l(&m_d->nodeSubgraphLock);
0293     KisSafeReadNodeList::const_iterator iter;
0294     FOREACH_SAFE(iter, m_d->nodes) {
0295         KisNodeSP child = (*iter);
0296         child->setGraphListener(graphListener);
0297     }
0298 }
0299 
0300 void KisNode::setParent(KisNodeWSP parent)
0301 {
0302     QWriteLocker l(&m_d->nodeSubgraphLock);
0303     m_d->parent = parent;
0304 }
0305 
0306 KisNodeSP KisNode::parent() const
0307 {
0308     QReadLocker l(&m_d->nodeSubgraphLock);
0309     return m_d->parent.isValid() ? KisNodeSP(m_d->parent) : KisNodeSP();
0310 }
0311 
0312 KisBaseNodeSP KisNode::parentCallback() const
0313 {
0314     return parent();
0315 }
0316 
0317 void KisNode::notifyParentVisibilityChanged(bool value)
0318 {
0319     QReadLocker l(&m_d->nodeSubgraphLock);
0320 
0321     KisSafeReadNodeList::const_iterator iter;
0322     FOREACH_SAFE(iter, m_d->nodes) {
0323         KisNodeSP child = (*iter);
0324         child->notifyParentVisibilityChanged(value);
0325     }
0326 }
0327 
0328 void KisNode::baseNodeChangedCallback()
0329 {
0330     if(m_d->graphListener) {
0331         m_d->graphListener->nodeChanged(this);
0332         emit sigNodeChangedInternal();
0333     }
0334 }
0335 
0336 void KisNode::baseNodeInvalidateAllFramesCallback()
0337 {
0338     if(m_d->graphListener) {
0339         m_d->graphListener->invalidateAllFrames();
0340     }
0341 }
0342 
0343 void KisNode::baseNodeCollapsedChangedCallback()
0344 {
0345     if(m_d->graphListener) {
0346         m_d->graphListener->nodeCollapsedChanged(this);
0347     }
0348 }
0349 
0350 void KisNode::addKeyframeChannel(KisKeyframeChannel *channel)
0351 {
0352     channel->setNode(this);
0353     KisBaseNode::addKeyframeChannel(channel);
0354 
0355     if (m_d->graphListener) {
0356         m_d->graphListener->keyframeChannelHasBeenAdded(this, channel);
0357     }
0358 }
0359 
0360 KisNodeSP KisNode::firstChild() const
0361 {
0362     QReadLocker l(&m_d->nodeSubgraphLock);
0363     return !m_d->nodes.isEmpty() ? m_d->nodes.first() : 0;
0364 }
0365 
0366 KisNodeSP KisNode::lastChild() const
0367 {
0368     QReadLocker l(&m_d->nodeSubgraphLock);
0369     return !m_d->nodes.isEmpty() ? m_d->nodes.last() : 0;
0370 }
0371 
0372 KisNodeSP KisNode::prevChildImpl(KisNodeSP child)
0373 {
0374     /**
0375      * Warning: mind locking policy!
0376      *
0377      * The graph locks must be *always* taken in descending
0378      * order. That is if you want to (or it implicitly happens that
0379      * you) take a lock of a parent and a chil, you must first take
0380      * the lock of a parent, and only after that ask a child to do the
0381      * same.  Otherwise you'll get a deadlock.
0382      */
0383 
0384     QReadLocker l(&m_d->nodeSubgraphLock);
0385 
0386     int i = m_d->nodes.indexOf(child) - 1;
0387     return i >= 0 ? m_d->nodes.at(i) : 0;
0388 }
0389 
0390 KisNodeSP KisNode::nextChildImpl(KisNodeSP child)
0391 {
0392     /**
0393      * See a comment in KisNode::prevChildImpl()
0394      */
0395     QReadLocker l(&m_d->nodeSubgraphLock);
0396 
0397     int i = m_d->nodes.indexOf(child) + 1;
0398     return i > 0 && i < m_d->nodes.size() ? m_d->nodes.at(i) : 0;
0399 }
0400 
0401 KisNodeSP KisNode::prevSibling() const
0402 {
0403     KisNodeSP parentNode = parent();
0404     return parentNode ? parentNode->prevChildImpl(const_cast<KisNode*>(this)) : 0;
0405 }
0406 
0407 KisNodeSP KisNode::nextSibling() const
0408 {
0409     KisNodeSP parentNode = parent();
0410     return parentNode ? parentNode->nextChildImpl(const_cast<KisNode*>(this)) : 0;
0411 }
0412 
0413 quint32 KisNode::childCount() const
0414 {
0415     QReadLocker l(&m_d->nodeSubgraphLock);
0416     return m_d->nodes.size();
0417 }
0418 
0419 
0420 KisNodeSP KisNode::at(quint32 index) const
0421 {
0422     QReadLocker l(&m_d->nodeSubgraphLock);
0423 
0424     if (!m_d->nodes.isEmpty() && index < (quint32)m_d->nodes.size()) {
0425         return m_d->nodes.at(index);
0426     }
0427 
0428     return 0;
0429 }
0430 
0431 int KisNode::index(const KisNodeSP node) const
0432 {
0433     QReadLocker l(&m_d->nodeSubgraphLock);
0434 
0435     return m_d->nodes.indexOf(node);
0436 }
0437 
0438 QList<KisNodeSP> KisNode::childNodes(const QStringList & nodeTypes, const KoProperties & properties) const
0439 {
0440     QReadLocker l(&m_d->nodeSubgraphLock);
0441 
0442     QList<KisNodeSP> nodes;
0443 
0444     KisSafeReadNodeList::const_iterator iter;
0445     FOREACH_SAFE(iter, m_d->nodes) {
0446         if (*iter) {
0447             if (properties.isEmpty() || (*iter)->check(properties)) {
0448                 bool rightType = true;
0449 
0450                 if(!nodeTypes.isEmpty()) {
0451                     rightType = false;
0452                     Q_FOREACH (const QString &nodeType,  nodeTypes) {
0453                         if ((*iter)->inherits(nodeType.toLatin1())) {
0454                             rightType = true;
0455                             break;
0456                         }
0457                     }
0458                 }
0459                 if (rightType) {
0460                     nodes.append(*iter);
0461                 }
0462             }
0463         }
0464     }
0465     return nodes;
0466 }
0467 
0468 bool KisNode::add(KisNodeSP newNode, KisNodeSP aboveThis)
0469 {
0470     KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(newNode, false);
0471     KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!aboveThis || aboveThis->parent().data() == this, false);
0472     KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(allowAsChild(newNode), false);
0473     KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!newNode->parent(), false);
0474     KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(index(newNode) < 0, false);
0475 
0476     int idx = aboveThis ? this->index(aboveThis) + 1 : 0;
0477 
0478     // theoretical race condition may happen here ('idx' may become
0479     // deprecated until the write lock will be held). But we ignore
0480     // it, because it is not supported to add/remove nodes from two
0481     // concurrent threads simultaneously
0482 
0483     if (m_d->graphListener) {
0484         m_d->graphListener->aboutToAddANode(this, idx);
0485     }
0486 
0487     {
0488         QWriteLocker l(&m_d->nodeSubgraphLock);
0489 
0490         newNode->createNodeProgressProxy();
0491 
0492         m_d->nodes.insert(idx, newNode);
0493 
0494         newNode->setParent(this);
0495         newNode->setGraphListener(m_d->graphListener);
0496     }
0497 
0498     newNode->setImage(image());
0499 
0500     if (m_d->graphListener) {
0501         m_d->graphListener->nodeHasBeenAdded(this, idx);
0502     }
0503 
0504     childNodeChanged(newNode);
0505 
0506     return true;
0507 }
0508 
0509 bool KisNode::remove(quint32 index)
0510 {
0511     if (index < childCount()) {
0512         KisNodeSP removedNode = at(index);
0513 
0514         if (m_d->graphListener) {
0515             m_d->graphListener->aboutToRemoveANode(this, index);
0516         }
0517 
0518         removedNode->setImage(0);
0519 
0520         {
0521             QWriteLocker l(&m_d->nodeSubgraphLock);
0522 
0523             removedNode->setGraphListener(0);
0524 
0525             removedNode->setParent(0);   // after calling aboutToRemoveANode or then the model get broken according to TT's modeltest
0526             m_d->nodes.removeAt(index);
0527         }
0528 
0529         if (m_d->graphListener) {
0530             m_d->graphListener->nodeHasBeenRemoved(this, index);
0531         }
0532 
0533         childNodeChanged(removedNode);
0534 
0535         return true;
0536     }
0537     return false;
0538 }
0539 
0540 bool KisNode::remove(KisNodeSP node)
0541 {
0542     return node->parent().data() == this ? remove(index(node)) : false;
0543 }
0544 
0545 KisNodeProgressProxy* KisNode::nodeProgressProxy() const
0546 {
0547     if (m_d->nodeProgressProxy) {
0548         return m_d->nodeProgressProxy;
0549     } else if (parent()) {
0550         return parent()->nodeProgressProxy();
0551     }
0552     return 0;
0553 }
0554 
0555 KisBusyProgressIndicator* KisNode::busyProgressIndicator() const
0556 {
0557     if (m_d->busyProgressIndicator) {
0558         return m_d->busyProgressIndicator;
0559     } else if (parent()) {
0560         return parent()->busyProgressIndicator();
0561     }
0562     return 0;
0563 }
0564 
0565 void KisNode::createNodeProgressProxy()
0566 {
0567     if (!m_d->nodeProgressProxy) {
0568         m_d->nodeProgressProxy = new KisNodeProgressProxy(this);
0569         m_d->busyProgressIndicator = new KisBusyProgressIndicator(m_d->nodeProgressProxy);
0570 
0571         m_d->nodeProgressProxy->moveToThread(this->thread());
0572         m_d->busyProgressIndicator->moveToThread(this->thread());
0573     }
0574 }
0575 
0576 void KisNode::setDirty()
0577 {
0578     setDirty(extent());
0579 }
0580 
0581 void KisNode::setDirty(const QVector<QRect> &rects)
0582 {
0583     if(m_d->graphListener) {
0584         m_d->graphListener->requestProjectionUpdate(this, rects, true);
0585     }
0586 }
0587 
0588 void KisNode::setDirty(const KisRegion &region)
0589 {
0590     setDirty(region.rects());
0591 }
0592 
0593 void KisNode::setDirty(const QRect & rect)
0594 {
0595     setDirty(QVector<QRect>({rect}));
0596 }
0597 
0598 void KisNode::setDirtyDontResetAnimationCache()
0599 {
0600     setDirtyDontResetAnimationCache(QVector<QRect>({extent()}));
0601 }
0602 
0603 void KisNode::setDirtyDontResetAnimationCache(const QRect &rect)
0604 {
0605     setDirtyDontResetAnimationCache(QVector<QRect>({rect}));
0606 }
0607 
0608 void KisNode::setDirtyDontResetAnimationCache(const QVector<QRect> &rects)
0609 {
0610     if(m_d->graphListener) {
0611         m_d->graphListener->requestProjectionUpdate(this, rects, false);
0612     }
0613 }
0614 
0615 void KisNode::invalidateFrames(const KisTimeSpan &range, const QRect &rect)
0616 {
0617     if(m_d->graphListener) {
0618         m_d->graphListener->invalidateFrames(range, rect);
0619     }
0620 }
0621 
0622 KisFrameChangeUpdateRecipe
0623 KisNode::Private::handleKeyframeChannelUpdateImpl(const KisKeyframeChannel *channel, int time)
0624 {
0625     KisFrameChangeUpdateRecipe recipe;
0626 
0627     recipe.affectedRange = channel->affectedFrames(time);
0628     recipe.affectedRect = channel->affectedRect(time);
0629 
0630     if (parent->image()) {
0631         KisDefaultBoundsSP bounds(new KisDefaultBounds(parent->image()));
0632 
0633         if (recipe.affectedRange.contains(bounds->currentTime())) {
0634             recipe.totalDirtyRect = recipe.affectedRect;
0635         }
0636     }
0637 
0638     return recipe;
0639 }
0640 
0641 void KisNode::handleKeyframeChannelFrameChange(const KisKeyframeChannel *channel, int time)
0642 {
0643     m_d->handleKeyframeChannelUpdateImpl(channel, time).notify(this);
0644 }
0645 
0646 void KisNode::handleKeyframeChannelFrameAdded(const KisKeyframeChannel *channel, int time)
0647 {
0648     m_d->handleKeyframeChannelUpdateImpl(channel, time).notify(this);
0649 }
0650 
0651 KisFrameChangeUpdateRecipe KisNode::handleKeyframeChannelFrameAboutToBeRemovedImpl(const KisKeyframeChannel *channel, int time)
0652 {
0653     return m_d->handleKeyframeChannelUpdateImpl(channel, time);
0654 }
0655 
0656 void KisNode::handleKeyframeChannelFrameAboutToBeRemoved(const KisKeyframeChannel *channel, int time)
0657 {
0658     KIS_SAFE_ASSERT_RECOVER_NOOP(!m_d->frameRemovalUpdateRecipe);
0659     m_d->frameRemovalUpdateRecipe = handleKeyframeChannelFrameAboutToBeRemovedImpl(channel, time);
0660 }
0661 
0662 void KisNode::handleKeyframeChannelFrameHasBeenRemoved(const KisKeyframeChannel *channel, int time)
0663 {
0664     KIS_SAFE_ASSERT_RECOVER_RETURN(m_d->frameRemovalUpdateRecipe);
0665     m_d->frameRemovalUpdateRecipe->notify(this);
0666     m_d->frameRemovalUpdateRecipe = std::nullopt;
0667 }
0668 
0669 void KisNode::requestTimeSwitch(int time)
0670 {
0671     if(m_d->graphListener) {
0672         m_d->graphListener->requestTimeSwitch(time);
0673     }
0674 }
0675 
0676 void KisNode::syncLodCache()
0677 {
0678     // noop. everything is done by getLodCapableDevices()
0679 }
0680 
0681 KisPaintDeviceList KisNode::getLodCapableDevices() const
0682 {
0683     KisPaintDeviceList list;
0684 
0685     KisPaintDeviceSP device = paintDevice();
0686     if (device) {
0687         list << device;
0688     }
0689 
0690     KisPaintDeviceSP originalDevice = original();
0691     if (originalDevice && originalDevice != device) {
0692         list << originalDevice;
0693     }
0694 
0695     list << projectionPlane()->getLodCapableDevices();
0696 
0697     return list;
0698 }