diff --git a/libs/image/kis_colorspace_convert_visitor.cpp b/libs/image/kis_colorspace_convert_visitor.cpp index c56b3e85ed..2ceab37e8e 100644 --- a/libs/image/kis_colorspace_convert_visitor.cpp +++ b/libs/image/kis_colorspace_convert_visitor.cpp @@ -1,160 +1,160 @@ /* * Copyright (c) 2005 C. Boemann * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_colorspace_convert_visitor.h" #include "kis_image.h" #include "kis_paint_device.h" #include "kis_undo_adapter.h" #include "kis_adjustment_layer.h" #include "kis_paint_layer.h" #include "kis_group_layer.h" #include "lazybrush/kis_colorize_mask.h" #include "kis_external_layer_iface.h" #include "filter/kis_filter_configuration.h" #include "filter/kis_filter_registry.h" #include "filter/kis_filter.h" #include "kis_generator.h" #include "kis_generator_registry.h" #include "generator/kis_generator_layer.h" #include "kis_time_range.h" #include KisColorSpaceConvertVisitor::KisColorSpaceConvertVisitor(KisImageWSP image, const KoColorSpace *srcColorSpace, const KoColorSpace *dstColorSpace, KoColorConversionTransformation::Intent renderingIntent, KoColorConversionTransformation::ConversionFlags conversionFlags) : KisNodeVisitor() , m_image(image) , m_srcColorSpace(srcColorSpace) , m_dstColorSpace(dstColorSpace) , m_renderingIntent(renderingIntent) , m_conversionFlags(conversionFlags) { } KisColorSpaceConvertVisitor::~KisColorSpaceConvertVisitor() { } bool KisColorSpaceConvertVisitor::visit(KisGroupLayer * layer) { convertPaintDevice(layer); KisLayerSP child = qobject_cast(layer->firstChild().data()); while (child) { child->accept(*this); child = qobject_cast(child->nextSibling().data()); } layer->resetCache(); return true; } bool KisColorSpaceConvertVisitor::visit(KisPaintLayer *layer) { return convertPaintDevice(layer); } bool KisColorSpaceConvertVisitor::visit(KisGeneratorLayer *layer) { layer->resetCache(); return true; } bool KisColorSpaceConvertVisitor::visit(KisAdjustmentLayer * layer) { // XXX: Make undoable! if (layer->filter()->name() == "perchannel") { // Per-channel filters need to be reset because of different number // of channels. This makes undo very tricky, but so be it. // XXX: Make this more generic for after 1.6, when we'll have many // channel-specific filters. KisFilterSP f = KisFilterRegistry::instance()->value("perchannel"); layer->setFilter(f->defaultConfiguration()); } layer->resetCache(); return true; } bool KisColorSpaceConvertVisitor::convertPaintDevice(KisLayer* layer) { if (*m_dstColorSpace == *layer->colorSpace()) return true; bool alphaLock = false; if (m_srcColorSpace->colorModelId() != m_dstColorSpace->colorModelId()) { layer->setChannelFlags(m_emptyChannelFlags); KisPaintLayer *paintLayer = 0; if ((paintLayer = dynamic_cast(layer))) { alphaLock = paintLayer->alphaLocked(); paintLayer->setChannelLockFlags(QBitArray()); } } KisImageSP image = m_image.toStrongRef(); if (!image) { return false; } if (layer->original()) { KUndo2Command* cmd = layer->original()->convertTo(m_dstColorSpace, m_renderingIntent, m_conversionFlags); if (cmd) { image->undoAdapter()->addCommand(cmd); } } if (layer->paintDevice()) { KUndo2Command* cmd = layer->paintDevice()->convertTo(m_dstColorSpace, m_renderingIntent, m_conversionFlags); if (cmd) { image->undoAdapter()->addCommand(cmd); } } if (layer->projection()) { KUndo2Command* cmd = layer->projection()->convertTo(m_dstColorSpace, m_renderingIntent, m_conversionFlags); if (cmd) { image->undoAdapter()->addCommand(cmd); } } KisPaintLayer *paintLayer = 0; if ((paintLayer = dynamic_cast(layer))) { paintLayer->setAlphaLocked(alphaLock); } layer->setDirty(); - layer->invalidateFrames(KisTimeRange::infinite(0), layer->extent()); + layer->invalidateFrames(KisFrameSet::infiniteFrom(0), layer->extent()); return true; } bool KisColorSpaceConvertVisitor::visit(KisColorizeMask *mask) { KisImageSP image = m_image.toStrongRef(); if (!image) { return false; } KUndo2Command* cmd = mask->setColorSpace(m_dstColorSpace, m_renderingIntent, m_conversionFlags); if (cmd) { image->undoAdapter()->addCommand(cmd); } return true; } diff --git a/libs/image/kis_image.cc b/libs/image/kis_image.cc index fbea2059cd..2c31805efb 100644 --- a/libs/image/kis_image.cc +++ b/libs/image/kis_image.cc @@ -1,1840 +1,1840 @@ /* * Copyright (c) 2002 Patrick Julien * Copyright (c) 2007 Boudewijn Rempt * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_image.h" #include // WORDS_BIGENDIAN #include #include #include #include #include #include #include #include #include #include #include "KoColorSpaceRegistry.h" #include "KoColor.h" #include "KoColorProfile.h" #include #include "KisProofingConfiguration.h" #include "kis_adjustment_layer.h" #include "kis_annotation.h" #include "kis_change_profile_visitor.h" #include "kis_colorspace_convert_visitor.h" #include "kis_count_visitor.h" #include "kis_filter_strategy.h" #include "kis_group_layer.h" #include "commands/kis_image_commands.h" #include "kis_layer.h" #include "kis_meta_data_merge_strategy_registry.h" #include "kis_name_server.h" #include "kis_paint_layer.h" #include "kis_projection_leaf.h" #include "kis_painter.h" #include "kis_selection.h" #include "kis_transaction.h" #include "kis_meta_data_merge_strategy.h" #include "kis_memory_statistics_server.h" #include "kis_image_config.h" #include "kis_update_scheduler.h" #include "kis_image_signal_router.h" #include "kis_image_animation_interface.h" #include "kis_stroke_strategy.h" #include "kis_simple_stroke_strategy.h" #include "kis_image_barrier_locker.h" #include "kis_undo_stores.h" #include "kis_legacy_undo_adapter.h" #include "kis_post_execution_undo_adapter.h" #include "kis_transform_worker.h" #include "kis_processing_applicator.h" #include "processing/kis_crop_processing_visitor.h" #include "processing/kis_crop_selections_processing_visitor.h" #include "processing/kis_transform_processing_visitor.h" #include "commands_new/kis_image_resize_command.h" #include "commands_new/kis_image_set_resolution_command.h" #include "commands_new/kis_activate_selection_mask_command.h" #include "kis_composite_progress_proxy.h" #include "kis_layer_composition.h" #include "kis_wrapped_rect.h" #include "kis_crop_saved_extra_data.h" #include "kis_layer_utils.h" #include "kis_lod_transform.h" #include "kis_suspend_projection_updates_stroke_strategy.h" #include "kis_sync_lod_cache_stroke_strategy.h" #include "kis_projection_updates_filter.h" #include "kis_layer_projection_plane.h" #include "kis_update_time_monitor.h" #include #include #include "kis_time_range.h" // #define SANITY_CHECKS #ifdef SANITY_CHECKS #define SANITY_CHECK_LOCKED(name) \ if (!locked()) warnKrita() << "Locking policy failed:" << name \ << "has been called without the image" \ "being locked"; #else #define SANITY_CHECK_LOCKED(name) #endif struct KisImageSPStaticRegistrar { KisImageSPStaticRegistrar() { qRegisterMetaType("KisImageSP"); } }; static KisImageSPStaticRegistrar __registrar; class KisImage::KisImagePrivate { public: KisImagePrivate(KisImage *_q, qint32 w, qint32 h, const KoColorSpace *c, KisUndoStore *undo, KisImageAnimationInterface *_animationInterface) : q(_q) , lockedForReadOnly(false) , width(w) , height(h) , colorSpace(c ? c : KoColorSpaceRegistry::instance()->rgb8()) , nserver(1) , undoStore(undo ? undo : new KisDumbUndoStore()) , legacyUndoAdapter(undoStore.data(), _q) , postExecutionUndoAdapter(undoStore.data(), _q) , signalRouter(_q) , animationInterface(_animationInterface) , scheduler(_q, _q) , axesCenter(QPointF(0.5, 0.5)) { { KisImageConfig cfg(true); if (cfg.enableProgressReporting()) { scheduler.setProgressProxy(&compositeProgressProxy); } // Each of these lambdas defines a new factory function. scheduler.setLod0ToNStrokeStrategyFactory( [=](bool forgettable) { return KisLodSyncPair( new KisSyncLodCacheStrokeStrategy(KisImageWSP(q), forgettable), KisSyncLodCacheStrokeStrategy::createJobsData(KisImageWSP(q))); }); scheduler.setSuspendUpdatesStrokeStrategyFactory( [=]() { return KisSuspendResumePair( new KisSuspendProjectionUpdatesStrokeStrategy(KisImageWSP(q), true), KisSuspendProjectionUpdatesStrokeStrategy::createSuspendJobsData(KisImageWSP(q))); }); scheduler.setResumeUpdatesStrokeStrategyFactory( [=]() { return KisSuspendResumePair( new KisSuspendProjectionUpdatesStrokeStrategy(KisImageWSP(q), false), KisSuspendProjectionUpdatesStrokeStrategy::createResumeJobsData(KisImageWSP(q))); }); } connect(q, SIGNAL(sigImageModified()), KisMemoryStatisticsServer::instance(), SLOT(notifyImageChanged())); } ~KisImagePrivate() { /** * Stop animation interface. It may use the rootLayer. */ delete animationInterface; /** * First delete the nodes, while strokes * and undo are still alive */ rootLayer.clear(); } KisImage *q; quint32 lockCount = 0; bool lockedForReadOnly; qint32 width; qint32 height; double xres = 1.0; double yres = 1.0; const KoColorSpace * colorSpace; KisProofingConfigurationSP proofingConfig; KisSelectionSP deselectedGlobalSelection; KisGroupLayerSP rootLayer; // The layers are contained in here KisSelectionMaskSP targetOverlaySelectionMask; // the overlay switching stroke will try to switch into this mask KisSelectionMaskSP overlaySelectionMask; QList compositions; KisNodeSP isolatedRootNode; bool wrapAroundModePermitted = false; KisNameServer nserver; QScopedPointer undoStore; KisLegacyUndoAdapter legacyUndoAdapter; KisPostExecutionUndoAdapter postExecutionUndoAdapter; vKisAnnotationSP annotations; QAtomicInt disableUIUpdateSignals; KisProjectionUpdatesFilterSP projectionUpdatesFilter; KisImageSignalRouter signalRouter; KisImageAnimationInterface *animationInterface; KisUpdateScheduler scheduler; QAtomicInt disableDirtyRequests; KisCompositeProgressProxy compositeProgressProxy; bool blockLevelOfDetail = false; QPointF axesCenter; bool tryCancelCurrentStrokeAsync(); void notifyProjectionUpdatedInPatches(const QRect &rc); }; KisImage::KisImage(KisUndoStore *undoStore, qint32 width, qint32 height, const KoColorSpace * colorSpace, const QString& name) : QObject(0) , KisShared() , m_d(new KisImagePrivate(this, width, height, colorSpace, undoStore, new KisImageAnimationInterface(this))) { // make sure KisImage belongs to the GUI thread moveToThread(qApp->thread()); connect(this, SIGNAL(sigInternalStopIsolatedModeRequested()), SLOT(stopIsolatedMode())); setObjectName(name); setRootLayer(new KisGroupLayer(this, "root", OPACITY_OPAQUE_U8)); } KisImage::~KisImage() { dbgImage << "deleting kisimage" << objectName(); /** * Request the tools to end currently running strokes */ waitForDone(); delete m_d; disconnect(); // in case Qt gets confused } KisImage *KisImage::clone(bool exactCopy) { return new KisImage(*this, 0, exactCopy); } KisImage::KisImage(const KisImage& rhs, KisUndoStore *undoStore, bool exactCopy) : KisNodeFacade(), KisNodeGraphListener(), KisShared(), m_d(new KisImagePrivate(this, rhs.width(), rhs.height(), rhs.colorSpace(), undoStore ? undoStore : new KisDumbUndoStore(), new KisImageAnimationInterface(*rhs.animationInterface(), this))) { // make sure KisImage belongs to the GUI thread moveToThread(qApp->thread()); connect(this, SIGNAL(sigInternalStopIsolatedModeRequested()), SLOT(stopIsolatedMode())); setObjectName(rhs.objectName()); m_d->xres = rhs.m_d->xres; m_d->yres = rhs.m_d->yres; if (rhs.m_d->proofingConfig) { m_d->proofingConfig = toQShared(new KisProofingConfiguration(*rhs.m_d->proofingConfig)); } KisNodeSP newRoot = rhs.root()->clone(); newRoot->setGraphListener(this); newRoot->setImage(this); m_d->rootLayer = dynamic_cast(newRoot.data()); setRoot(newRoot); if (exactCopy || rhs.m_d->isolatedRootNode) { QQueue linearizedNodes; KisLayerUtils::recursiveApplyNodes(rhs.root(), [&linearizedNodes](KisNodeSP node) { linearizedNodes.enqueue(node); }); KisLayerUtils::recursiveApplyNodes(newRoot, [&linearizedNodes, exactCopy, &rhs, this](KisNodeSP node) { KisNodeSP refNode = linearizedNodes.dequeue(); if (exactCopy) { node->setUuid(refNode->uuid()); } if (rhs.m_d->isolatedRootNode && rhs.m_d->isolatedRootNode == refNode) { m_d->isolatedRootNode = node; } }); } Q_FOREACH (KisLayerCompositionSP comp, rhs.m_d->compositions) { m_d->compositions << toQShared(new KisLayerComposition(*comp, this)); } rhs.m_d->nserver = KisNameServer(rhs.m_d->nserver); vKisAnnotationSP newAnnotations; Q_FOREACH (KisAnnotationSP annotation, rhs.m_d->annotations) { newAnnotations << annotation->clone(); } m_d->annotations = newAnnotations; KIS_ASSERT_RECOVER_NOOP(!rhs.m_d->projectionUpdatesFilter); KIS_ASSERT_RECOVER_NOOP(!rhs.m_d->disableUIUpdateSignals); KIS_ASSERT_RECOVER_NOOP(!rhs.m_d->disableDirtyRequests); m_d->blockLevelOfDetail = rhs.m_d->blockLevelOfDetail; /** * The overlay device is not inherited when cloning the image! */ if (rhs.m_d->overlaySelectionMask) { const QRect dirtyRect = rhs.m_d->overlaySelectionMask->extent(); m_d->rootLayer->setDirty(dirtyRect); } } void KisImage::aboutToAddANode(KisNode *parent, int index) { KisNodeGraphListener::aboutToAddANode(parent, index); SANITY_CHECK_LOCKED("aboutToAddANode"); } void KisImage::nodeHasBeenAdded(KisNode *parent, int index) { KisNodeGraphListener::nodeHasBeenAdded(parent, index); SANITY_CHECK_LOCKED("nodeHasBeenAdded"); m_d->signalRouter.emitNodeHasBeenAdded(parent, index); KisNodeSP newNode = parent->at(index); if (!dynamic_cast(newNode.data())) { emit sigInternalStopIsolatedModeRequested(); } } void KisImage::aboutToRemoveANode(KisNode *parent, int index) { KisNodeSP deletedNode = parent->at(index); if (!dynamic_cast(deletedNode.data())) { emit sigInternalStopIsolatedModeRequested(); } KisNodeGraphListener::aboutToRemoveANode(parent, index); SANITY_CHECK_LOCKED("aboutToRemoveANode"); m_d->signalRouter.emitAboutToRemoveANode(parent, index); } void KisImage::nodeChanged(KisNode* node) { KisNodeGraphListener::nodeChanged(node); requestStrokeEnd(); m_d->signalRouter.emitNodeChanged(node); } void KisImage::invalidateAllFrames() { - invalidateFrames(KisTimeRange::infinite(0), QRect()); + invalidateFrames(KisFrameSet::infiniteFrom(0), QRect()); } void KisImage::setOverlaySelectionMask(KisSelectionMaskSP mask) { if (m_d->targetOverlaySelectionMask == mask) return; m_d->targetOverlaySelectionMask = mask; struct UpdateOverlaySelectionStroke : public KisSimpleStrokeStrategy { UpdateOverlaySelectionStroke(KisImageSP image) : KisSimpleStrokeStrategy("update-overlay-selection-mask", kundo2_noi18n("update-overlay-selection-mask")), m_image(image) { this->enableJob(JOB_INIT, true, KisStrokeJobData::BARRIER, KisStrokeJobData::EXCLUSIVE); setClearsRedoOnStart(false); } void initStrokeCallback() { KisSelectionMaskSP oldMask = m_image->m_d->overlaySelectionMask; KisSelectionMaskSP newMask = m_image->m_d->targetOverlaySelectionMask; if (oldMask == newMask) return; KIS_SAFE_ASSERT_RECOVER_RETURN(!newMask || newMask->graphListener() == m_image); m_image->m_d->overlaySelectionMask = newMask; if (oldMask || newMask) { m_image->m_d->rootLayer->notifyChildMaskChanged(); } if (oldMask) { m_image->m_d->rootLayer->setDirtyDontResetAnimationCache(oldMask->extent()); } if (newMask) { newMask->setDirty(); } m_image->undoAdapter()->emitSelectionChanged(); } private: KisImageSP m_image; }; KisStrokeId id = startStroke(new UpdateOverlaySelectionStroke(this)); endStroke(id); } KisSelectionMaskSP KisImage::overlaySelectionMask() const { return m_d->overlaySelectionMask; } bool KisImage::hasOverlaySelectionMask() const { return m_d->overlaySelectionMask; } KisSelectionSP KisImage::globalSelection() const { KisSelectionMaskSP selectionMask = m_d->rootLayer->selectionMask(); if (selectionMask) { return selectionMask->selection(); } else { return 0; } } void KisImage::setGlobalSelection(KisSelectionSP globalSelection) { KisSelectionMaskSP selectionMask = m_d->rootLayer->selectionMask(); if (!globalSelection) { if (selectionMask) { removeNode(selectionMask); } } else { if (!selectionMask) { selectionMask = new KisSelectionMask(this); selectionMask->initSelection(m_d->rootLayer); addNode(selectionMask); // If we do not set the selection now, the setActive call coming next // can be very, very expensive, depending on the size of the image. selectionMask->setSelection(globalSelection); selectionMask->setActive(true); } else { selectionMask->setSelection(globalSelection); } KIS_SAFE_ASSERT_RECOVER_NOOP(m_d->rootLayer->childCount() > 0); KIS_SAFE_ASSERT_RECOVER_NOOP(m_d->rootLayer->selectionMask()); } m_d->deselectedGlobalSelection = 0; m_d->legacyUndoAdapter.emitSelectionChanged(); } void KisImage::deselectGlobalSelection() { KisSelectionSP savedSelection = globalSelection(); setGlobalSelection(0); m_d->deselectedGlobalSelection = savedSelection; } bool KisImage::canReselectGlobalSelection() { return m_d->deselectedGlobalSelection; } void KisImage::reselectGlobalSelection() { if(m_d->deselectedGlobalSelection) { setGlobalSelection(m_d->deselectedGlobalSelection); } } QString KisImage::nextLayerName(const QString &_baseName) const { QString baseName = _baseName; if (m_d->nserver.currentSeed() == 0) { m_d->nserver.number(); return i18n("background"); } if (baseName.isEmpty()) { baseName = i18n("Layer"); } return QString("%1 %2").arg(baseName).arg(m_d->nserver.number()); } void KisImage::rollBackLayerName() { m_d->nserver.rollback(); } KisCompositeProgressProxy* KisImage::compositeProgressProxy() { return &m_d->compositeProgressProxy; } bool KisImage::locked() const { return m_d->lockCount != 0; } void KisImage::barrierLock(bool readOnly) { if (!locked()) { requestStrokeEnd(); m_d->scheduler.barrierLock(); m_d->lockedForReadOnly = readOnly; } else { m_d->lockedForReadOnly &= readOnly; } m_d->lockCount++; } bool KisImage::tryBarrierLock(bool readOnly) { bool result = true; if (!locked()) { result = m_d->scheduler.tryBarrierLock(); m_d->lockedForReadOnly = readOnly; } if (result) { m_d->lockCount++; m_d->lockedForReadOnly &= readOnly; } return result; } bool KisImage::isIdle(bool allowLocked) { return (allowLocked || !locked()) && m_d->scheduler.isIdle(); } void KisImage::lock() { if (!locked()) { requestStrokeEnd(); m_d->scheduler.lock(); } m_d->lockCount++; m_d->lockedForReadOnly = false; } void KisImage::unlock() { Q_ASSERT(locked()); if (locked()) { m_d->lockCount--; if (m_d->lockCount == 0) { m_d->scheduler.unlock(!m_d->lockedForReadOnly); } } } void KisImage::blockUpdates() { m_d->scheduler.blockUpdates(); } void KisImage::unblockUpdates() { m_d->scheduler.unblockUpdates(); } void KisImage::setSize(const QSize& size) { m_d->width = size.width(); m_d->height = size.height(); } void KisImage::resizeImageImpl(const QRect& newRect, bool cropLayers) { if (newRect == bounds() && !cropLayers) return; KUndo2MagicString actionName = cropLayers ? kundo2_i18n("Crop Image") : kundo2_i18n("Resize Image"); KisImageSignalVector emitSignals; emitSignals << ComplexSizeChangedSignal(newRect, newRect.size()); emitSignals << ModifiedSignal; KisCropSavedExtraData *extraData = new KisCropSavedExtraData(cropLayers ? KisCropSavedExtraData::CROP_IMAGE : KisCropSavedExtraData::RESIZE_IMAGE, newRect); KisProcessingApplicator applicator(this, m_d->rootLayer, KisProcessingApplicator::RECURSIVE | KisProcessingApplicator::NO_UI_UPDATES, emitSignals, actionName, extraData); if (cropLayers || !newRect.topLeft().isNull()) { KisProcessingVisitorSP visitor = new KisCropProcessingVisitor(newRect, cropLayers, true); applicator.applyVisitorAllFrames(visitor, KisStrokeJobData::CONCURRENT); } applicator.applyCommand(new KisImageResizeCommand(this, newRect.size())); applicator.end(); } void KisImage::resizeImage(const QRect& newRect) { resizeImageImpl(newRect, false); } void KisImage::cropImage(const QRect& newRect) { resizeImageImpl(newRect, true); } void KisImage::cropNode(KisNodeSP node, const QRect& newRect) { bool isLayer = qobject_cast(node.data()); KUndo2MagicString actionName = isLayer ? kundo2_i18n("Crop Layer") : kundo2_i18n("Crop Mask"); KisImageSignalVector emitSignals; emitSignals << ModifiedSignal; KisCropSavedExtraData *extraData = new KisCropSavedExtraData(KisCropSavedExtraData::CROP_LAYER, newRect, node); KisProcessingApplicator applicator(this, node, KisProcessingApplicator::RECURSIVE, emitSignals, actionName, extraData); KisProcessingVisitorSP visitor = new KisCropProcessingVisitor(newRect, true, false); applicator.applyVisitorAllFrames(visitor, KisStrokeJobData::CONCURRENT); applicator.end(); } void KisImage::scaleImage(const QSize &size, qreal xres, qreal yres, KisFilterStrategy *filterStrategy) { bool resolutionChanged = xres != xRes() && yres != yRes(); bool sizeChanged = size != this->size(); if (!resolutionChanged && !sizeChanged) return; KisImageSignalVector emitSignals; if (resolutionChanged) emitSignals << ResolutionChangedSignal; if (sizeChanged) emitSignals << ComplexSizeChangedSignal(bounds(), size); emitSignals << ModifiedSignal; KUndo2MagicString actionName = sizeChanged ? kundo2_i18n("Scale Image") : kundo2_i18n("Change Image Resolution"); KisProcessingApplicator::ProcessingFlags signalFlags = (resolutionChanged || sizeChanged) ? KisProcessingApplicator::NO_UI_UPDATES : KisProcessingApplicator::NONE; KisProcessingApplicator applicator(this, m_d->rootLayer, KisProcessingApplicator::RECURSIVE | signalFlags, emitSignals, actionName); qreal sx = qreal(size.width()) / this->size().width(); qreal sy = qreal(size.height()) / this->size().height(); QTransform shapesCorrection; if (resolutionChanged) { shapesCorrection = QTransform::fromScale(xRes() / xres, yRes() / yres); } KisProcessingVisitorSP visitor = new KisTransformProcessingVisitor(sx, sy, 0, 0, QPointF(), 0, 0, 0, filterStrategy, shapesCorrection); applicator.applyVisitorAllFrames(visitor, KisStrokeJobData::CONCURRENT); if (resolutionChanged) { KUndo2Command *parent = new KisResetShapesCommand(m_d->rootLayer); new KisImageSetResolutionCommand(this, xres, yres, parent); applicator.applyCommand(parent); } if (sizeChanged) { applicator.applyCommand(new KisImageResizeCommand(this, size)); } applicator.end(); } void KisImage::scaleNode(KisNodeSP node, qreal scaleX, qreal scaleY, KisFilterStrategy *filterStrategy) { KUndo2MagicString actionName(kundo2_i18n("Scale Layer")); KisImageSignalVector emitSignals; emitSignals << ModifiedSignal; KisProcessingApplicator applicator(this, node, KisProcessingApplicator::RECURSIVE, emitSignals, actionName); KisProcessingVisitorSP visitor = new KisTransformProcessingVisitor(scaleX, scaleY, 0, 0, QPointF(), 0, 0, 0, filterStrategy); applicator.applyVisitorAllFrames(visitor, KisStrokeJobData::CONCURRENT); applicator.end(); } void KisImage::rotateImpl(const KUndo2MagicString &actionName, KisNodeSP rootNode, bool resizeImage, double radians) { QPointF offset; QSize newSize; { KisTransformWorker worker(0, 1.0, 1.0, 0, 0, 0, 0, radians, 0, 0, 0, 0); QTransform transform = worker.transform(); if (resizeImage) { QRect newRect = transform.mapRect(bounds()); newSize = newRect.size(); offset = -newRect.topLeft(); } else { QPointF origin = QRectF(rootNode->exactBounds()).center(); newSize = size(); offset = -(transform.map(origin) - origin); } } bool sizeChanged = resizeImage && (newSize.width() != width() || newSize.height() != height()); // These signals will be emitted after processing is done KisImageSignalVector emitSignals; if (sizeChanged) emitSignals << ComplexSizeChangedSignal(bounds(), newSize); emitSignals << ModifiedSignal; // These flags determine whether updates are transferred to the UI during processing KisProcessingApplicator::ProcessingFlags signalFlags = sizeChanged ? KisProcessingApplicator::NO_UI_UPDATES : KisProcessingApplicator::NONE; KisProcessingApplicator applicator(this, rootNode, KisProcessingApplicator::RECURSIVE | signalFlags, emitSignals, actionName); KisFilterStrategy *filter = KisFilterStrategyRegistry::instance()->value("Bicubic"); KisProcessingVisitorSP visitor = new KisTransformProcessingVisitor(1.0, 1.0, 0.0, 0.0, QPointF(), radians, offset.x(), offset.y(), filter); applicator.applyVisitorAllFrames(visitor, KisStrokeJobData::CONCURRENT); if (sizeChanged) { applicator.applyCommand(new KisImageResizeCommand(this, newSize)); } applicator.end(); } void KisImage::rotateImage(double radians) { rotateImpl(kundo2_i18n("Rotate Image"), root(), true, radians); } void KisImage::rotateNode(KisNodeSP node, double radians) { if (node->inherits("KisMask")) { rotateImpl(kundo2_i18n("Rotate Mask"), node, false, radians); } else { rotateImpl(kundo2_i18n("Rotate Layer"), node, false, radians); } } void KisImage::shearImpl(const KUndo2MagicString &actionName, KisNodeSP rootNode, bool resizeImage, double angleX, double angleY, const QPointF &origin) { //angleX, angleY are in degrees const qreal pi = 3.1415926535897932385; const qreal deg2rad = pi / 180.0; qreal tanX = tan(angleX * deg2rad); qreal tanY = tan(angleY * deg2rad); QPointF offset; QSize newSize; { KisTransformWorker worker(0, 1.0, 1.0, tanX, tanY, origin.x(), origin.y(), 0, 0, 0, 0, 0); QRect newRect = worker.transform().mapRect(bounds()); newSize = newRect.size(); if (resizeImage) offset = -newRect.topLeft(); } if (newSize == size()) return; KisImageSignalVector emitSignals; if (resizeImage) emitSignals << ComplexSizeChangedSignal(bounds(), newSize); emitSignals << ModifiedSignal; KisProcessingApplicator::ProcessingFlags signalFlags = KisProcessingApplicator::RECURSIVE; if (resizeImage) signalFlags |= KisProcessingApplicator::NO_UI_UPDATES; KisProcessingApplicator applicator(this, rootNode, signalFlags, emitSignals, actionName); KisFilterStrategy *filter = KisFilterStrategyRegistry::instance()->value("Bilinear"); KisProcessingVisitorSP visitor = new KisTransformProcessingVisitor(1.0, 1.0, tanX, tanY, origin, 0, offset.x(), offset.y(), filter); applicator.applyVisitorAllFrames(visitor, KisStrokeJobData::CONCURRENT); if (resizeImage) { applicator.applyCommand(new KisImageResizeCommand(this, newSize)); } applicator.end(); } void KisImage::shearNode(KisNodeSP node, double angleX, double angleY) { QPointF shearOrigin = QRectF(bounds()).center(); if (node->inherits("KisMask")) { shearImpl(kundo2_i18n("Shear Mask"), node, false, angleX, angleY, shearOrigin); } else { shearImpl(kundo2_i18n("Shear Layer"), node, false, angleX, angleY, shearOrigin); } } void KisImage::shear(double angleX, double angleY) { shearImpl(kundo2_i18n("Shear Image"), m_d->rootLayer, true, angleX, angleY, QPointF()); } void KisImage::convertImageColorSpace(const KoColorSpace *dstColorSpace, KoColorConversionTransformation::Intent renderingIntent, KoColorConversionTransformation::ConversionFlags conversionFlags) { if (!dstColorSpace) return; const KoColorSpace *srcColorSpace = m_d->colorSpace; undoAdapter()->beginMacro(kundo2_i18n("Convert Image Color Space")); undoAdapter()->addCommand(new KisImageLockCommand(KisImageWSP(this), true)); undoAdapter()->addCommand(new KisImageSetProjectionColorSpaceCommand(KisImageWSP(this), dstColorSpace)); KisColorSpaceConvertVisitor visitor(this, srcColorSpace, dstColorSpace, renderingIntent, conversionFlags); m_d->rootLayer->accept(visitor); undoAdapter()->addCommand(new KisImageLockCommand(KisImageWSP(this), false)); undoAdapter()->endMacro(); setModified(); } bool KisImage::assignImageProfile(const KoColorProfile *profile) { if (!profile) return false; const KoColorSpace *dstCs = KoColorSpaceRegistry::instance()->colorSpace(colorSpace()->colorModelId().id(), colorSpace()->colorDepthId().id(), profile); const KoColorSpace *srcCs = colorSpace(); if (!dstCs) return false; m_d->colorSpace = dstCs; KisChangeProfileVisitor visitor(srcCs, dstCs); bool retval = m_d->rootLayer->accept(visitor); m_d->signalRouter.emitNotification(ProfileChangedSignal); return retval; } void KisImage::convertProjectionColorSpace(const KoColorSpace *dstColorSpace) { if (*m_d->colorSpace == *dstColorSpace) return; undoAdapter()->beginMacro(kundo2_i18n("Convert Projection Color Space")); undoAdapter()->addCommand(new KisImageLockCommand(KisImageWSP(this), true)); undoAdapter()->addCommand(new KisImageSetProjectionColorSpaceCommand(KisImageWSP(this), dstColorSpace)); undoAdapter()->addCommand(new KisImageLockCommand(KisImageWSP(this), false)); undoAdapter()->endMacro(); setModified(); } void KisImage::setProjectionColorSpace(const KoColorSpace * colorSpace) { m_d->colorSpace = colorSpace; m_d->rootLayer->resetCache(); m_d->signalRouter.emitNotification(ColorSpaceChangedSignal); } const KoColorSpace * KisImage::colorSpace() const { return m_d->colorSpace; } const KoColorProfile * KisImage::profile() const { return colorSpace()->profile(); } double KisImage::xRes() const { return m_d->xres; } double KisImage::yRes() const { return m_d->yres; } void KisImage::setResolution(double xres, double yres) { m_d->xres = xres; m_d->yres = yres; m_d->signalRouter.emitNotification(ResolutionChangedSignal); } QPointF KisImage::documentToPixel(const QPointF &documentCoord) const { return QPointF(documentCoord.x() * xRes(), documentCoord.y() * yRes()); } QPoint KisImage::documentToImagePixelFloored(const QPointF &documentCoord) const { QPointF pixelCoord = documentToPixel(documentCoord); return QPoint(qFloor(pixelCoord.x()), qFloor(pixelCoord.y())); } QRectF KisImage::documentToPixel(const QRectF &documentRect) const { return QRectF(documentToPixel(documentRect.topLeft()), documentToPixel(documentRect.bottomRight())); } QPointF KisImage::pixelToDocument(const QPointF &pixelCoord) const { return QPointF(pixelCoord.x() / xRes(), pixelCoord.y() / yRes()); } QPointF KisImage::pixelToDocument(const QPoint &pixelCoord) const { return QPointF((pixelCoord.x() + 0.5) / xRes(), (pixelCoord.y() + 0.5) / yRes()); } QRectF KisImage::pixelToDocument(const QRectF &pixelCoord) const { return QRectF(pixelToDocument(pixelCoord.topLeft()), pixelToDocument(pixelCoord.bottomRight())); } qint32 KisImage::width() const { return m_d->width; } qint32 KisImage::height() const { return m_d->height; } KisGroupLayerSP KisImage::rootLayer() const { Q_ASSERT(m_d->rootLayer); return m_d->rootLayer; } KisPaintDeviceSP KisImage::projection() const { if (m_d->isolatedRootNode) { return m_d->isolatedRootNode->projection(); } Q_ASSERT(m_d->rootLayer); KisPaintDeviceSP projection = m_d->rootLayer->projection(); Q_ASSERT(projection); return projection; } qint32 KisImage::nlayers() const { QStringList list; list << "KisLayer"; KisCountVisitor visitor(list, KoProperties()); m_d->rootLayer->accept(visitor); return visitor.count(); } qint32 KisImage::nHiddenLayers() const { QStringList list; list << "KisLayer"; KoProperties properties; properties.setProperty("visible", false); KisCountVisitor visitor(list, properties); m_d->rootLayer->accept(visitor); return visitor.count(); } void KisImage::flatten() { KisLayerUtils::flattenImage(this); } void KisImage::mergeMultipleLayers(QList mergedNodes, KisNodeSP putAfter) { if (!KisLayerUtils::tryMergeSelectionMasks(this, mergedNodes, putAfter)) { KisLayerUtils::mergeMultipleLayers(this, mergedNodes, putAfter); } } void KisImage::mergeDown(KisLayerSP layer, const KisMetaData::MergeStrategy* strategy) { KisLayerUtils::mergeDown(this, layer, strategy); } void KisImage::flattenLayer(KisLayerSP layer) { KisLayerUtils::flattenLayer(this, layer); } void KisImage::setModified() { m_d->signalRouter.emitNotification(ModifiedSignal); } QImage KisImage::convertToQImage(QRect imageRect, const KoColorProfile * profile) { qint32 x; qint32 y; qint32 w; qint32 h; imageRect.getRect(&x, &y, &w, &h); return convertToQImage(x, y, w, h, profile); } QImage KisImage::convertToQImage(qint32 x, qint32 y, qint32 w, qint32 h, const KoColorProfile * profile) { KisPaintDeviceSP dev = projection(); if (!dev) return QImage(); QImage image = dev->convertToQImage(const_cast(profile), x, y, w, h, KoColorConversionTransformation::internalRenderingIntent(), KoColorConversionTransformation::internalConversionFlags()); return image; } QImage KisImage::convertToQImage(const QSize& scaledImageSize, const KoColorProfile *profile) { if (scaledImageSize.isEmpty()) { return QImage(); } KisPaintDeviceSP dev = new KisPaintDevice(colorSpace()); KisPainter gc; gc.copyAreaOptimized(QPoint(0, 0), projection(), dev, bounds()); gc.end(); double scaleX = qreal(scaledImageSize.width()) / width(); double scaleY = qreal(scaledImageSize.height()) / height(); QPointer updater = new KoDummyUpdater(); KisTransformWorker worker(dev, scaleX, scaleY, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, updater, KisFilterStrategyRegistry::instance()->value("Bicubic")); worker.run(); delete updater; return dev->convertToQImage(profile); } void KisImage::notifyLayersChanged() { m_d->signalRouter.emitNotification(LayersChangedSignal); } QRect KisImage::bounds() const { return QRect(0, 0, width(), height()); } QRect KisImage::effectiveLodBounds() const { QRect boundRect = bounds(); const int lod = currentLevelOfDetail(); if (lod > 0) { KisLodTransform t(lod); boundRect = t.map(boundRect); } return boundRect; } KisPostExecutionUndoAdapter* KisImage::postExecutionUndoAdapter() const { const int lod = currentLevelOfDetail(); return lod > 0 ? m_d->scheduler.lodNPostExecutionUndoAdapter() : &m_d->postExecutionUndoAdapter; } void KisImage::setUndoStore(KisUndoStore *undoStore) { m_d->legacyUndoAdapter.setUndoStore(undoStore); m_d->postExecutionUndoAdapter.setUndoStore(undoStore); m_d->undoStore.reset(undoStore); } KisUndoStore* KisImage::undoStore() { return m_d->undoStore.data(); } KisUndoAdapter* KisImage::undoAdapter() const { return &m_d->legacyUndoAdapter; } void KisImage::setDefaultProjectionColor(const KoColor &color) { KIS_ASSERT_RECOVER_RETURN(m_d->rootLayer); m_d->rootLayer->setDefaultProjectionColor(color); } KoColor KisImage::defaultProjectionColor() const { KIS_ASSERT_RECOVER(m_d->rootLayer) { return KoColor(Qt::transparent, m_d->colorSpace); } return m_d->rootLayer->defaultProjectionColor(); } void KisImage::setRootLayer(KisGroupLayerSP rootLayer) { emit sigInternalStopIsolatedModeRequested(); KoColor defaultProjectionColor(Qt::transparent, m_d->colorSpace); if (m_d->rootLayer) { m_d->rootLayer->setGraphListener(0); m_d->rootLayer->disconnect(); KisPaintDeviceSP original = m_d->rootLayer->original(); defaultProjectionColor = original->defaultPixel(); } m_d->rootLayer = rootLayer; m_d->rootLayer->disconnect(); m_d->rootLayer->setGraphListener(this); m_d->rootLayer->setImage(this); KisPaintDeviceSP newOriginal = m_d->rootLayer->original(); newOriginal->setDefaultPixel(defaultProjectionColor); setRoot(m_d->rootLayer.data()); } void KisImage::addAnnotation(KisAnnotationSP annotation) { // Find the icc annotation, if there is one vKisAnnotationSP_it it = m_d->annotations.begin(); while (it != m_d->annotations.end()) { if ((*it)->type() == annotation->type()) { *it = annotation; return; } ++it; } m_d->annotations.push_back(annotation); } KisAnnotationSP KisImage::annotation(const QString& type) { vKisAnnotationSP_it it = m_d->annotations.begin(); while (it != m_d->annotations.end()) { if ((*it)->type() == type) { return *it; } ++it; } return KisAnnotationSP(0); } void KisImage::removeAnnotation(const QString& type) { vKisAnnotationSP_it it = m_d->annotations.begin(); while (it != m_d->annotations.end()) { if ((*it)->type() == type) { m_d->annotations.erase(it); return; } ++it; } } vKisAnnotationSP_it KisImage::beginAnnotations() { return m_d->annotations.begin(); } vKisAnnotationSP_it KisImage::endAnnotations() { return m_d->annotations.end(); } void KisImage::notifyAboutToBeDeleted() { emit sigAboutToBeDeleted(); } KisImageSignalRouter* KisImage::signalRouter() { return &m_d->signalRouter; } void KisImage::waitForDone() { requestStrokeEnd(); m_d->scheduler.waitForDone(); } KisStrokeId KisImage::startStroke(KisStrokeStrategy *strokeStrategy) { /** * Ask open strokes to end gracefully. All the strokes clients * (including the one calling this method right now) will get * a notification that they should probably end their strokes. * However this is purely their choice whether to end a stroke * or not. */ if (strokeStrategy->requestsOtherStrokesToEnd()) { requestStrokeEnd(); } /** * Some of the strokes can cancel their work with undoing all the * changes they did to the paint devices. The problem is that undo * stack will know nothing about it. Therefore, just notify it * explicitly */ if (strokeStrategy->clearsRedoOnStart()) { m_d->undoStore->purgeRedoState(); } return m_d->scheduler.startStroke(strokeStrategy); } void KisImage::KisImagePrivate::notifyProjectionUpdatedInPatches(const QRect &rc) { KisImageConfig imageConfig(true); int patchWidth = imageConfig.updatePatchWidth(); int patchHeight = imageConfig.updatePatchHeight(); for (int y = 0; y < rc.height(); y += patchHeight) { for (int x = 0; x < rc.width(); x += patchWidth) { QRect patchRect(x, y, patchWidth, patchHeight); patchRect &= rc; QtConcurrent::run(std::bind(&KisImage::notifyProjectionUpdated, q, patchRect)); } } } bool KisImage::startIsolatedMode(KisNodeSP node) { struct StartIsolatedModeStroke : public KisSimpleStrokeStrategy { StartIsolatedModeStroke(KisNodeSP node, KisImageSP image) : KisSimpleStrokeStrategy("start-isolated-mode", kundo2_noi18n("start-isolated-mode")), m_node(node), m_image(image) { this->enableJob(JOB_INIT, true, KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); setClearsRedoOnStart(false); } void initStrokeCallback() { // pass-though node don't have any projection prepared, so we should // explicitly regenerate it before activating isolated mode. m_node->projectionLeaf()->explicitlyRegeneratePassThroughProjection(); m_image->m_d->isolatedRootNode = m_node; emit m_image->sigIsolatedModeChanged(); // the GUI uses our thread to do the color space conversion so we // need to emit this signal in multiple threads m_image->m_d->notifyProjectionUpdatedInPatches(m_image->bounds()); m_image->invalidateAllFrames(); } private: KisNodeSP m_node; KisImageSP m_image; }; KisStrokeId id = startStroke(new StartIsolatedModeStroke(node, this)); endStroke(id); return true; } void KisImage::stopIsolatedMode() { if (!m_d->isolatedRootNode) return; struct StopIsolatedModeStroke : public KisSimpleStrokeStrategy { StopIsolatedModeStroke(KisImageSP image) : KisSimpleStrokeStrategy("stop-isolated-mode", kundo2_noi18n("stop-isolated-mode")), m_image(image) { this->enableJob(JOB_INIT); setClearsRedoOnStart(false); } void initStrokeCallback() { if (!m_image->m_d->isolatedRootNode) return; //KisNodeSP oldRootNode = m_image->m_d->isolatedRootNode; m_image->m_d->isolatedRootNode = 0; emit m_image->sigIsolatedModeChanged(); // the GUI uses our thread to do the color space conversion so we // need to emit this signal in multiple threads m_image->m_d->notifyProjectionUpdatedInPatches(m_image->bounds()); m_image->invalidateAllFrames(); // TODO: Substitute notifyProjectionUpdated() with this code // when update optimization is implemented // // QRect updateRect = bounds() | oldRootNode->extent(); // oldRootNode->setDirty(updateRect); } private: KisImageSP m_image; }; KisStrokeId id = startStroke(new StopIsolatedModeStroke(this)); endStroke(id); } KisNodeSP KisImage::isolatedModeRoot() const { return m_d->isolatedRootNode; } void KisImage::addJob(KisStrokeId id, KisStrokeJobData *data) { KisUpdateTimeMonitor::instance()->reportJobStarted(data); m_d->scheduler.addJob(id, data); } void KisImage::endStroke(KisStrokeId id) { m_d->scheduler.endStroke(id); } bool KisImage::cancelStroke(KisStrokeId id) { return m_d->scheduler.cancelStroke(id); } bool KisImage::KisImagePrivate::tryCancelCurrentStrokeAsync() { return scheduler.tryCancelCurrentStrokeAsync(); } void KisImage::requestUndoDuringStroke() { emit sigUndoDuringStrokeRequested(); } void KisImage::requestStrokeCancellation() { if (!m_d->tryCancelCurrentStrokeAsync()) { emit sigStrokeCancellationRequested(); } } UndoResult KisImage::tryUndoUnfinishedLod0Stroke() { return m_d->scheduler.tryUndoLastStrokeAsync(); } void KisImage::requestStrokeEnd() { emit sigStrokeEndRequested(); emit sigStrokeEndRequestedActiveNodeFiltered(); } void KisImage::requestStrokeEndActiveNode() { emit sigStrokeEndRequested(); } void KisImage::refreshGraph(KisNodeSP root) { refreshGraph(root, bounds(), bounds()); } void KisImage::refreshGraph(KisNodeSP root, const QRect &rc, const QRect &cropRect) { if (!root) root = m_d->rootLayer; m_d->animationInterface->notifyNodeChanged(root.data(), rc, true); m_d->scheduler.fullRefresh(root, rc, cropRect); } void KisImage::initialRefreshGraph() { /** * NOTE: Tricky part. We set crop rect to null, so the clones * will not rely on precalculated projections of their sources */ refreshGraphAsync(0, bounds(), QRect()); waitForDone(); } void KisImage::refreshGraphAsync(KisNodeSP root) { refreshGraphAsync(root, bounds(), bounds()); } void KisImage::refreshGraphAsync(KisNodeSP root, const QRect &rc) { refreshGraphAsync(root, rc, bounds()); } void KisImage::refreshGraphAsync(KisNodeSP root, const QRect &rc, const QRect &cropRect) { if (!root) root = m_d->rootLayer; m_d->animationInterface->notifyNodeChanged(root.data(), rc, true); m_d->scheduler.fullRefreshAsync(root, rc, cropRect); } void KisImage::requestProjectionUpdateNoFilthy(KisNodeSP pseudoFilthy, const QRect &rc, const QRect &cropRect) { KIS_ASSERT_RECOVER_RETURN(pseudoFilthy); m_d->animationInterface->notifyNodeChanged(pseudoFilthy.data(), rc, false); m_d->scheduler.updateProjectionNoFilthy(pseudoFilthy, rc, cropRect); } void KisImage::addSpontaneousJob(KisSpontaneousJob *spontaneousJob) { m_d->scheduler.addSpontaneousJob(spontaneousJob); } void KisImage::setProjectionUpdatesFilter(KisProjectionUpdatesFilterSP filter) { // update filters are *not* recursive! KIS_ASSERT_RECOVER_NOOP(!filter || !m_d->projectionUpdatesFilter); m_d->projectionUpdatesFilter = filter; } KisProjectionUpdatesFilterSP KisImage::projectionUpdatesFilter() const { return m_d->projectionUpdatesFilter; } void KisImage::disableDirtyRequests() { setProjectionUpdatesFilter(KisProjectionUpdatesFilterSP(new KisDropAllProjectionUpdatesFilter())); } void KisImage::enableDirtyRequests() { setProjectionUpdatesFilter(KisProjectionUpdatesFilterSP()); } void KisImage::disableUIUpdates() { m_d->disableUIUpdateSignals.ref(); } void KisImage::enableUIUpdates() { m_d->disableUIUpdateSignals.deref(); } void KisImage::notifyProjectionUpdated(const QRect &rc) { KisUpdateTimeMonitor::instance()->reportUpdateFinished(rc); if (!m_d->disableUIUpdateSignals) { int lod = currentLevelOfDetail(); QRect dirtyRect = !lod ? rc : KisLodTransform::upscaledRect(rc, lod); if (dirtyRect.isEmpty()) return; emit sigImageUpdated(dirtyRect); } } void KisImage::setWorkingThreadsLimit(int value) { m_d->scheduler.setThreadsLimit(value); } int KisImage::workingThreadsLimit() const { return m_d->scheduler.threadsLimit(); } void KisImage::notifySelectionChanged() { /** * The selection is calculated asynchromously, so it is not * handled by disableUIUpdates() and other special signals of * KisImageSignalRouter */ m_d->legacyUndoAdapter.emitSelectionChanged(); /** * Editing of selection masks doesn't necessary produce a * setDirty() call, so in the end of the stroke we need to request * direct update of the UI's cache. */ if (m_d->isolatedRootNode && dynamic_cast(m_d->isolatedRootNode.data())) { notifyProjectionUpdated(bounds()); } } void KisImage::requestProjectionUpdateImpl(KisNode *node, const QVector &rects, const QRect &cropRect) { if (rects.isEmpty()) return; m_d->scheduler.updateProjection(node, rects, cropRect); } void KisImage::requestProjectionUpdate(KisNode *node, const QVector &rects, bool resetAnimationCache) { if (m_d->projectionUpdatesFilter && m_d->projectionUpdatesFilter->filter(this, node, rects, resetAnimationCache)) { return; } if (resetAnimationCache) { m_d->animationInterface->notifyNodeChanged(node, rects, false); } /** * Here we use 'permitted' instead of 'active' intentively, * because the updates may come after the actual stroke has been * finished. And having some more updates for the stroke not * supporting the wrap-around mode will not make much harm. */ if (m_d->wrapAroundModePermitted) { QVector allSplitRects; const QRect boundRect = effectiveLodBounds(); Q_FOREACH (const QRect &rc, rects) { KisWrappedRect splitRect(rc, boundRect); allSplitRects.append(splitRect); } requestProjectionUpdateImpl(node, allSplitRects, boundRect); } else { requestProjectionUpdateImpl(node, rects, bounds()); } KisNodeGraphListener::requestProjectionUpdate(node, rects, resetAnimationCache); } -void KisImage::invalidateFrames(const KisTimeRange &range, const QRect &rect) +void KisImage::invalidateFrames(const KisFrameSet &frames, const QRect &rect) { - m_d->animationInterface->invalidateFrames(range, rect); + m_d->animationInterface->invalidateFrames(frames, rect); } void KisImage::requestTimeSwitch(int time) { m_d->animationInterface->requestTimeSwitchNonGUI(time); } KisNode *KisImage::graphOverlayNode() const { return m_d->overlaySelectionMask.data(); } QList KisImage::compositions() { return m_d->compositions; } void KisImage::addComposition(KisLayerCompositionSP composition) { m_d->compositions.append(composition); } void KisImage::removeComposition(KisLayerCompositionSP composition) { m_d->compositions.removeAll(composition); } bool checkMasksNeedConversion(KisNodeSP root, const QRect &bounds) { KisSelectionMask *mask = dynamic_cast(root.data()); if (mask && (!bounds.contains(mask->paintDevice()->exactBounds()) || mask->selection()->hasShapeSelection())) { return true; } KisNodeSP node = root->firstChild(); while (node) { if (checkMasksNeedConversion(node, bounds)) { return true; } node = node->nextSibling(); } return false; } void KisImage::setWrapAroundModePermitted(bool value) { if (m_d->wrapAroundModePermitted != value) { requestStrokeEnd(); } m_d->wrapAroundModePermitted = value; if (m_d->wrapAroundModePermitted && checkMasksNeedConversion(root(), bounds())) { KisProcessingApplicator applicator(this, root(), KisProcessingApplicator::RECURSIVE, KisImageSignalVector() << ModifiedSignal, kundo2_i18n("Crop Selections")); KisProcessingVisitorSP visitor = new KisCropSelectionsProcessingVisitor(bounds()); applicator.applyVisitor(visitor, KisStrokeJobData::CONCURRENT); applicator.end(); } } bool KisImage::wrapAroundModePermitted() const { return m_d->wrapAroundModePermitted; } bool KisImage::wrapAroundModeActive() const { return m_d->wrapAroundModePermitted && m_d->scheduler.wrapAroundModeSupported(); } void KisImage::setDesiredLevelOfDetail(int lod) { if (m_d->blockLevelOfDetail) { qWarning() << "WARNING: KisImage::setDesiredLevelOfDetail()" << "was called while LoD functionality was being blocked!"; return; } m_d->scheduler.setDesiredLevelOfDetail(lod); } int KisImage::currentLevelOfDetail() const { if (m_d->blockLevelOfDetail) { return 0; } return m_d->scheduler.currentLevelOfDetail(); } void KisImage::setLevelOfDetailBlocked(bool value) { KisImageBarrierLockerRaw l(this); if (value && !m_d->blockLevelOfDetail) { m_d->scheduler.setDesiredLevelOfDetail(0); } m_d->blockLevelOfDetail = value; } void KisImage::explicitRegenerateLevelOfDetail() { if (!m_d->blockLevelOfDetail) { m_d->scheduler.explicitRegenerateLevelOfDetail(); } } bool KisImage::levelOfDetailBlocked() const { return m_d->blockLevelOfDetail; } void KisImage::notifyNodeCollpasedChanged() { emit sigNodeCollapsedChanged(); } KisImageAnimationInterface* KisImage::animationInterface() const { return m_d->animationInterface; } void KisImage::setProofingConfiguration(KisProofingConfigurationSP proofingConfig) { m_d->proofingConfig = proofingConfig; emit sigProofingConfigChanged(); } KisProofingConfigurationSP KisImage::proofingConfiguration() const { if (m_d->proofingConfig) { return m_d->proofingConfig; } return KisProofingConfigurationSP(); } QPointF KisImage::mirrorAxesCenter() const { return m_d->axesCenter; } void KisImage::setMirrorAxesCenter(const QPointF &value) const { m_d->axesCenter = value; } diff --git a/libs/image/kis_image.h b/libs/image/kis_image.h index ba68762313..a856f8e44a 100644 --- a/libs/image/kis_image.h +++ b/libs/image/kis_image.h @@ -1,1121 +1,1122 @@ /* * Copyright (c) 2002 Patrick Julien * Copyright (c) 2007 Boudewijn Rempt * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KIS_IMAGE_H_ #define KIS_IMAGE_H_ #include #include #include #include #include #include #include #include "kis_paint_device.h" // msvc cannot handle forward declarations, so include kis_paint_device here #include "kis_types.h" #include "kis_shared.h" #include "kis_node_graph_listener.h" #include "kis_node_facade.h" #include "kis_image_interfaces.h" #include "kis_strokes_queue_undo_result.h" #include class KisDocument; class KoColorSpace; class KoColor; class KisCompositeProgressProxy; class KisUndoStore; class KisUndoAdapter; class KisImageSignalRouter; class KisPostExecutionUndoAdapter; class KisFilterStrategy; class KoColorProfile; class KisLayerComposition; class KisSpontaneousJob; class KisImageAnimationInterface; class KUndo2MagicString; class KisProofingConfiguration; +class KisFrameSet; namespace KisMetaData { class MergeStrategy; } /** * This is the image class, it contains a tree of KisLayer stack and * meta information about the image. And it also provides some * functions to manipulate the whole image. */ class KRITAIMAGE_EXPORT KisImage : public QObject, public KisStrokesFacade, public KisStrokeUndoFacade, public KisUpdatesFacade, public KisProjectionUpdateListener, public KisNodeFacade, public KisNodeGraphListener, public KisShared { Q_OBJECT public: /// @param colorSpace can be null. in that case it will be initialised to a default color space. KisImage(KisUndoStore *undoStore, qint32 width, qint32 height, const KoColorSpace *colorSpace, const QString& name); ~KisImage() override; public: // KisNodeGraphListener implementation void aboutToAddANode(KisNode *parent, int index) override; void nodeHasBeenAdded(KisNode *parent, int index) override; void aboutToRemoveANode(KisNode *parent, int index) override; void nodeChanged(KisNode * node) override; void invalidateAllFrames() override; void notifySelectionChanged() override; void requestProjectionUpdate(KisNode *node, const QVector &rects, bool resetAnimationCache) override; - void invalidateFrames(const KisTimeRange &range, const QRect &rect) override; + void invalidateFrames(const KisFrameSet &range, const QRect &rect) override; void requestTimeSwitch(int time) override; KisNode* graphOverlayNode() const override; public: // KisProjectionUpdateListener implementation void notifyProjectionUpdated(const QRect &rc) override; public: /** * Set the number of threads used by the image's working threads */ void setWorkingThreadsLimit(int value); /** * Return the number of threads available to the image's working threads */ int workingThreadsLimit() const; /** * Makes a copy of the image with all the layers. If possible, shallow * copies of the layers are made. * * \p exactCopy shows if the copied image should look *exactly* the same as * the other one (according to it's .kra xml representation). It means that * the layers will have the same UUID keys and, therefore, you are not * expected to use the copied image anywhere except for saving. Don't use * this option if you plan to work with the copied image later. */ KisImage *clone(bool exactCopy = false); /** * Render the projection onto a QImage. */ QImage convertToQImage(qint32 x1, qint32 y1, qint32 width, qint32 height, const KoColorProfile * profile); /** * Render the projection onto a QImage. * (this is an overloaded function) */ QImage convertToQImage(QRect imageRect, const KoColorProfile * profile); /** * Render a thumbnail of the projection onto a QImage. */ QImage convertToQImage(const QSize& scaledImageSize, const KoColorProfile *profile); /** * [low-level] Lock the image without waiting for all the internal job queues are processed * * WARNING: Don't use it unless you really know what you are doing! Use barrierLock() instead! * * Waits for all the **currently running** internal jobs to complete and locks the image * for writing. Please note that this function does **not** wait for all the internal * queues to process, so there might be some non-finished actions pending. It means that * you just postpone these actions until you unlock() the image back. Until then, then image * might easily be frozen in some inconsistent state. * * The only sane usage for this function is to lock the image for **emergency** * processing, when some internal action or scheduler got hung up, and you just want * to fetch some data from the image without races. * * In all other cases, please use barrierLock() instead! */ void lock(); /** * Unlocks the image and starts/resumes all the pending internal jobs. If the image * has been locked for a non-readOnly access, then all the internal caches of the image * (e.g. lod-planes) are reset and regeneration jobs are scheduled. */ void unlock(); /** * @return return true if the image is in a locked state, i.e. all the internal * jobs are blocked from execution by calling wither lock() or barrierLock(). * * When the image is locked, the user can do some modifications to the image * contents safely without a perspective having race conditions with internal * image jobs. */ bool locked() const; /** * Sets the mask (it must be a part of the node hierarchy already) to be paited on * the top of all layers. This method does all the locking and syncing for you. It * is executed asynchronously. */ void setOverlaySelectionMask(KisSelectionMaskSP mask); /** * \see setOverlaySelectionMask */ KisSelectionMaskSP overlaySelectionMask() const; /** * \see setOverlaySelectionMask */ bool hasOverlaySelectionMask() const; /** * @return the global selection object or 0 if there is none. The * global selection is always read-write. */ KisSelectionSP globalSelection() const; /** * Retrieve the next automatic layername (XXX: fix to add option to return Mask X) */ QString nextLayerName(const QString &baseName = "") const; /** * Set the automatic layer name counter one back. */ void rollBackLayerName(); /** * @brief start asynchronous operation on resizing the image * * The method will resize the image to fit the new size without * dropping any pixel data. The GUI will get correct * notification with old and new sizes, so it adjust canvas origin * accordingly and avoid jumping of the canvas on screen * * @param newRect the rectangle of the image which will be visible * after operation is completed * * Please note that the actual operation starts asynchronously in * a background, so you cannot expect the image having new size * right after ths call. */ void resizeImage(const QRect& newRect); /** * @brief start asynchronous operation on cropping the image * * The method will **drop** all the image data outside \p newRect * and resize the image to fit the new size. The GUI will get correct * notification with old and new sizes, so it adjust canvas origin * accordingly and avoid jumping of the canvas on screen * * @param newRect the rectangle of the image which will be cut-out * * Please note that the actual operation starts asynchronously in * a background, so you cannot expect the image having new size * right after ths call. */ void cropImage(const QRect& newRect); /** * @brief start asynchronous operation on cropping a subtree of nodes starting at \p node * * The method will **drop** all the layer data outside \p newRect. Neither * image nor a layer will be moved anywhere * * @param newRect the rectangle of the layer which will be cut-out * * Please note that the actual operation starts asynchronously in * a background, so you cannot expect the image having new size * right after ths call. */ void cropNode(KisNodeSP node, const QRect& newRect); /** * @brief start asynchronous operation on scaling the image * @param size new image size in pixels * @param xres new image x-resolution pixels-per-pt * @param yres new image y-resolution pixels-per-pt * @param filterStrategy filtering strategy * * Please note that the actual operation starts asynchronously in * a background, so you cannot expect the image having new size * right after ths call. */ void scaleImage(const QSize &size, qreal xres, qreal yres, KisFilterStrategy *filterStrategy); /** * @brief start asynchronous operation on scaling a subtree of nodes starting at \p node * @param node node to scale * @param scaleX, @param scaleY scale coefficient to be applied to the node * @param filterStrategy filtering strategy * * Please note that the actual operation starts asynchronously in * a background, so you cannot expect the image having new size * right after ths call. */ void scaleNode(KisNodeSP node, qreal scaleX, qreal scaleY, KisFilterStrategy *filterStrategy); /** * @brief start asynchronous operation on rotating the image * * The image is resized to fit the rotated rectangle * * @param radians rotation angle in radians * * Please note that the actual operation starts asynchronously in * a background, so you cannot expect the operation being completed * right after the call */ void rotateImage(double radians); /** * @brief start asynchronous operation on rotating a subtree of nodes starting at \p node * * The image is not resized! * * @param node the root of the subtree to rotate * @param radians rotation angle in radians * * Please note that the actual operation starts asynchronously in * a background, so you cannot expect the operation being completed * right after the call */ void rotateNode(KisNodeSP node, double radians); /** * @brief start asynchronous operation on shearing the image * * The image is resized to fit the sheared polygon * * @param angleX, @param angleY are given in degrees. * * Please note that the actual operation starts asynchronously in * a background, so you cannot expect the operation being completed * right after the call */ void shear(double angleX, double angleY); /** * @brief start asynchronous operation on shearing a subtree of nodes starting at \p node * * The image is not resized! * * @param node the root of the subtree to rotate * @param angleX, @param angleY are given in degrees. * * Please note that the actual operation starts asynchronously in * a background, so you cannot expect the operation being completed * right after the call */ void shearNode(KisNodeSP node, double angleX, double angleY); /** * Convert the image and all its layers to the dstColorSpace */ void convertImageColorSpace(const KoColorSpace *dstColorSpace, KoColorConversionTransformation::Intent renderingIntent, KoColorConversionTransformation::ConversionFlags conversionFlags); /** * Set the color space of the projection (and the root layer) * to dstColorSpace. No conversion is done for other layers, * their colorspace can differ. * NOTE: Note conversion is done, only regeneration, so no rendering * intent needed */ void convertProjectionColorSpace(const KoColorSpace *dstColorSpace); // Get the profile associated with this image const KoColorProfile * profile() const; /** * Set the profile of the image to the new profile and do the same for * all layers that have the same colorspace and profile of the image. * It doesn't do any pixel conversion. * * This is essential if you have loaded an image that didn't * have an embedded profile to which you want to attach the right profile. * * This does not create an undo action; only call it when creating or * loading an image. * * @returns false if the profile could not be assigned */ bool assignImageProfile(const KoColorProfile *profile); /** * Returns the current undo adapter. You can add new commands to the * undo stack using the adapter. This adapter is used for a backward * compatibility for old commands created before strokes. It blocks * all the porcessing at the scheduler, waits until it's finished * and executes commands exclusively. */ KisUndoAdapter* undoAdapter() const; /** * This adapter is used by the strokes system. The commands are added * to it *after* redo() is done (in the scheduler context). They are * wrapped into a special command and added to the undo stack. redo() * in not called. */ KisPostExecutionUndoAdapter* postExecutionUndoAdapter() const override; /** * Replace current undo store with the new one. The old store * will be deleted. * This method is used by KisDocument for dropping all the commands * during file loading. */ void setUndoStore(KisUndoStore *undoStore); /** * Return current undo store of the image */ KisUndoStore* undoStore(); /** * Tell the image it's modified; this emits the sigImageModified * signal. This happens when the image needs to be saved */ void setModified(); /** * The default colorspace of this image: new layers will have this * colorspace and the projection will have this colorspace. */ const KoColorSpace * colorSpace() const; /** * X resolution in pixels per pt */ double xRes() const; /** * Y resolution in pixels per pt */ double yRes() const; /** * Set the resolution in pixels per pt. */ void setResolution(double xres, double yres); /** * Convert a document coordinate to a pixel coordinate. * * @param documentCoord PostScript Pt coordinate to convert. */ QPointF documentToPixel(const QPointF &documentCoord) const; /** * Convert a document coordinate to an integer pixel coordinate rounded down. * * @param documentCoord PostScript Pt coordinate to convert. */ QPoint documentToImagePixelFloored(const QPointF &documentCoord) const; /** * Convert a document rectangle to a pixel rectangle. * * @param documentRect PostScript Pt rectangle to convert. */ QRectF documentToPixel(const QRectF &documentRect) const; /** * Convert a pixel coordinate to a document coordinate. * * @param pixelCoord pixel coordinate to convert. */ QPointF pixelToDocument(const QPointF &pixelCoord) const; /** * Convert an integer pixel coordinate to a document coordinate. * The document coordinate is at the centre of the pixel. * * @param pixelCoord pixel coordinate to convert. */ QPointF pixelToDocument(const QPoint &pixelCoord) const; /** * Convert a document rectangle to an integer pixel rectangle. * * @param pixelCoord pixel coordinate to convert. */ QRectF pixelToDocument(const QRectF &pixelCoord) const; /** * Return the width of the image */ qint32 width() const; /** * Return the height of the image */ qint32 height() const; /** * Return the size of the image */ QSize size() const { return QSize(width(), height()); } /** * @return the root node of the image node graph */ KisGroupLayerSP rootLayer() const; /** * Return the projection; that is, the complete, composited * representation of this image. */ KisPaintDeviceSP projection() const; /** * Return the number of layers (not other nodes) that are in this * image. */ qint32 nlayers() const; /** * Return the number of layers (not other node types) that are in * this image and that are hidden. */ qint32 nHiddenLayers() const; /** * Merge all visible layers and discard hidden ones. */ void flatten(); /** * Merge the specified layer with the layer * below this layer, remove the specified layer. */ void mergeDown(KisLayerSP l, const KisMetaData::MergeStrategy* strategy); /** * flatten the layer: that is, the projection becomes the layer * and all subnodes are removed. If this is not a paint layer, it will morph * into a paint layer. */ void flattenLayer(KisLayerSP layer); /** * Merges layers in \p mergedLayers and creates a new layer above * \p putAfter */ void mergeMultipleLayers(QList mergedLayers, KisNodeSP putAfter); /// @return the exact bounds of the image in pixel coordinates. QRect bounds() const; /** * Returns the actual bounds of the image, taking LevelOfDetail * into account. This value is used as a bounds() value of * KisDefaultBounds object. */ QRect effectiveLodBounds() const; /// use if the layers have changed _completely_ (eg. when flattening) void notifyLayersChanged(); /** * Sets the default color of the root layer projection. All the layers * will be merged on top of this very color */ void setDefaultProjectionColor(const KoColor &color); /** * \see setDefaultProjectionColor() */ KoColor defaultProjectionColor() const; void setRootLayer(KisGroupLayerSP rootLayer); /** * Add an annotation for this image. This can be anything: Gamma, EXIF, etc. * Note that the "icc" annotation is reserved for the color strategies. * If the annotation already exists, overwrite it with this one. */ void addAnnotation(KisAnnotationSP annotation); /** get the annotation with the given type, can return 0 */ KisAnnotationSP annotation(const QString& type); /** delete the annotation, if the image contains it */ void removeAnnotation(const QString& type); /** * Start of an iteration over the annotations of this image (including the ICC Profile) */ vKisAnnotationSP_it beginAnnotations(); /** end of an iteration over the annotations of this image */ vKisAnnotationSP_it endAnnotations(); /** * Called before the image is delted and sends the sigAboutToBeDeleted signal */ void notifyAboutToBeDeleted(); KisImageSignalRouter* signalRouter(); /** * Returns whether we can reselect current global selection * * \see reselectGlobalSelection() */ bool canReselectGlobalSelection(); /** * Returns the layer compositions for the image */ QList compositions(); /** * Adds a new layer composition, will be saved with the image */ void addComposition(KisLayerCompositionSP composition); /** * Remove the layer compostion */ void removeComposition(KisLayerCompositionSP composition); /** * Permit or deny the wrap-around mode for all the paint devices * of the image. Note that permitting the wraparound mode will not * necessarily activate it right now. To be activated the wrap * around mode should be 1) permitted; 2) supported by the * currently running stroke. */ void setWrapAroundModePermitted(bool value); /** * \return whether the wrap-around mode is permitted for this * image. If the wrap around mode is permitted and the * currently running stroke supports it, the mode will be * activated for all paint devices of the image. * * \see setWrapAroundMode */ bool wrapAroundModePermitted() const; /** * \return whether the wraparound mode is activated for all the * devices of the image. The mode is activated when both * factors are true: the user permitted it and the stroke * supports it */ bool wrapAroundModeActive() const; /** * \return current level of detail which is used when processing the image. * Current working zoom = 2 ^ (- currentLevelOfDetail()). Default value is * null, which means we work on the original image. */ int currentLevelOfDetail() const; /** * Notify KisImage which level of detail should be used in the * lod-mode. Setting the mode does not guarantee the LOD to be * used. It will be activated only when the stokes supports it. */ void setDesiredLevelOfDetail(int lod); /** * Relative position of the mirror axis center * 0,0 - topleft corner of the image * 1,1 - bottomright corner of the image */ QPointF mirrorAxesCenter() const; /** * Sets the relative position of the axes center * \see mirrorAxesCenter() for details */ void setMirrorAxesCenter(const QPointF &value) const; public Q_SLOTS: /** * Explicitly start regeneration of LoD planes of all the devices * in the image. This call should be performed when the user is idle, * just to make the quality of image updates better. */ void explicitRegenerateLevelOfDetail(); public: /** * Blocks usage of level of detail functionality. After this method * has been called, no new strokes will use LoD. */ void setLevelOfDetailBlocked(bool value); /** * \see setLevelOfDetailBlocked() */ bool levelOfDetailBlocked() const; /** * Notifies that the node collapsed state has changed */ void notifyNodeCollpasedChanged(); KisImageAnimationInterface *animationInterface() const; /** * @brief setProofingConfiguration, this sets the image's proofing configuration, and signals * the proofingConfiguration has changed. * @param proofingConfig - the kis proofing config that will be used instead. */ void setProofingConfiguration(KisProofingConfigurationSP proofingConfig); /** * @brief proofingConfiguration * @return the proofing configuration of the image. */ KisProofingConfigurationSP proofingConfiguration() const; public Q_SLOTS: bool startIsolatedMode(KisNodeSP node); void stopIsolatedMode(); public: KisNodeSP isolatedModeRoot() const; Q_SIGNALS: /** * Emitted whenever an action has caused the image to be * recomposited. * * @param rc The rect that has been recomposited. */ void sigImageUpdated(const QRect &); /** Emitted whenever the image has been modified, so that it doesn't match with the version saved on disk. */ void sigImageModified(); /** * The signal is emitted when the size of the image is changed. * \p oldStillPoint and \p newStillPoint give the receiver the * hint about how the new and old rect of the image correspond to * each other. They specify the point of the image around which * the conversion was done. This point will stay still on the * user's screen. That is the \p newStillPoint of the new image * will be painted at the same screen position, where \p * oldStillPoint of the old image was painted. * * \param oldStillPoint is a still point represented in *old* * image coordinates * * \param newStillPoint is a still point represented in *new* * image coordinates */ void sigSizeChanged(const QPointF &oldStillPoint, const QPointF &newStillPoint); void sigProfileChanged(const KoColorProfile * profile); void sigColorSpaceChanged(const KoColorSpace* cs); void sigResolutionChanged(double xRes, double yRes); void sigRequestNodeReselection(KisNodeSP activeNode, const KisNodeList &selectedNodes); /** * Inform the model that a node was changed */ void sigNodeChanged(KisNodeSP node); /** * Inform that the image is going to be deleted */ void sigAboutToBeDeleted(); /** * The signal is emitted right after a node has been connected * to the graph of the nodes. * * WARNING: you must not request any graph-related information * about the node being run in a not-scheduler thread. If you need * information about the parent/siblings of the node connect * with Qt::DirectConnection, get needed information and then * emit another Qt::AutoConnection signal to pass this information * to your thread. See details of the implementation * in KisDummiesfacadeBase. */ void sigNodeAddedAsync(KisNodeSP node); /** * This signal is emitted right before a node is going to removed * from the graph of the nodes. * * WARNING: you must not request any graph-related information * about the node being run in a not-scheduler thread. * * \see comment in sigNodeAddedAsync() */ void sigRemoveNodeAsync(KisNodeSP node); /** * Emitted when the root node of the image has changed. * It happens, e.g. when we flatten the image. When * this happens the receiver should reload information * about the image */ void sigLayersChangedAsync(); /** * Emitted when the UI has requested the undo of the last stroke's * operation. The point is, we cannot deal with the internals of * the stroke without its creator knowing about it (which most * probably cause a crash), so we just forward this request from * the UI to the creator of the stroke. * * If your tool supports undoing part of its work, just listen to * this signal and undo when it comes */ void sigUndoDuringStrokeRequested(); /** * Emitted when the UI has requested the cancellation of * the stroke. The point is, we cannot cancel the stroke * without its creator knowing about it (which most probably * cause a crash), so we just forward this request from the UI * to the creator of the stroke. * * If your tool supports cancelling of its work in the middle * of operation, just listen to this signal and cancel * the stroke when it comes */ void sigStrokeCancellationRequested(); /** * Emitted when the image decides that the stroke should better * be ended. The point is, we cannot just end the stroke * without its creator knowing about it (which most probably * cause a crash), so we just forward this request from the UI * to the creator of the stroke. * * If your tool supports long strokes that may involve multiple * mouse actions in one stroke, just listen to this signal and * end the stroke when it comes. */ void sigStrokeEndRequested(); /** * Same as sigStrokeEndRequested() but is not emitted when the active node * is changed. */ void sigStrokeEndRequestedActiveNodeFiltered(); /** * Emitted when the isolated mode status has changed. * * Can be used by the receivers to catch a fact of forcefully * stopping the isolated mode by the image when some complex * action was requested */ void sigIsolatedModeChanged(); /** * Emitted when one or more nodes changed the collapsed state * */ void sigNodeCollapsedChanged(); /** * Emitted when the proofing configuration of the image is being changed. * */ void sigProofingConfigChanged(); /** * Internal signal for asynchronously requesting isolated mode to stop. Don't use it * outside KisImage, use sigIsolatedModeChanged() instead. */ void sigInternalStopIsolatedModeRequested(); public Q_SLOTS: KisCompositeProgressProxy* compositeProgressProxy(); bool isIdle(bool allowLocked = false); /** * @brief Wait until all the queued background jobs are completed and lock the image. * * KisImage object has a local scheduler that executes long-running image * rendering/modifying jobs (we call them "strokes") in a background. Basically, * one should either access the image from the scope of such jobs (strokes) or * just lock the image before using. * * Calling barrierLock() will wait until all the queued operations are finished * and lock the image, so you can start accessing it in a safe way. * * @p readOnly tells the image if the caller is going to modify the image during * holding the lock. Locking with non-readOnly access will reset all * the internal caches of the image (lod-planes) when the lock status * will be lifted. */ void barrierLock(bool readOnly = false); /** * @brief Tries to lock the image without waiting for the jobs to finish * * Same as barrierLock(), but doesn't block execution of the calling thread * until all the background jobs are finished. Instead, in case of presence of * unfinished jobs in the queue, it just returns false * * @return whether the lock has been acquired * @see barrierLock */ bool tryBarrierLock(bool readOnly = false); /** * Wait for all the internal image jobs to complete and return without locking * the image. This function is handly for tests or other synchronous actions, * when one needs to wait for the result of his actions. */ void waitForDone(); KisStrokeId startStroke(KisStrokeStrategy *strokeStrategy) override; void addJob(KisStrokeId id, KisStrokeJobData *data) override; void endStroke(KisStrokeId id) override; bool cancelStroke(KisStrokeId id) override; /** * @brief blockUpdates block updating the image projection */ void blockUpdates() override; /** * @brief unblockUpdates unblock updating the image project. This * only restarts the scheduler and does not schedule a full refresh. */ void unblockUpdates() override; /** * Disables notification of the UI about the changes in the image. * This feature is used by KisProcessingApplicator. It is needed * when we change the size of the image. In this case, the whole * image will be reloaded into UI by sigSizeChanged(), so there is * no need to inform the UI about individual dirty rects. */ void disableUIUpdates() override; /** * \see disableUIUpdates */ void enableUIUpdates() override; /** * Disables the processing of all the setDirty() requests that * come to the image. The incoming requests are effectively * *dropped*. * * This feature is used by KisProcessingApplicator. For many cases * it provides its own updates interface, which recalculates the * whole subtree of nodes. But while we change any particular * node, it can ask for an update itself. This method is a way of * blocking such intermediate (and excessive) requests. * * NOTE: this is a convenience function for setProjectionUpdatesFilter() * that installs a predefined filter that eats everything. Please * note that these calls are *not* recursive */ void disableDirtyRequests() override; /** * \see disableDirtyRequests() */ void enableDirtyRequests() override; /** * Installs a filter object that will filter all the incoming projection update * requests. If the filter return true, the incoming update is dropped. * * NOTE: you cannot set filters recursively! */ void setProjectionUpdatesFilter(KisProjectionUpdatesFilterSP filter) override; /** * \see setProjectionUpdatesFilter() */ KisProjectionUpdatesFilterSP projectionUpdatesFilter() const override; void refreshGraphAsync(KisNodeSP root = KisNodeSP()) override; void refreshGraphAsync(KisNodeSP root, const QRect &rc) override; void refreshGraphAsync(KisNodeSP root, const QRect &rc, const QRect &cropRect) override; /** * Triggers synchronous recomposition of the projection */ void refreshGraph(KisNodeSP root = KisNodeSP()); void refreshGraph(KisNodeSP root, const QRect& rc, const QRect &cropRect); void initialRefreshGraph(); /** * Initiate a stack regeneration skipping the recalculation of the * filthy node's projection. * * Works exactly as pseudoFilthy->setDirty() with the only * exception that pseudoFilthy::updateProjection() will not be * called. That is used by KisRecalculateTransformMaskJob to avoid * cyclic dependencies. */ void requestProjectionUpdateNoFilthy(KisNodeSP pseudoFilthy, const QRect &rc, const QRect &cropRect); /** * Adds a spontaneous job to the updates queue. * * A spontaneous job may do some trivial tasks in the background, * like updating the outline of selection or purging unused tiles * from the existing paint devices. */ void addSpontaneousJob(KisSpontaneousJob *spontaneousJob); /** * This method is called by the UI (*not* by the creator of the * stroke) when it thinks the current stroke should undo its last * action, for example, when the user presses Ctrl+Z while some * stroke is active. * * If the creator of the stroke supports undoing of intermediate * actions, it will be notified about this request and can undo * its last action. */ void requestUndoDuringStroke(); /** * This method is called by the UI (*not* by the creator of the * stroke) when it thinks current stroke should be cancelled. If * there is a running stroke that has already been detached from * its creator (ended or cancelled), it will be forcefully * cancelled and reverted. If there is an open stroke present, and * if its creator supports cancelling, it will be notified about * the request and the stroke will be cancelled */ void requestStrokeCancellation(); /** * This method requests the last stroke executed on the image to become undone. * If the stroke is not ended, or if all the Lod0 strokes are completed, the method * returns UNDO_FAIL. If the last Lod0 is going to be finished soon, then UNDO_WAIT * is returned and the caller should just wait for its completion and call global undo * instead. UNDO_OK means one unfinished stroke has been undone. */ UndoResult tryUndoUnfinishedLod0Stroke(); /** * This method is called when image or some other part of Krita * (*not* the creator of the stroke) decides that the stroke * should be ended. If the creator of the stroke supports it, it * will be notified and the stroke will be cancelled */ void requestStrokeEnd(); /** * Same as requestStrokeEnd() but is called by view manager when * the current node is changed. Use to dintinguish * sigStrokeEndRequested() and * sigStrokeEndRequestedActiveNodeFiltered() which are used by * KisNodeJugglerCompressed */ void requestStrokeEndActiveNode(); private: KisImage(const KisImage& rhs, KisUndoStore *undoStore, bool exactCopy); KisImage& operator=(const KisImage& rhs); void emitSizeChanged(); void resizeImageImpl(const QRect& newRect, bool cropLayers); void rotateImpl(const KUndo2MagicString &actionName, KisNodeSP rootNode, bool resizeImage, double radians); void shearImpl(const KUndo2MagicString &actionName, KisNodeSP rootNode, bool resizeImage, double angleX, double angleY, const QPointF &origin); void safeRemoveTwoNodes(KisNodeSP node1, KisNodeSP node2); void refreshHiddenArea(KisNodeSP rootNode, const QRect &preparedArea); void requestProjectionUpdateImpl(KisNode *node, const QVector &rects, const QRect &cropRect); friend class KisImageResizeCommand; void setSize(const QSize& size); friend class KisImageSetProjectionColorSpaceCommand; void setProjectionColorSpace(const KoColorSpace * colorSpace); friend class KisDeselectGlobalSelectionCommand; friend class KisReselectGlobalSelectionCommand; friend class KisSetGlobalSelectionCommand; friend class KisImageTest; friend class Document; // For libkis /** * Replaces the current global selection with globalSelection. If * \p globalSelection is empty, removes the selection object, so that * \ref globalSelection() will return 0 after that. */ void setGlobalSelection(KisSelectionSP globalSelection); /** * Deselects current global selection. * \ref globalSelection() will return 0 after that. */ void deselectGlobalSelection(); /** * Reselects current deselected selection * * \see deselectGlobalSelection() */ void reselectGlobalSelection(); private: class KisImagePrivate; KisImagePrivate * m_d; }; #endif // KIS_IMAGE_H_ diff --git a/libs/image/kis_image_animation_interface.cpp b/libs/image/kis_image_animation_interface.cpp index 55e0e49bc2..27861cade8 100644 --- a/libs/image/kis_image_animation_interface.cpp +++ b/libs/image/kis_image_animation_interface.cpp @@ -1,423 +1,423 @@ /* * Copyright (c) 2015 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_image_animation_interface.h" #include #include "kis_global.h" #include "kis_image.h" #include "kis_regenerate_frame_stroke_strategy.h" #include "kis_switch_time_stroke_strategy.h" #include "kis_keyframe_channel.h" #include "kis_time_range.h" #include "kis_post_execution_undo_adapter.h" #include "commands_new/kis_switch_current_time_command.h" #include "kis_layer_utils.h" struct KisImageAnimationInterface::Private { Private() : image(0), externalFrameActive(false), frameInvalidationBlocked(false), cachedLastFrameValue(-1), audioChannelMuted(false), audioChannelVolume(0.5), m_currentTime(0), m_currentUITime(0) { } Private(const Private &rhs, KisImage *newImage) : image(newImage), externalFrameActive(false), frameInvalidationBlocked(false), fullClipRange(rhs.fullClipRange), playbackRange(rhs.playbackRange), framerate(rhs.framerate), cachedLastFrameValue(-1), audioChannelFileName(rhs.audioChannelFileName), audioChannelMuted(rhs.audioChannelMuted), audioChannelVolume(rhs.audioChannelVolume), m_currentTime(rhs.m_currentTime), m_currentUITime(rhs.m_currentUITime) { } KisImage *image; bool externalFrameActive; bool frameInvalidationBlocked; KisTimeSpan fullClipRange; KisTimeSpan playbackRange; int framerate; int cachedLastFrameValue; QString audioChannelFileName; bool audioChannelMuted; qreal audioChannelVolume; KisSwitchTimeStrokeStrategy::SharedTokenWSP switchToken; inline int currentTime() const { return m_currentTime; } inline int currentUITime() const { return m_currentUITime; } inline void setCurrentTime(int value) { m_currentTime = value; } inline void setCurrentUITime(int value) { m_currentUITime = value; } private: int m_currentTime; int m_currentUITime; }; KisImageAnimationInterface::KisImageAnimationInterface(KisImage *image) : QObject(image), m_d(new Private) { m_d->image = image; m_d->framerate = 24; m_d->fullClipRange = KisTimeSpan(0, 100); connect(this, SIGNAL(sigInternalRequestTimeSwitch(int, bool)), SLOT(switchCurrentTimeAsync(int, bool))); } KisImageAnimationInterface::KisImageAnimationInterface(const KisImageAnimationInterface &rhs, KisImage *newImage) : m_d(new Private(*rhs.m_d, newImage)) { connect(this, SIGNAL(sigInternalRequestTimeSwitch(int, bool)), SLOT(switchCurrentTimeAsync(int, bool))); } KisImageAnimationInterface::~KisImageAnimationInterface() { } bool KisImageAnimationInterface::hasAnimation() const { bool hasAnimation = false; KisLayerUtils::recursiveApplyNodes( m_d->image->root(), [&hasAnimation](KisNodeSP node) { hasAnimation |= node->isAnimated(); }); return hasAnimation; } int KisImageAnimationInterface::currentTime() const { return m_d->currentTime(); } int KisImageAnimationInterface::currentUITime() const { return m_d->currentUITime(); } const KisTimeSpan& KisImageAnimationInterface::fullClipRange() const { return m_d->fullClipRange; } void KisImageAnimationInterface::setFullClipRange(const KisTimeSpan range) { m_d->fullClipRange = range; emit sigFullClipRangeChanged(); } void KisImageAnimationInterface::setFullClipRangeStartTime(int column) { KisTimeSpan newRange(column, m_d->fullClipRange.end()); setFullClipRange(newRange); } void KisImageAnimationInterface::setFullClipRangeEndTime(int column) { KisTimeSpan newRange(m_d->fullClipRange.start(), column); setFullClipRange(newRange); } const KisTimeSpan& KisImageAnimationInterface::playbackRange() const { return !m_d->playbackRange.isEmpty() ? m_d->playbackRange : m_d->fullClipRange; } void KisImageAnimationInterface::setPlaybackRange(const KisTimeSpan range) { m_d->playbackRange = range; emit sigPlaybackRangeChanged(); } int KisImageAnimationInterface::framerate() const { return m_d->framerate; } QString KisImageAnimationInterface::audioChannelFileName() const { return m_d->audioChannelFileName; } void KisImageAnimationInterface::setAudioChannelFileName(const QString &fileName) { QFileInfo info(fileName); KIS_SAFE_ASSERT_RECOVER_NOOP(fileName.isEmpty() || info.isAbsolute()); m_d->audioChannelFileName = fileName.isEmpty() ? fileName : info.absoluteFilePath(); emit sigAudioChannelChanged(); } bool KisImageAnimationInterface::isAudioMuted() const { return m_d->audioChannelMuted; } void KisImageAnimationInterface::setAudioMuted(bool value) { m_d->audioChannelMuted = value; emit sigAudioChannelChanged(); } qreal KisImageAnimationInterface::audioVolume() const { return m_d->audioChannelVolume; } void KisImageAnimationInterface::setAudioVolume(qreal value) { m_d->audioChannelVolume = value; emit sigAudioVolumeChanged(); } void KisImageAnimationInterface::setFramerate(int fps) { m_d->framerate = fps; emit sigFramerateChanged(); } KisImageWSP KisImageAnimationInterface::image() const { return m_d->image; } bool KisImageAnimationInterface::externalFrameActive() const { return m_d->externalFrameActive; } void KisImageAnimationInterface::requestTimeSwitchWithUndo(int time) { if (currentUITime() == time) return; requestTimeSwitchNonGUI(time, true); } void KisImageAnimationInterface::setDefaultProjectionColor(const KoColor &color) { int savedTime = 0; saveAndResetCurrentTime(currentTime(), &savedTime); m_d->image->setDefaultProjectionColor(color); restoreCurrentTime(&savedTime); } void KisImageAnimationInterface::requestTimeSwitchNonGUI(int time, bool useUndo) { emit sigInternalRequestTimeSwitch(time, useUndo); } void KisImageAnimationInterface::explicitlySetCurrentTime(int frameId) { m_d->setCurrentTime(frameId); } void KisImageAnimationInterface::switchCurrentTimeAsync(int frameId, bool useUndo) { if (currentUITime() == frameId) return; - const KisTimeRange range = KisTimeRange::calculateIdenticalFramesRecursive(m_d->image->root(), currentUITime()); + const KisFrameSet range = calculateIdenticalFramesRecursive(m_d->image->root(), currentUITime()); const bool needsRegeneration = !range.contains(frameId); KisSwitchTimeStrokeStrategy::SharedTokenSP token = m_d->switchToken.toStrongRef(); if (!token || !token->tryResetDestinationTime(frameId, needsRegeneration)) { { KisPostExecutionUndoAdapter *undoAdapter = useUndo ? m_d->image->postExecutionUndoAdapter() : 0; KisSwitchTimeStrokeStrategy *strategy = new KisSwitchTimeStrokeStrategy(frameId, needsRegeneration, this, undoAdapter); m_d->switchToken = strategy->token(); KisStrokeId stroke = m_d->image->startStroke(strategy); m_d->image->endStroke(stroke); } if (needsRegeneration) { KisStrokeStrategy *strategy = new KisRegenerateFrameStrokeStrategy(this); KisStrokeId strokeId = m_d->image->startStroke(strategy); m_d->image->endStroke(strokeId); } } m_d->setCurrentUITime(frameId); emit sigUiTimeChanged(frameId); } void KisImageAnimationInterface::requestFrameRegeneration(int frameId, const QRegion &dirtyRegion) { KisStrokeStrategy *strategy = new KisRegenerateFrameStrokeStrategy(frameId, dirtyRegion, this); QList jobs = KisRegenerateFrameStrokeStrategy::createJobsData(m_d->image); KisStrokeId stroke = m_d->image->startStroke(strategy); Q_FOREACH (KisStrokeJobData* job, jobs) { m_d->image->addJob(stroke, job); } m_d->image->endStroke(stroke); } void KisImageAnimationInterface::saveAndResetCurrentTime(int frameId, int *savedValue) { m_d->externalFrameActive = true; *savedValue = m_d->currentTime(); m_d->setCurrentTime(frameId); } void KisImageAnimationInterface::restoreCurrentTime(int *savedValue) { m_d->setCurrentTime(*savedValue); m_d->externalFrameActive = false; } void KisImageAnimationInterface::notifyFrameReady() { emit sigFrameReady(m_d->currentTime()); } void KisImageAnimationInterface::notifyFrameCancelled() { emit sigFrameCancelled(); } KisUpdatesFacade* KisImageAnimationInterface::updatesFacade() const { return m_d->image; } void KisImageAnimationInterface::notifyNodeChanged(const KisNode *node, const QRect &rect, bool recursive) { notifyNodeChanged(node, QVector({rect}), recursive); } void KisImageAnimationInterface::notifyNodeChanged(const KisNode *node, const QVector &rects, bool recursive) { if (externalFrameActive() || m_d->frameInvalidationBlocked) return; // even overlay selection masks are not rendered in the cache if (node->inherits("KisSelectionMask")) return; const int currentTime = m_d->currentTime(); - KisTimeRange invalidateRange; + KisFrameSet invalidateRange; if (recursive) { - invalidateRange = KisTimeRange::calculateAffectedFramesRecursive(node, currentTime); + invalidateRange = calculateAffectedFramesRecursive(node, currentTime); } else { - invalidateRange = KisTimeRange::calculateNodeAffectedFrames(node, currentTime); + invalidateRange = calculateNodeAffectedFrames(node, currentTime); } // we compress the updated rect (atm, no one uses it anyway) QRect unitedRect; Q_FOREACH (const QRect &rc, rects) { unitedRect |= rc; } invalidateFrames(invalidateRange, unitedRect); } -void KisImageAnimationInterface::invalidateFrames(const KisTimeRange &range, const QRect &rect) +void KisImageAnimationInterface::invalidateFrames(const KisFrameSet &range, const QRect &rect) { m_d->cachedLastFrameValue = -1; emit sigFramesChanged(range, rect); } void KisImageAnimationInterface::blockFrameInvalidation(bool value) { m_d->frameInvalidationBlocked = value; } int findLastKeyframeTimeRecursive(KisNodeSP node) { int time = 0; KisKeyframeChannel *channel; Q_FOREACH (channel, node->keyframeChannels()) { KisKeyframeSP keyframe = channel->lastKeyframe(); if (keyframe) { time = std::max(time, keyframe->time()); } } KisNodeSP child = node->firstChild(); while (child) { time = std::max(time, findLastKeyframeTimeRecursive(child)); child = child->nextSibling(); } return time; } int KisImageAnimationInterface::totalLength() { if (m_d->cachedLastFrameValue < 0) { m_d->cachedLastFrameValue = findLastKeyframeTimeRecursive(m_d->image->root()); } int lastKey = m_d->cachedLastFrameValue; lastKey = std::max(lastKey, m_d->fullClipRange.end()); lastKey = std::max(lastKey, m_d->currentUITime()); return lastKey + 1; } diff --git a/libs/image/kis_image_animation_interface.h b/libs/image/kis_image_animation_interface.h index 866c2b6fc4..64bf791637 100644 --- a/libs/image/kis_image_animation_interface.h +++ b/libs/image/kis_image_animation_interface.h @@ -1,215 +1,215 @@ /* * Copyright (c) 2015 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __KIS_IMAGE_ANIMATION_INTERFACE_H #define __KIS_IMAGE_ANIMATION_INTERFACE_H #include #include #include "kis_types.h" #include "kritaimage_export.h" class KisUpdatesFacade; -class KisTimeRange; +class KisFrameSet; class KisTimeSpan; class KoColor; namespace KisLayerUtils { struct SwitchFrameCommand; } class KRITAIMAGE_EXPORT KisImageAnimationInterface : public QObject { Q_OBJECT public: KisImageAnimationInterface(KisImage *image); KisImageAnimationInterface(const KisImageAnimationInterface &rhs, KisImage *newImage); ~KisImageAnimationInterface() override; /** * Returns true of the image has at least one animated layer */ bool hasAnimation() const; /** * Returns currently active frame of the underlying image. Some strokes * can override this value and it will report a different value. */ int currentTime() const; /** * Same as currentTime, except it isn't changed when background strokes * are running. */ int currentUITime() const; /** * While any non-current frame is being regenerated by the * strategy, the image is kept in a special state, named * 'externalFrameActive'. Is this state the following applies: * * 1) All the animated paint devices switch its state into * frameId() defined by global time. * * 2) All animation-not-capable devices switch to a temporary * content device, which *is in undefined state*. The stroke * should regenerate the image projection manually. */ bool externalFrameActive() const; void requestTimeSwitchWithUndo(int time); void requestTimeSwitchNonGUI(int time, bool useUndo = false); public Q_SLOTS: /** * Switches current frame (synchronously) and starts an * asynchronous regeneration of the entire image. */ void switchCurrentTimeAsync(int frameId, bool useUndo = false); public: /** * Start a background thread that will recalculate some extra frame. * The result will be reported using two types of signals: * * 1) KisImage::sigImageUpdated() will be emitted for every chunk * of updated area. * * 2) sigFrameReady() will be emitted in the end of the operation. * IMPORTANT: to get the result you must connect to this signal * with Qt::DirectConnection and fetch the result from * frameProjection(). After the signal handler is exited, the * data will no longer be available. */ void requestFrameRegeneration(int frameId, const QRegion &dirtyRegion); void notifyNodeChanged(const KisNode *node, const QRect &rect, bool recursive); void notifyNodeChanged(const KisNode *node, const QVector &rects, bool recursive); - void invalidateFrames(const KisTimeRange &range, const QRect &rect); + void invalidateFrames(const KisFrameSet &range, const QRect &rect); /** * Changes the default color of the "external frame" projection of * the image's root layer. Please note that this command should be * executed from a context of an exclusive job! */ void setDefaultProjectionColor(const KoColor &color); /** * The current time range selected by user. * @return current time range */ const KisTimeSpan& fullClipRange() const; void setFullClipRange(const KisTimeSpan range); void setFullClipRangeStartTime(int column); void setFullClipRangeEndTime(int column); const KisTimeSpan &playbackRange() const; void setPlaybackRange(const KisTimeSpan range); int framerate() const; /** * @return **absolute** file name of the audio channel file */ QString audioChannelFileName() const; /** * Sets **absolute** file name of the audio channel file. Don't try to pass * a relative path, it'll assert! */ void setAudioChannelFileName(const QString &fileName); /** * @return is the audio channel is currently muted */ bool isAudioMuted() const; /** * Mutes the audio channel */ void setAudioMuted(bool value); /** * Returns the preferred audio value in rangle [0, 1] */ qreal audioVolume() const; /** * Set the preferred volume for the audio channel in range [0, 1] */ void setAudioVolume(qreal value); public Q_SLOTS: void setFramerate(int fps); public: KisImageWSP image() const; int totalLength(); private: // interface for: friend class KisRegenerateFrameStrokeStrategy; friend class KisAnimationFrameCacheTest; friend struct KisLayerUtils::SwitchFrameCommand; friend class KisImageTest; void saveAndResetCurrentTime(int frameId, int *savedValue); void restoreCurrentTime(int *savedValue); void notifyFrameReady(); void notifyFrameCancelled(); KisUpdatesFacade* updatesFacade() const; void blockFrameInvalidation(bool value); friend class KisSwitchTimeStrokeStrategy; void explicitlySetCurrentTime(int frameId); Q_SIGNALS: void sigFrameReady(int time); void sigFrameCancelled(); void sigUiTimeChanged(int newTime); - void sigFramesChanged(const KisTimeRange &range, const QRect &rect); + void sigFramesChanged(const KisFrameSet &range, const QRect &rect); void sigInternalRequestTimeSwitch(int frameId, bool useUndo); void sigFramerateChanged(); void sigFullClipRangeChanged(); void sigPlaybackRangeChanged(); /** * Emitted when the audio channel of the document is changed */ void sigAudioChannelChanged(); /** * Emitted when audion volume changes. Please note that it doesn't change * when you mute the channel! When muting, sigAudioChannelChanged() is used instead! */ void sigAudioVolumeChanged(); private: struct Private; const QScopedPointer m_d; }; #endif /* __KIS_IMAGE_ANIMATION_INTERFACE_H */ diff --git a/libs/image/kis_keyframe_channel.cpp b/libs/image/kis_keyframe_channel.cpp index 00fa1c1665..4200ecbcb2 100644 --- a/libs/image/kis_keyframe_channel.cpp +++ b/libs/image/kis_keyframe_channel.cpp @@ -1,687 +1,687 @@ /* * Copyright (c) 2015 Jouni Pentikäinen * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_keyframe_channel.h" #include "KoID.h" #include "kis_global.h" #include "kis_node.h" #include "kis_time_range.h" #include "kundo2command.h" #include "kis_keyframe_commands.h" #include const KoID KisKeyframeChannel::Content = KoID("content", ki18n("Content")); const KoID KisKeyframeChannel::Opacity = KoID("opacity", ki18n("Opacity")); const KoID KisKeyframeChannel::TransformArguments = KoID("transform_arguments", ki18n("Transform")); const KoID KisKeyframeChannel::TransformPositionX = KoID("transform_pos_x", ki18n("Position (X)")); const KoID KisKeyframeChannel::TransformPositionY = KoID("transform_pos_y", ki18n("Position (Y)")); const KoID KisKeyframeChannel::TransformScaleX = KoID("transform_scale_x", ki18n("Scale (X)")); const KoID KisKeyframeChannel::TransformScaleY = KoID("transform_scale_y", ki18n("Scale (Y)")); const KoID KisKeyframeChannel::TransformShearX = KoID("transform_shear_x", ki18n("Shear (X)")); const KoID KisKeyframeChannel::TransformShearY = KoID("transform_shear_y", ki18n("Shear (Y)")); const KoID KisKeyframeChannel::TransformRotationX = KoID("transform_rotation_x", ki18n("Rotation (X)")); const KoID KisKeyframeChannel::TransformRotationY = KoID("transform_rotation_y", ki18n("Rotation (Y)")); const KoID KisKeyframeChannel::TransformRotationZ = KoID("transform_rotation_z", ki18n("Rotation (Z)")); struct KisKeyframeChannel::Private { Private() {} Private(const Private &rhs, KisNodeWSP newParentNode) { node = newParentNode; id = rhs.id; defaultBounds = rhs.defaultBounds; haveBrokenFrameTimeBug = rhs.haveBrokenFrameTimeBug; } KeyframesMap keys; KisNodeWSP node; KoID id; KisDefaultBoundsBaseSP defaultBounds; bool haveBrokenFrameTimeBug = false; }; KisKeyframeChannel::KisKeyframeChannel(const KoID &id, KisDefaultBoundsBaseSP defaultBounds) : m_d(new Private) { m_d->id = id; m_d->node = 0; m_d->defaultBounds = defaultBounds; } KisKeyframeChannel::KisKeyframeChannel(const KisKeyframeChannel &rhs, KisNode *newParentNode) : m_d(new Private(*rhs.m_d, newParentNode)) { KIS_ASSERT_RECOVER_NOOP(&rhs != this); Q_FOREACH(KisKeyframeSP keyframe, rhs.m_d->keys) { m_d->keys.insert(keyframe->time(), keyframe->cloneFor(this)); } } KisKeyframeChannel::~KisKeyframeChannel() {} QString KisKeyframeChannel::id() const { return m_d->id.id(); } QString KisKeyframeChannel::name() const { return m_d->id.name(); } void KisKeyframeChannel::setNode(KisNodeWSP node) { m_d->node = node; } KisNodeWSP KisKeyframeChannel::node() const { return m_d->node; } int KisKeyframeChannel::keyframeCount() const { return m_d->keys.count(); } KisKeyframeChannel::KeyframesMap& KisKeyframeChannel::keys() { return m_d->keys; } const KisKeyframeChannel::KeyframesMap& KisKeyframeChannel::constKeys() const { return m_d->keys; } #define LAZY_INITIALIZE_PARENT_COMMAND(cmd) \ QScopedPointer __tempCommand; \ if (!parentCommand) { \ __tempCommand.reset(new KUndo2Command()); \ cmd = __tempCommand.data(); \ } KisKeyframeSP KisKeyframeChannel::addKeyframe(int time, KUndo2Command *parentCommand) { LAZY_INITIALIZE_PARENT_COMMAND(parentCommand); return insertKeyframe(time, KisKeyframeSP(), parentCommand); } KisKeyframeSP KisKeyframeChannel::copyKeyframe(const KisKeyframeSP keyframe, int newTime, KUndo2Command *parentCommand) { LAZY_INITIALIZE_PARENT_COMMAND(parentCommand); return insertKeyframe(newTime, keyframe, parentCommand); } KisKeyframeSP KisKeyframeChannel::linkKeyframe(const KisKeyframeSP, int, KUndo2Command*) { return KisKeyframeSP(); } KisKeyframeSP KisKeyframeChannel::insertKeyframe(int time, const KisKeyframeSP copySrc, KUndo2Command *parentCommand) { KisKeyframeSP keyframe = keyframeAt(time); if (keyframe) { deleteKeyframeImpl(keyframe, parentCommand, false); } Q_ASSERT(parentCommand); keyframe = createKeyframe(time, copySrc, parentCommand); KUndo2Command *cmd = new KisReplaceKeyframeCommand(this, keyframe->time(), keyframe, parentCommand); cmd->redo(); return keyframe; } bool KisKeyframeChannel::deleteKeyframe(KisKeyframeSP keyframe, KUndo2Command *parentCommand) { return deleteKeyframeImpl(keyframe, parentCommand, true); } bool KisKeyframeChannel::moveKeyframe(KisKeyframeSP keyframe, int newTime, KUndo2Command *parentCommand) { LAZY_INITIALIZE_PARENT_COMMAND(parentCommand); if (newTime == keyframe->time()) return false; KisKeyframeSP other = keyframeAt(newTime); if (other) { deleteKeyframeImpl(other, parentCommand, false); } const int srcTime = keyframe->time(); KUndo2Command *cmd = new KisMoveFrameCommand(this, keyframe, srcTime, newTime, parentCommand); cmd->redo(); if (srcTime == 0) { addKeyframe(srcTime, parentCommand); } return true; } bool KisKeyframeChannel::swapFrames(int lhsTime, int rhsTime, KUndo2Command *parentCommand) { LAZY_INITIALIZE_PARENT_COMMAND(parentCommand); if (lhsTime == rhsTime) return false; KisKeyframeSP lhsFrame = keyframeAt(lhsTime); KisKeyframeSP rhsFrame = keyframeAt(rhsTime); if (!lhsFrame && !rhsFrame) return false; if (lhsFrame && !rhsFrame) { moveKeyframe(lhsFrame, rhsTime, parentCommand); } else if (!lhsFrame && rhsFrame) { moveKeyframe(rhsFrame, lhsTime, parentCommand); } else { KUndo2Command *cmd = new KisSwapFramesCommand(this, lhsFrame, rhsFrame, parentCommand); cmd->redo(); } return true; } bool KisKeyframeChannel::deleteKeyframeImpl(KisKeyframeSP keyframe, KUndo2Command *parentCommand, bool recreate) { LAZY_INITIALIZE_PARENT_COMMAND(parentCommand); Q_ASSERT(parentCommand); KUndo2Command *cmd = new KisReplaceKeyframeCommand(this, keyframe->time(), KisKeyframeSP(), parentCommand); cmd->redo(); destroyKeyframe(keyframe, parentCommand); if (recreate && keyframe->time() == 0) { addKeyframe(0, parentCommand); } return true; } void KisKeyframeChannel::moveKeyframeImpl(KisKeyframeSP keyframe, int newTime) { KIS_ASSERT_RECOVER_RETURN(keyframe); KIS_ASSERT_RECOVER_RETURN(!keyframeAt(newTime)); - KisTimeRange rangeSrc = affectedFrames(keyframe->time()); + KisFrameSet rangeSrc = affectedFrames(keyframe->time()); QRect rectSrc = affectedRect(keyframe); emit sigKeyframeAboutToBeMoved(keyframe, newTime); m_d->keys.remove(keyframe->time()); int oldTime = keyframe->time(); keyframe->setTime(newTime); m_d->keys.insert(newTime, keyframe); emit sigKeyframeMoved(keyframe, oldTime); - KisTimeRange rangeDst = affectedFrames(keyframe->time()); + KisFrameSet rangeDst = affectedFrames(keyframe->time()); QRect rectDst = affectedRect(keyframe); requestUpdate(rangeSrc, rectSrc); requestUpdate(rangeDst, rectDst); } void KisKeyframeChannel::swapKeyframesImpl(KisKeyframeSP lhsKeyframe, KisKeyframeSP rhsKeyframe) { KIS_ASSERT_RECOVER_RETURN(lhsKeyframe); KIS_ASSERT_RECOVER_RETURN(rhsKeyframe); - KisTimeRange rangeLhs = affectedFrames(lhsKeyframe->time()); - KisTimeRange rangeRhs = affectedFrames(rhsKeyframe->time()); + KisFrameSet rangeLhs = affectedFrames(lhsKeyframe->time()); + KisFrameSet rangeRhs = affectedFrames(rhsKeyframe->time()); const QRect rectLhsSrc = affectedRect(lhsKeyframe); const QRect rectRhsSrc = affectedRect(rhsKeyframe); const int lhsTime = lhsKeyframe->time(); const int rhsTime = rhsKeyframe->time(); emit sigKeyframeAboutToBeMoved(lhsKeyframe, rhsTime); emit sigKeyframeAboutToBeMoved(rhsKeyframe, lhsTime); m_d->keys.remove(lhsTime); m_d->keys.remove(rhsTime); rhsKeyframe->setTime(lhsTime); lhsKeyframe->setTime(rhsTime); m_d->keys.insert(lhsTime, rhsKeyframe); m_d->keys.insert(rhsTime, lhsKeyframe); emit sigKeyframeMoved(lhsKeyframe, lhsTime); emit sigKeyframeMoved(rhsKeyframe, rhsTime); const QRect rectLhsDst = affectedRect(lhsKeyframe); const QRect rectRhsDst = affectedRect(rhsKeyframe); requestUpdate(rangeLhs, rectLhsSrc | rectRhsDst); requestUpdate(rangeRhs, rectRhsSrc | rectLhsDst); } KisKeyframeSP KisKeyframeChannel::replaceKeyframeAt(int time, KisKeyframeSP newKeyframe) { Q_ASSERT(newKeyframe.isNull() || time == newKeyframe->time()); KisKeyframeSP existingKeyframe = keyframeAt(time); if (!existingKeyframe.isNull()) { removeKeyframeLogical(existingKeyframe); } if (!newKeyframe.isNull()) { insertKeyframeLogical(newKeyframe); } return existingKeyframe; } void KisKeyframeChannel::insertKeyframeLogical(KisKeyframeSP keyframe) { const int time = keyframe->time(); emit sigKeyframeAboutToBeAdded(keyframe); m_d->keys.insert(time, keyframe); emit sigKeyframeAdded(keyframe); QRect rect = affectedRect(keyframe); - KisTimeRange range = affectedFrames(time); + KisFrameSet range = affectedFrames(time); requestUpdate(range, rect); } void KisKeyframeChannel::removeKeyframeLogical(KisKeyframeSP keyframe) { QRect rect = affectedRect(keyframe); - KisTimeRange range = affectedFrames(keyframe->time()); + KisFrameSet range = affectedFrames(keyframe->time()); emit sigKeyframeAboutToBeRemoved(keyframe); m_d->keys.remove(keyframe->time()); emit sigKeyframeRemoved(keyframe); requestUpdate(range, rect); } KisKeyframeSP KisKeyframeChannel::keyframeAt(int time) const { KeyframesMap::const_iterator i = m_d->keys.constFind(time); if (i != m_d->keys.constEnd()) { return i.value(); } return KisKeyframeSP(); } KisKeyframeSP KisKeyframeChannel::activeKeyframeAt(int time) const { KeyframesMap::const_iterator i = activeKeyIterator(time); if (i != m_d->keys.constEnd()) { return i.value(); } return KisKeyframeSP(); } KisKeyframeSP KisKeyframeChannel::currentlyActiveKeyframe() const { return activeKeyframeAt(currentTime()); } KisKeyframeSP KisKeyframeChannel::firstKeyframe() const { if (m_d->keys.isEmpty()) return KisKeyframeSP(); return m_d->keys.first(); } KisKeyframeSP KisKeyframeChannel::nextKeyframe(KisKeyframeSP keyframe) const { KeyframesMap::const_iterator i = m_d->keys.constFind(keyframe->time()); if (i == m_d->keys.constEnd()) return KisKeyframeSP(0); i++; if (i == m_d->keys.constEnd()) return KisKeyframeSP(0); return i.value(); } KisKeyframeSP KisKeyframeChannel::previousKeyframe(KisKeyframeSP keyframe) const { KeyframesMap::const_iterator i = m_d->keys.constFind(keyframe->time()); if (i == m_d->keys.constBegin() || i == m_d->keys.constEnd()) return KisKeyframeSP(0); i--; return i.value(); } KisKeyframeSP KisKeyframeChannel::lastKeyframe() const { if (m_d->keys.isEmpty()) return KisKeyframeSP(0); return (m_d->keys.end()-1).value(); } void KisKeyframeChannel::activeKeyframeRange(int time, int *first, int *last) const { *first = *last = -1; const KisKeyframeSP currentKeyframe = activeKeyframeAt(time); if (currentKeyframe.isNull()) return; *first = currentKeyframe->time(); const KisKeyframeSP next = nextKeyframe(currentKeyframe); if (!next.isNull()) { *last = next->time() - 1; } } int KisKeyframeChannel::framesHash() const { KeyframesMap::const_iterator it = m_d->keys.constBegin(); KeyframesMap::const_iterator end = m_d->keys.constEnd(); int hash = 0; while (it != end) { hash += it.key(); ++it; } return hash; } QSet KisKeyframeChannel::allKeyframeIds() const { QSet frames; KeyframesMap::const_iterator it = m_d->keys.constBegin(); KeyframesMap::const_iterator end = m_d->keys.constEnd(); while (it != end) { frames.insert(it.key()); ++it; } return frames; } -KisTimeRange KisKeyframeChannel::affectedFrames(int time) const +KisFrameSet KisKeyframeChannel::affectedFrames(int time) const { - if (m_d->keys.isEmpty()) return KisTimeRange::infinite(0); + if (m_d->keys.isEmpty()) return KisFrameSet::infiniteFrom(0); KeyframesMap::const_iterator active = activeKeyIterator(time); KeyframesMap::const_iterator next; int from; if (active == m_d->keys.constEnd()) { // No active keyframe, ie. time is before the first keyframe from = 0; next = m_d->keys.constBegin(); } else { from = active.key(); next = active + 1; } if (next == m_d->keys.constEnd()) { - return KisTimeRange::infinite(from); + return KisFrameSet::infiniteFrom(from); } else { - return KisTimeRange::fromTime(from, next.key() - 1); + return KisFrameSet::between(from, next.key() - 1); } } -KisTimeRange KisKeyframeChannel::identicalFrames(int time) const +KisFrameSet KisKeyframeChannel::identicalFrames(int time) const { KeyframesMap::const_iterator active = activeKeyIterator(time); if (active != m_d->keys.constEnd() && (active+1) != m_d->keys.constEnd()) { if (active->data()->interpolationMode() != KisKeyframe::Constant) { - return KisTimeRange::fromTime(time, time); + return KisFrameSet::between(time, time); } } return affectedFrames(time); } int KisKeyframeChannel::keyframeRowIndexOf(KisKeyframeSP keyframe) const { KeyframesMap::const_iterator it = m_d->keys.constBegin(); KeyframesMap::const_iterator end = m_d->keys.constEnd(); int row = 0; for (; it != end; ++it) { if (it.value().data() == keyframe) { return row; } row++; } return -1; } KisKeyframeSP KisKeyframeChannel::keyframeAtRow(int row) const { KeyframesMap::const_iterator it = m_d->keys.constBegin(); KeyframesMap::const_iterator end = m_d->keys.constEnd(); for (; it != end; ++it) { if (row <= 0) { return it.value(); } row--; } return KisKeyframeSP(); } int KisKeyframeChannel::keyframeInsertionRow(int time) const { KeyframesMap::const_iterator it = m_d->keys.constBegin(); KeyframesMap::const_iterator end = m_d->keys.constEnd(); int row = 0; for (; it != end; ++it) { if (it.value()->time() > time) { break; } row++; } return row; } QDomElement KisKeyframeChannel::toXML(QDomDocument doc, const QString &layerFilename) { QDomElement channelElement = doc.createElement("channel"); channelElement.setAttribute("name", id()); Q_FOREACH (KisKeyframeSP keyframe, m_d->keys.values()) { QDomElement keyframeElement = doc.createElement("keyframe"); keyframeElement.setAttribute("time", keyframe->time()); keyframeElement.setAttribute("color-label", keyframe->colorLabel()); saveKeyframe(keyframe, keyframeElement, layerFilename); channelElement.appendChild(keyframeElement); } return channelElement; } void KisKeyframeChannel::loadXML(const QDomElement &channelNode) { for (QDomElement keyframeNode = channelNode.firstChildElement(); !keyframeNode.isNull(); keyframeNode = keyframeNode.nextSiblingElement()) { if (keyframeNode.nodeName().toUpper() != "KEYFRAME") continue; KisKeyframeSP keyframe = loadKeyframe(keyframeNode); KIS_SAFE_ASSERT_RECOVER(keyframe) { continue; } if (keyframeNode.hasAttribute("color-label")) { keyframe->setColorLabel(keyframeNode.attribute("color-label").toUInt()); } m_d->keys.insert(keyframe->time(), keyframe); } } bool KisKeyframeChannel::swapExternalKeyframe(KisKeyframeChannel *srcChannel, int srcTime, int dstTime, KUndo2Command *parentCommand) { if (srcChannel->id() != id()) { warnKrita << "Cannot copy frames from different ids:" << ppVar(srcChannel->id()) << ppVar(id()); return KisKeyframeSP(); } LAZY_INITIALIZE_PARENT_COMMAND(parentCommand); KisKeyframeSP srcFrame = srcChannel->keyframeAt(srcTime); KisKeyframeSP dstFrame = keyframeAt(dstTime); if (!dstFrame && srcFrame) { copyExternalKeyframe(srcChannel, srcTime, dstTime, parentCommand); srcChannel->deleteKeyframe(srcFrame, parentCommand); } else if (dstFrame && !srcFrame) { srcChannel->copyExternalKeyframe(this, dstTime, srcTime, parentCommand); deleteKeyframe(dstFrame, parentCommand); } else if (dstFrame && srcFrame) { const int fakeFrameTime = -1; KisKeyframeSP newKeyframe = createKeyframe(fakeFrameTime, KisKeyframeSP(), parentCommand); uploadExternalKeyframe(srcChannel, srcTime, newKeyframe); srcChannel->copyExternalKeyframe(this, dstTime, srcTime, parentCommand); // do not recreate frame! deleteKeyframeImpl(dstFrame, parentCommand, false); newKeyframe->setTime(dstTime); KUndo2Command *cmd = new KisReplaceKeyframeCommand(this, newKeyframe->time(), newKeyframe, parentCommand); cmd->redo(); } return true; } KisKeyframeSP KisKeyframeChannel::copyExternalKeyframe(KisKeyframeChannel *srcChannel, int srcTime, int dstTime, KUndo2Command *parentCommand) { if (srcChannel->id() != id()) { warnKrita << "Cannot copy frames from different ids:" << ppVar(srcChannel->id()) << ppVar(id()); return KisKeyframeSP(); } LAZY_INITIALIZE_PARENT_COMMAND(parentCommand); KisKeyframeSP dstFrame = keyframeAt(dstTime); if (dstFrame) { deleteKeyframeImpl(dstFrame, parentCommand, false); } KisKeyframeSP newKeyframe = createKeyframe(dstTime, KisKeyframeSP(), parentCommand); uploadExternalKeyframe(srcChannel, srcTime, newKeyframe); KUndo2Command *cmd = new KisReplaceKeyframeCommand(this, newKeyframe->time(), newKeyframe, parentCommand); cmd->redo(); return newKeyframe; } KisKeyframeChannel::KeyframesMap::const_iterator KisKeyframeChannel::activeKeyIterator(int time) const { KeyframesMap::const_iterator i = const_cast(&m_d->keys)->upperBound(time); if (i == m_d->keys.constBegin()) return m_d->keys.constEnd(); return --i; } -void KisKeyframeChannel::requestUpdate(const KisTimeRange &range, const QRect &rect) +void KisKeyframeChannel::requestUpdate(const KisFrameSet &range, const QRect &rect) { if (m_d->node) { m_d->node->invalidateFrames(range, rect); int currentTime = m_d->defaultBounds->currentTime(); if (range.contains(currentTime)) { m_d->node->setDirty(rect); } } } void KisKeyframeChannel::workaroundBrokenFrameTimeBug(int *time) { /** * Between Krita 4.1 and 4.4 Krita had a bug which resulted in creating frames * with negative time stamp. The bug has been fixed, but there might be some files * still in the wild. * * TODO: remove this workaround in Krita 5.0, when no such file are left :) */ if (*time < 0) { qWarning() << "WARNING: Loading a file with negative animation frames!"; qWarning() << " The file has been saved with a buggy version of Krita."; qWarning() << " All the frames with negative ids will be dropped!"; qWarning() << " " << ppVar(this->id()) << ppVar(*time); m_d->haveBrokenFrameTimeBug = true; *time = 0; } if (m_d->haveBrokenFrameTimeBug) { while (keyframeAt(*time)) { (*time)++; } } } int KisKeyframeChannel::currentTime() const { return m_d->defaultBounds->currentTime(); } qreal KisKeyframeChannel::minScalarValue() const { return 0; } qreal KisKeyframeChannel::maxScalarValue() const { return 0; } qreal KisKeyframeChannel::scalarValue(const KisKeyframeSP keyframe) const { Q_UNUSED(keyframe); return 0; } void KisKeyframeChannel::setScalarValue(KisKeyframeSP keyframe, qreal value, KUndo2Command *parentCommand) { Q_UNUSED(keyframe); Q_UNUSED(value); Q_UNUSED(parentCommand); } diff --git a/libs/image/kis_keyframe_channel.h b/libs/image/kis_keyframe_channel.h index fc78f89a22..c6ee70cd18 100644 --- a/libs/image/kis_keyframe_channel.h +++ b/libs/image/kis_keyframe_channel.h @@ -1,179 +1,179 @@ /* * Copyright (c) 2015 Jouni Pentikäinen * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KIS_KEYFRAME_CHANNEL_H #define KIS_KEYFRAME_CHANNEL_H #include #include #include #include "kis_types.h" #include "KoID.h" #include "kritaimage_export.h" #include "kis_keyframe.h" #include "kis_default_bounds.h" -class KisTimeRange; +class KisFrameSet; class KRITAIMAGE_EXPORT KisKeyframeChannel : public QObject { Q_OBJECT public: // Standard Keyframe Ids static const KoID Content; static const KoID Opacity; static const KoID TransformArguments; static const KoID TransformPositionX; static const KoID TransformPositionY; static const KoID TransformScaleX; static const KoID TransformScaleY; static const KoID TransformShearX; static const KoID TransformShearY; static const KoID TransformRotationX; static const KoID TransformRotationY; static const KoID TransformRotationZ; public: KisKeyframeChannel(const KoID& id, KisDefaultBoundsBaseSP defaultBounds); KisKeyframeChannel(const KisKeyframeChannel &rhs, KisNode *newParentNode); ~KisKeyframeChannel() override; QString id() const; QString name() const; void setNode(KisNodeWSP node); KisNodeWSP node() const; KisKeyframeSP addKeyframe(int time, KUndo2Command *parentCommand = 0); bool deleteKeyframe(KisKeyframeSP keyframe, KUndo2Command *parentCommand = 0); bool moveKeyframe(KisKeyframeSP keyframe, int newTime, KUndo2Command *parentCommand = 0); bool swapFrames(int lhsTime, int rhsTime, KUndo2Command *parentCommand = 0); KisKeyframeSP copyKeyframe(const KisKeyframeSP keyframe, int newTime, KUndo2Command *parentCommand = 0); virtual KisKeyframeSP linkKeyframe(const KisKeyframeSP keyframe, int newTime, KUndo2Command *parentCommand = 0); KisKeyframeSP copyExternalKeyframe(KisKeyframeChannel *srcChannel, int srcTime, int dstTime, KUndo2Command *parentCommand = 0); bool swapExternalKeyframe(KisKeyframeChannel *srcChannel, int srcTime, int dstTime, KUndo2Command *parentCommand = 0); KisKeyframeSP keyframeAt(int time) const; KisKeyframeSP activeKeyframeAt(int time) const; KisKeyframeSP currentlyActiveKeyframe() const; KisKeyframeSP firstKeyframe() const; KisKeyframeSP nextKeyframe(KisKeyframeSP keyframe) const; KisKeyframeSP previousKeyframe(KisKeyframeSP keyframe) const; KisKeyframeSP lastKeyframe() const; /** * Finds the span of time of the keyframe active at given time. * If there is no active keyframe, first will be -1. * If the keyframe continues indefinitely, last will be -1. */ void activeKeyframeRange(int time, int *first, int *last) const; /** * Calculates a pseudo-unique keyframes hash. The hash changes * every time any frame is added/removed/moved */ int framesHash() const; QSet allKeyframeIds() const; /** * Get the set of frames affected by any changes to the value * of the active keyframe at the given time. */ - KisTimeRange affectedFrames(int time) const; + virtual KisFrameSet affectedFrames(int time) const; /** * Get a set of frames for which the channel gives identical * results, compared to the given frame. * * Note: this set may be different than the set of affected frames * due to interpolation. */ - KisTimeRange identicalFrames(int time) const; + virtual KisFrameSet identicalFrames(int time) const; int keyframeCount() const; int keyframeRowIndexOf(KisKeyframeSP keyframe) const; KisKeyframeSP keyframeAtRow(int row) const; int keyframeInsertionRow(int time) const; virtual bool hasScalarValue() const = 0; virtual qreal minScalarValue() const; virtual qreal maxScalarValue() const; virtual qreal scalarValue(const KisKeyframeSP keyframe) const; virtual void setScalarValue(KisKeyframeSP keyframe, qreal value, KUndo2Command *parentCommand = 0); virtual QDomElement toXML(QDomDocument doc, const QString &layerFilename); virtual void loadXML(const QDomElement &channelNode); int currentTime() const; Q_SIGNALS: void sigKeyframeAboutToBeAdded(KisKeyframeSP keyframe); void sigKeyframeAdded(KisKeyframeSP keyframe); void sigKeyframeAboutToBeRemoved(KisKeyframeSP keyframe); void sigKeyframeRemoved(KisKeyframeSP keyframe); void sigKeyframeAboutToBeMoved(KisKeyframeSP keyframe, int toTime); void sigKeyframeMoved(KisKeyframeSP keyframe, int fromTime); void sigKeyframeChanged(KisKeyframeSP keyframe); protected: typedef QMap KeyframesMap; KeyframesMap &keys(); const KeyframesMap &constKeys() const; KeyframesMap::const_iterator activeKeyIterator(int time) const; virtual KisKeyframeSP createKeyframe(int time, const KisKeyframeSP copySrc, KUndo2Command *parentCommand) = 0; virtual void destroyKeyframe(KisKeyframeSP key, KUndo2Command *parentCommand) = 0; virtual void uploadExternalKeyframe(KisKeyframeChannel *srcChannel, int srcTime, KisKeyframeSP dstFrame) = 0; virtual QRect affectedRect(KisKeyframeSP key) = 0; - virtual void requestUpdate(const KisTimeRange &range, const QRect &rect); + virtual void requestUpdate(const KisFrameSet &range, const QRect &rect); virtual KisKeyframeSP loadKeyframe(const QDomElement &keyframeNode) = 0; virtual void saveKeyframe(KisKeyframeSP keyframe, QDomElement keyframeElement, const QString &layerFilename) = 0; void workaroundBrokenFrameTimeBug(int *time); private: KisKeyframeSP replaceKeyframeAt(int time, KisKeyframeSP newKeyframe); void insertKeyframeLogical(KisKeyframeSP keyframe); void removeKeyframeLogical(KisKeyframeSP keyframe); bool deleteKeyframeImpl(KisKeyframeSP keyframe, KUndo2Command *parentCommand, bool recreate); void moveKeyframeImpl(KisKeyframeSP keyframe, int newTime); void swapKeyframesImpl(KisKeyframeSP lhsKeyframe, KisKeyframeSP rhsKeyframe); friend class KisMoveFrameCommand; friend class KisReplaceKeyframeCommand; friend class KisSwapFramesCommand; private: KisKeyframeSP insertKeyframe(int time, const KisKeyframeSP copySrc, KUndo2Command *parentCommand); struct Private; QScopedPointer m_d; }; #endif // KIS_KEYFRAME_CHANNEL_H diff --git a/libs/image/kis_node.cpp b/libs/image/kis_node.cpp index dc283205c4..1136b8a74b 100644 --- a/libs/image/kis_node.cpp +++ b/libs/image/kis_node.cpp @@ -1,656 +1,656 @@ /* * Copyright (c) 2007 Boudewijn Rempt * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_node.h" #include #include #include #include #include #include #include #include #include "kis_global.h" #include "kis_node_graph_listener.h" #include "kis_node_visitor.h" #include "kis_processing_visitor.h" #include "kis_node_progress_proxy.h" #include "kis_busy_progress_indicator.h" #include "kis_clone_layer.h" #include "kis_safe_read_list.h" typedef KisSafeReadList KisSafeReadNodeList; #include "kis_abstract_projection_plane.h" #include "kis_projection_leaf.h" #include "kis_undo_adapter.h" #include "kis_keyframe_channel.h" /** *The link between KisProjection and KisImageUpdater *uses queued signals with an argument of KisNodeSP type, *so we should register it beforehand */ struct KisNodeSPStaticRegistrar { KisNodeSPStaticRegistrar() { qRegisterMetaType("KisNodeSP"); } }; static KisNodeSPStaticRegistrar __registrar1; struct KisNodeListStaticRegistrar { KisNodeListStaticRegistrar() { qRegisterMetaType("KisNodeList"); } }; static KisNodeListStaticRegistrar __registrar2; /** * Note about "thread safety" of KisNode * * 1) One can *read* any information about node and node graph in any * number of threads concurrently. This operation is safe because * of the usage of KisSafeReadNodeList and will run concurrently * (lock-free). * * 2) One can *write* any information into the node or node graph in a * single thread only! Changing the graph concurrently is *not* * sane and therefore not supported. * * 3) One can *read and write* information about the node graph * concurrently, given that there is only *one* writer thread and * any number of reader threads. Please note that in this case the * node's code is just guaranteed *not to crash*, which is ensured * by nodeSubgraphLock. You need to ensure the sanity of the data * read by the reader threads yourself! */ struct Q_DECL_HIDDEN KisNode::Private { public: Private(KisNode *node) : graphListener(0) , nodeProgressProxy(0) , busyProgressIndicator(0) , projectionLeaf(new KisProjectionLeaf(node)) { } KisNodeWSP parent; KisNodeGraphListener *graphListener; KisSafeReadNodeList nodes; KisNodeProgressProxy *nodeProgressProxy; KisBusyProgressIndicator *busyProgressIndicator; QReadWriteLock nodeSubgraphLock; KisProjectionLeafSP projectionLeaf; const KisNode* findSymmetricClone(const KisNode *srcRoot, const KisNode *dstRoot, const KisNode *srcTarget); void processDuplicatedClones(const KisNode *srcDuplicationRoot, const KisNode *dstDuplicationRoot, KisNode *node); }; /** * Finds the layer in \p dstRoot subtree, which has the same path as * \p srcTarget has in \p srcRoot */ const KisNode* KisNode::Private::findSymmetricClone(const KisNode *srcRoot, const KisNode *dstRoot, const KisNode *srcTarget) { if (srcRoot == srcTarget) return dstRoot; KisSafeReadNodeList::const_iterator srcIter = srcRoot->m_d->nodes.constBegin(); KisSafeReadNodeList::const_iterator dstIter = dstRoot->m_d->nodes.constBegin(); for (; srcIter != srcRoot->m_d->nodes.constEnd(); srcIter++, dstIter++) { KIS_ASSERT_RECOVER_RETURN_VALUE((srcIter != srcRoot->m_d->nodes.constEnd()) == (dstIter != dstRoot->m_d->nodes.constEnd()), 0); const KisNode *node = findSymmetricClone(srcIter->data(), dstIter->data(), srcTarget); if (node) return node; } return 0; } /** * This function walks through a subtrees of old and new layers and * searches for clone layers. For each clone layer it checks whether * its copyFrom() lays inside the old subtree, and if it is so resets * it to the corresponding layer in the new subtree. * * That is needed when the user duplicates a group layer with all its * layer subtree. In such a case all the "internal" clones must stay * "internal" and not point to the layers of the older group. */ void KisNode::Private::processDuplicatedClones(const KisNode *srcDuplicationRoot, const KisNode *dstDuplicationRoot, KisNode *node) { if (KisCloneLayer *clone = dynamic_cast(node)) { KIS_ASSERT_RECOVER_RETURN(clone->copyFrom()); const KisNode *newCopyFrom = findSymmetricClone(srcDuplicationRoot, dstDuplicationRoot, clone->copyFrom()); if (newCopyFrom) { KisLayer *newCopyFromLayer = qobject_cast(const_cast(newCopyFrom)); KIS_ASSERT_RECOVER_RETURN(newCopyFromLayer); clone->setCopyFrom(newCopyFromLayer); } } KisSafeReadNodeList::const_iterator iter; FOREACH_SAFE(iter, node->m_d->nodes) { KisNode *child = const_cast((*iter).data()); processDuplicatedClones(srcDuplicationRoot, dstDuplicationRoot, child); } } KisNode::KisNode() : m_d(new Private(this)) { m_d->parent = 0; m_d->graphListener = 0; moveToThread(qApp->thread()); } KisNode::KisNode(const KisNode & rhs) : KisBaseNode(rhs) , m_d(new Private(this)) { m_d->parent = 0; m_d->graphListener = 0; moveToThread(qApp->thread()); // HACK ALERT: we create opacity channel in KisBaseNode, but we cannot // initialize its node from there! So workaround it here! QMap channels = keyframeChannels(); for (auto it = channels.begin(); it != channels.end(); ++it) { it.value()->setNode(this); } // NOTE: the nodes are not supposed to be added/removed while // creation of another node, so we do *no* locking here! KisSafeReadNodeList::const_iterator iter; FOREACH_SAFE(iter, rhs.m_d->nodes) { KisNodeSP child = (*iter)->clone(); child->createNodeProgressProxy(); m_d->nodes.append(child); child->setParent(this); } m_d->processDuplicatedClones(&rhs, this, this); } KisNode::~KisNode() { if (m_d->busyProgressIndicator) { m_d->busyProgressIndicator->prepareDestroying(); m_d->busyProgressIndicator->deleteLater(); } if (m_d->nodeProgressProxy) { m_d->nodeProgressProxy->prepareDestroying(); m_d->nodeProgressProxy->deleteLater(); } { QWriteLocker l(&m_d->nodeSubgraphLock); m_d->nodes.clear(); } delete m_d; } QRect KisNode::needRect(const QRect &rect, PositionToFilthy pos) const { Q_UNUSED(pos); return rect; } QRect KisNode::changeRect(const QRect &rect, PositionToFilthy pos) const { Q_UNUSED(pos); return rect; } QRect KisNode::accessRect(const QRect &rect, PositionToFilthy pos) const { Q_UNUSED(pos); return rect; } void KisNode::childNodeChanged(KisNodeSP /*changedChildNode*/) { } KisAbstractProjectionPlaneSP KisNode::projectionPlane() const { KIS_ASSERT_RECOVER_NOOP(0 && "KisNode::projectionPlane() is not defined!"); static KisAbstractProjectionPlaneSP plane = toQShared(new KisDumbProjectionPlane()); return plane; } KisProjectionLeafSP KisNode::projectionLeaf() const { return m_d->projectionLeaf; } bool KisNode::accept(KisNodeVisitor &v) { return v.visit(this); } void KisNode::accept(KisProcessingVisitor &visitor, KisUndoAdapter *undoAdapter) { visitor.visit(this, undoAdapter); } int KisNode::graphSequenceNumber() const { return m_d->graphListener ? m_d->graphListener->graphSequenceNumber() : -1; } KisNodeGraphListener *KisNode::graphListener() const { return m_d->graphListener; } void KisNode::setGraphListener(KisNodeGraphListener *graphListener) { m_d->graphListener = graphListener; QReadLocker l(&m_d->nodeSubgraphLock); KisSafeReadNodeList::const_iterator iter; FOREACH_SAFE(iter, m_d->nodes) { KisNodeSP child = (*iter); child->setGraphListener(graphListener); } } void KisNode::setParent(KisNodeWSP parent) { QWriteLocker l(&m_d->nodeSubgraphLock); m_d->parent = parent; } KisNodeSP KisNode::parent() const { QReadLocker l(&m_d->nodeSubgraphLock); return m_d->parent.isValid() ? KisNodeSP(m_d->parent) : KisNodeSP(); } KisBaseNodeSP KisNode::parentCallback() const { return parent(); } void KisNode::notifyParentVisibilityChanged(bool value) { QReadLocker l(&m_d->nodeSubgraphLock); KisSafeReadNodeList::const_iterator iter; FOREACH_SAFE(iter, m_d->nodes) { KisNodeSP child = (*iter); child->notifyParentVisibilityChanged(value); } } void KisNode::baseNodeChangedCallback() { if(m_d->graphListener) { m_d->graphListener->nodeChanged(this); } } void KisNode::baseNodeInvalidateAllFramesCallback() { if(m_d->graphListener) { m_d->graphListener->invalidateAllFrames(); } } void KisNode::addKeyframeChannel(KisKeyframeChannel *channel) { channel->setNode(this); KisBaseNode::addKeyframeChannel(channel); } KisNodeSP KisNode::firstChild() const { QReadLocker l(&m_d->nodeSubgraphLock); return !m_d->nodes.isEmpty() ? m_d->nodes.first() : 0; } KisNodeSP KisNode::lastChild() const { QReadLocker l(&m_d->nodeSubgraphLock); return !m_d->nodes.isEmpty() ? m_d->nodes.last() : 0; } KisNodeSP KisNode::prevChildImpl(KisNodeSP child) { /** * Warning: mind locking policy! * * The graph locks must be *always* taken in descending * order. That is if you want to (or it implicitly happens that * you) take a lock of a parent and a chil, you must first take * the lock of a parent, and only after that ask a child to do the * same. Otherwise you'll get a deadlock. */ QReadLocker l(&m_d->nodeSubgraphLock); int i = m_d->nodes.indexOf(child) - 1; return i >= 0 ? m_d->nodes.at(i) : 0; } KisNodeSP KisNode::nextChildImpl(KisNodeSP child) { /** * See a comment in KisNode::prevChildImpl() */ QReadLocker l(&m_d->nodeSubgraphLock); int i = m_d->nodes.indexOf(child) + 1; return i > 0 && i < m_d->nodes.size() ? m_d->nodes.at(i) : 0; } KisNodeSP KisNode::prevSibling() const { KisNodeSP parentNode = parent(); return parentNode ? parentNode->prevChildImpl(const_cast(this)) : 0; } KisNodeSP KisNode::nextSibling() const { KisNodeSP parentNode = parent(); return parentNode ? parentNode->nextChildImpl(const_cast(this)) : 0; } quint32 KisNode::childCount() const { QReadLocker l(&m_d->nodeSubgraphLock); return m_d->nodes.size(); } KisNodeSP KisNode::at(quint32 index) const { QReadLocker l(&m_d->nodeSubgraphLock); if (!m_d->nodes.isEmpty() && index < (quint32)m_d->nodes.size()) { return m_d->nodes.at(index); } return 0; } int KisNode::index(const KisNodeSP node) const { QReadLocker l(&m_d->nodeSubgraphLock); return m_d->nodes.indexOf(node); } QList KisNode::childNodes(const QStringList & nodeTypes, const KoProperties & properties) const { QReadLocker l(&m_d->nodeSubgraphLock); QList nodes; KisSafeReadNodeList::const_iterator iter; FOREACH_SAFE(iter, m_d->nodes) { if (*iter) { if (properties.isEmpty() || (*iter)->check(properties)) { bool rightType = true; if(!nodeTypes.isEmpty()) { rightType = false; Q_FOREACH (const QString &nodeType, nodeTypes) { if ((*iter)->inherits(nodeType.toLatin1())) { rightType = true; break; } } } if (rightType) { nodes.append(*iter); } } } } return nodes; } KisNodeSP KisNode::findChildByName(const QString &name) { KisNodeSP child = firstChild(); while (child) { if (child->name() == name) { return child; } if (child->childCount() > 0) { KisNodeSP grandChild = child->findChildByName(name); if (grandChild) { return grandChild; } } child = child->nextSibling(); } return 0; } bool KisNode::add(KisNodeSP newNode, KisNodeSP aboveThis) { KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(newNode, false); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!aboveThis || aboveThis->parent().data() == this, false); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(allowAsChild(newNode), false); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!newNode->parent(), false); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(index(newNode) < 0, false); int idx = aboveThis ? this->index(aboveThis) + 1 : 0; // threoretical race condition may happen here ('idx' may become // deprecated until the write lock will be held). But we ignore // it, because it is not supported to add/remove nodes from two // concurrent threads simultaneously if (m_d->graphListener) { m_d->graphListener->aboutToAddANode(this, idx); } { QWriteLocker l(&m_d->nodeSubgraphLock); newNode->createNodeProgressProxy(); m_d->nodes.insert(idx, newNode); newNode->setParent(this); newNode->setGraphListener(m_d->graphListener); } if (m_d->graphListener) { m_d->graphListener->nodeHasBeenAdded(this, idx); } childNodeChanged(newNode); return true; } bool KisNode::remove(quint32 index) { if (index < childCount()) { KisNodeSP removedNode = at(index); if (m_d->graphListener) { m_d->graphListener->aboutToRemoveANode(this, index); } { QWriteLocker l(&m_d->nodeSubgraphLock); removedNode->setGraphListener(0); removedNode->setParent(0); // after calling aboutToRemoveANode or then the model get broken according to TT's modeltest m_d->nodes.removeAt(index); } if (m_d->graphListener) { m_d->graphListener->nodeHasBeenRemoved(this, index); } childNodeChanged(removedNode); return true; } return false; } bool KisNode::remove(KisNodeSP node) { return node->parent().data() == this ? remove(index(node)) : false; } KisNodeProgressProxy* KisNode::nodeProgressProxy() const { if (m_d->nodeProgressProxy) { return m_d->nodeProgressProxy; } else if (parent()) { return parent()->nodeProgressProxy(); } return 0; } KisBusyProgressIndicator* KisNode::busyProgressIndicator() const { if (m_d->busyProgressIndicator) { return m_d->busyProgressIndicator; } else if (parent()) { return parent()->busyProgressIndicator(); } return 0; } void KisNode::createNodeProgressProxy() { if (!m_d->nodeProgressProxy) { m_d->nodeProgressProxy = new KisNodeProgressProxy(this); m_d->busyProgressIndicator = new KisBusyProgressIndicator(m_d->nodeProgressProxy); m_d->nodeProgressProxy->moveToThread(this->thread()); m_d->busyProgressIndicator->moveToThread(this->thread()); } } void KisNode::setDirty() { setDirty(extent()); } void KisNode::setDirty(const QVector &rects) { if(m_d->graphListener) { m_d->graphListener->requestProjectionUpdate(this, rects, true); } } void KisNode::setDirty(const QRegion ®ion) { setDirty(region.rects()); } void KisNode::setDirty(const QRect & rect) { setDirty(QVector({rect})); } void KisNode::setDirtyDontResetAnimationCache() { setDirtyDontResetAnimationCache(QVector({extent()})); } void KisNode::setDirtyDontResetAnimationCache(const QRect &rect) { setDirtyDontResetAnimationCache(QVector({rect})); } void KisNode::setDirtyDontResetAnimationCache(const QVector &rects) { if(m_d->graphListener) { m_d->graphListener->requestProjectionUpdate(this, rects, false); } } -void KisNode::invalidateFrames(const KisTimeRange &range, const QRect &rect) +void KisNode::invalidateFrames(const KisFrameSet &range, const QRect &rect) { if(m_d->graphListener) { m_d->graphListener->invalidateFrames(range, rect); } } void KisNode::requestTimeSwitch(int time) { if(m_d->graphListener) { m_d->graphListener->requestTimeSwitch(time); } } void KisNode::syncLodCache() { // noop. everything is done by getLodCapableDevices() } KisPaintDeviceList KisNode::getLodCapableDevices() const { KisPaintDeviceList list; KisPaintDeviceSP device = paintDevice(); if (device) { list << device; } KisPaintDeviceSP originalDevice = original(); if (originalDevice && originalDevice != device) { list << originalDevice; } list << projectionPlane()->getLodCapableDevices(); return list; } diff --git a/libs/image/kis_node.h b/libs/image/kis_node.h index 297af29cfd..4e215c841b 100644 --- a/libs/image/kis_node.h +++ b/libs/image/kis_node.h @@ -1,424 +1,424 @@ /* * Copyright (c) 2007 Boudewijn Rempt * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef _KIS_NODE_H #define _KIS_NODE_H #include "kis_types.h" #include "kis_base_node.h" #include "kritaimage_export.h" #include class QRect; class QStringList; class KoProperties; class KisNodeVisitor; class KisNodeGraphListener; class KisNodeProgressProxy; class KisBusyProgressIndicator; class KisAbstractProjectionPlane; class KisProjectionLeaf; class KisKeyframeChannel; -class KisTimeRange; +class KisFrameSet; class KisUndoAdapter; /** * A KisNode is a KisBaseNode that knows about its direct peers, parent * and children and whether it can have children. * * THREAD-SAFETY: All const methods of this class and setDirty calls * are considered to be thread-safe(!). All the others * especially add(), remove() and setParent() must be * protected externally. * * NOTE: your subclasses must have the Q_OBJECT declaration, even if * you do not define new signals or slots. */ class KRITAIMAGE_EXPORT KisNode : public KisBaseNode { friend class KisFilterMaskTest; Q_OBJECT public: /** * The struct describing the position of the node * against the filthy node. * NOTE: please change KisBaseRectsWalker::getPositionToFilthy * when changing this struct */ enum PositionToFilthy { N_ABOVE_FILTHY = 0x08, N_FILTHY_PROJECTION = 0x20, N_FILTHY = 0x40, N_BELOW_FILTHY = 0x80 }; /** * Create an empty node without a parent. */ KisNode(); /** * Create a copy of this node. The copy will not have a parent * node. */ KisNode(const KisNode & rhs); /** * Delete this node */ ~KisNode() override; virtual KisNodeSP clone() const = 0; bool accept(KisNodeVisitor &v) override; void accept(KisProcessingVisitor &visitor, KisUndoAdapter *undoAdapter) override; /** * Re-implement this method to add constraints for the * subclasses that can be added as children to this node * * @return false if the given node is not allowed as a child to this node */ virtual bool allowAsChild(KisNodeSP) const = 0; /** * Set the entire node extent dirty; this percolates up to parent * nodes all the way to the root node. By default this is the * empty rect (through KisBaseNode::extent()) */ virtual void setDirty(); /** * Add the given rect to the set of dirty rects for this node; * this percolates up to parent nodes all the way to the root * node. */ void setDirty(const QRect & rect); /** * Add the given rects to the set of dirty rects for this node; * this percolates up to parent nodes all the way to the root * node. */ virtual void setDirty(const QVector &rects); /** * Add the given region to the set of dirty rects for this node; * this percolates up to parent nodes all the way to the root * node, if propagate is true; */ void setDirty(const QRegion ®ion); /** * Convenience override of multirect version of setDirtyDontResetAnimationCache() * * @see setDirtyDontResetAnimationCache(const QVector &rects) */ void setDirtyDontResetAnimationCache(); /** * Convenience override of multirect version of setDirtyDontResetAnimationCache() * * @see setDirtyDontResetAnimationCache(const QVector &rects) */ void setDirtyDontResetAnimationCache(const QRect &rect); /** * @brief setDirtyDontResetAnimationCache does almost the same thing as usual * setDirty() call, but doesn't reset the animation cache (since onlion skins are * not used when rendering animation. */ void setDirtyDontResetAnimationCache(const QVector &rects); /** * Informs that the frames in the given range are no longer valid * and need to be recached. * @param range frames to invalidate */ - void invalidateFrames(const KisTimeRange &range, const QRect &rect); + void invalidateFrames(const KisFrameSet &range, const QRect &rect); /** * Informs that the current world time should be changed. * Might be caused by e.g. undo operation */ void requestTimeSwitch(int time); /** * \return a pointer to a KisAbstractProjectionPlane interface of * the node. This interface is used by the image merging * framework to get information and to blending for the * layer. * * Please note the difference between need/change/accessRect and * the projectionPlane() interface. The former one gives * information about internal composition of the layer, and the * latter one about the total composition, including layer styles, * pass-through blending and etc. */ virtual KisAbstractProjectionPlaneSP projectionPlane() const; /** * Synchronizes LoD caches of the node with the current state of it. * The current level of detail is fetched from the image pointed by * default bounds object */ virtual void syncLodCache(); virtual KisPaintDeviceList getLodCapableDevices() const; /** * The rendering of the image may not always happen in the order * of the main graph. Pass-through nodes ake some subgraphs * linear, so it the order of rendering change. projectionLeaf() * is a special interface of KisNode that represents "a graph for * projection rendering". Therefore the nodes in projectionLeaf() * graph may have a different order the main one. */ virtual KisProjectionLeafSP projectionLeaf() const; protected: /** * \return internal changeRect() of the node. Do not mix with \see * projectionPlane() * * Some filters will cause a change of pixels those are outside * a requested rect. E.g. we change a rect of 2x2, then we want to * apply a convolution filter with kernel 4x4 (changeRect is * (2+2*3)x(2+2*3)=8x8) to that area. The rect that should be updated * on the layer will be exaclty 8x8. More than that the needRect for * that update will be 14x14. See \ref needeRect. */ virtual QRect changeRect(const QRect &rect, PositionToFilthy pos = N_FILTHY) const; /** * \return internal needRect() of the node. Do not mix with \see * projectionPlane() * * Some filters need pixels outside the current processing rect to * compute the new value (for instance, convolution filters) * See \ref changeRect * See \ref accessRect */ virtual QRect needRect(const QRect &rect, PositionToFilthy pos = N_FILTHY) const; /** * \return internal accessRect() of the node. Do not mix with \see * projectionPlane() * * Shows the area of image, that may be accessed during accessing * the node. * * Example. You have a layer that needs to prepare some rect on a * projection, say expectedRect. To perform this, the projection * of all the layers below of the size needRect(expectedRect) * should be calculeated by the merger beforehand and the layer * will access some other area of image inside the rect * accessRect(expectedRect) during updateProjection call. * * This knowledge about real access rect of a node is used by the * scheduler to avoid collisions between two multithreaded updaters * and so avoid flickering of the image. * * Currently, this method has nondefault value for shifted clone * layers only. */ virtual QRect accessRect(const QRect &rect, PositionToFilthy pos = N_FILTHY) const; /** * Called each time direct child nodes are added or removed under this * node as parent. This does not track changes inside the child nodes * or the child nodes' properties. */ virtual void childNodeChanged(KisNodeSP changedChildNode); public: // Graph methods /** * @return the graph sequence number calculated by the associated * graph listener. You can use it for checking for changes in the * graph. */ int graphSequenceNumber() const; /** * @return the graph listener this node belongs to. 0 if the node * does not belong to a grap listener. */ KisNodeGraphListener * graphListener() const; /** * Set the graph listener for this node. The graphlistener will be * informed before and after the list of child nodes has changed. */ void setGraphListener(KisNodeGraphListener * graphListener); /** * Returns the parent node of this node. This is 0 only for a root * node; otherwise this will be an actual Node */ KisNodeSP parent() const; /** * Returns the first child node of this node, or 0 if there are no * child nodes. */ KisNodeSP firstChild() const; /** * Returns the last child node of this node, or 0 if there are no * child nodes. */ KisNodeSP lastChild() const; /** * Returns the previous sibling of this node in the parent's list. * This is the node *above* this node in the composition stack. 0 * is returned if this child has no more previous siblings (== * firstChild()) */ KisNodeSP prevSibling() const; /** * Returns the next sibling of this node in the parent's list. * This is the node *below* this node in the composition stack. 0 * is returned if this child has no more next siblings (== * lastChild()) */ KisNodeSP nextSibling() const; /** * Returns how many direct child nodes this node has (not * recursive). */ quint32 childCount() const; /** * Retrieve the child node at the specified index. * * @return 0 if there is no node at this index. */ KisNodeSP at(quint32 index) const; /** * Retrieve the index of the specified child node. * * @return -1 if the specified node is not a child node of this * node. */ int index(const KisNodeSP node) const; /** * Return a list of child nodes of the current node that conform * to the specified constraints. There are no guarantees about the * order of the nodes in the list. The function is not recursive. * * @param nodeTypes. if not empty, only nodes that inherit the * classnames in this stringlist will be returned. * @param properties. if not empty, only nodes for which * KisNodeBase::check(properties) returns true will be returned. */ QList childNodes(const QStringList & nodeTypes, const KoProperties & properties) const; /** * @brief findChildByName finds the first child that has the given name * @param name the name to look for * @return the first child with the given name */ KisNodeSP findChildByName(const QString &name); public: /** * @return the node progress proxy used by this node, if this node has no progress * proxy, it will return the proxy of its parent, if the parent has no progress proxy * it will return 0 */ KisNodeProgressProxy* nodeProgressProxy() const; KisBusyProgressIndicator* busyProgressIndicator() const; private: /** * Create a node progress proxy for this node. You need to create a progress proxy only * if the node is going to appear in the layerbox, and it needs to be created before * the layer box is made aware of the proxy. */ void createNodeProgressProxy(); protected: KisBaseNodeSP parentCallback() const override; void notifyParentVisibilityChanged(bool value) override; void baseNodeChangedCallback() override; void baseNodeInvalidateAllFramesCallback() override; protected: void addKeyframeChannel(KisKeyframeChannel* channel) override; private: friend class KisNodeFacade; friend class KisNodeTest; friend class KisLayer; // Note: only for setting the preview mask! /** * Set the parent of this node. */ void setParent(KisNodeWSP parent); /** * Add the specified node above the specified node. If aboveThis * is 0, the node is added at the bottom. */ bool add(KisNodeSP newNode, KisNodeSP aboveThis); /** * Removes the node at the specified index from the child nodes. * * @return false if there is no node at this index */ bool remove(quint32 index); /** * Removes the node from the child nodes. * * @return false if there's no such node in this node. */ bool remove(KisNodeSP node); KisNodeSP prevChildImpl(KisNodeSP child); KisNodeSP nextChildImpl(KisNodeSP child); private: struct Private; Private * const m_d; }; Q_DECLARE_METATYPE(KisNodeSP) Q_DECLARE_METATYPE(KisNodeWSP) #endif diff --git a/libs/image/kis_node_graph_listener.cpp b/libs/image/kis_node_graph_listener.cpp index 583160c6a6..c7a9a0b95d 100644 --- a/libs/image/kis_node_graph_listener.cpp +++ b/libs/image/kis_node_graph_listener.cpp @@ -1,107 +1,107 @@ /* * Copyright (c) 2007 Boudewijn Rempt * Copyright (c) 2012 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_node_graph_listener.h" #include "kis_time_range.h" #include #include struct Q_DECL_HIDDEN KisNodeGraphListener::Private { Private() : sequenceNumber(0) {} int sequenceNumber; }; KisNodeGraphListener::KisNodeGraphListener() : m_d(new Private()) { } KisNodeGraphListener::~KisNodeGraphListener() { } void KisNodeGraphListener::aboutToAddANode(KisNode */*parent*/, int /*index*/) { m_d->sequenceNumber++; } void KisNodeGraphListener::nodeHasBeenAdded(KisNode */*parent*/, int /*index*/) { m_d->sequenceNumber++; } void KisNodeGraphListener::aboutToRemoveANode(KisNode */*parent*/, int /*index*/) { m_d->sequenceNumber++; } void KisNodeGraphListener::nodeHasBeenRemoved(KisNode */*parent*/, int /*index*/) { m_d->sequenceNumber++; } void KisNodeGraphListener::aboutToMoveNode(KisNode * /*node*/, int /*oldIndex*/, int /*newIndex*/) { m_d->sequenceNumber++; } void KisNodeGraphListener::nodeHasBeenMoved(KisNode * /*node*/, int /*oldIndex*/, int /*newIndex*/) { m_d->sequenceNumber++; } int KisNodeGraphListener::graphSequenceNumber() const { return m_d->sequenceNumber; } void KisNodeGraphListener::nodeChanged(KisNode * /*node*/) { } void KisNodeGraphListener::invalidateAllFrames() { } void KisNodeGraphListener::notifySelectionChanged() { } void KisNodeGraphListener::requestProjectionUpdate(KisNode * /*node*/, const QVector &/*rects*/, bool /*resetAnimationCache*/) { } -void KisNodeGraphListener::invalidateFrames(const KisTimeRange &range, const QRect &rect) +void KisNodeGraphListener::invalidateFrames(const KisFrameSet &range, const QRect &rect) { Q_UNUSED(range); Q_UNUSED(rect); } void KisNodeGraphListener::requestTimeSwitch(int time) { Q_UNUSED(time); } KisNode *KisNodeGraphListener::graphOverlayNode() const { return 0; } diff --git a/libs/image/kis_node_graph_listener.h b/libs/image/kis_node_graph_listener.h index 83951b1062..adc5fbbeae 100644 --- a/libs/image/kis_node_graph_listener.h +++ b/libs/image/kis_node_graph_listener.h @@ -1,125 +1,125 @@ /* * Copyright (c) 2007 Boudewijn Rempt * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KIS_NODE_GRAPH_LISTENER_H_ #define KIS_NODE_GRAPH_LISTENER_H_ #include "kritaimage_export.h" #include -class KisTimeRange; +class KisFrameSet; class KisNode; class QRect; /** * Implementations of this class are called by nodes whenever the node * graph changes. These implementations can then emit the right * signals so Qt interview models can be updated before and after * changes. * * The reason for this go-between is that we don't want our nodes to * be QObjects, nor to have sig-slot connections between every node * and every mode. * * It also manages the sequence number of the graph. This is a number * which can be used as a checksum for whether the graph has chenged * from some period of time or not. \see graphSequenceNumber() */ class KRITAIMAGE_EXPORT KisNodeGraphListener { public: KisNodeGraphListener(); virtual ~KisNodeGraphListener(); /** * Inform the model that we're going to add a node. */ virtual void aboutToAddANode(KisNode *parent, int index); /** * Inform the model we're done adding a node. */ virtual void nodeHasBeenAdded(KisNode *parent, int index); /** * Inform the model we're going to remove a node. */ virtual void aboutToRemoveANode(KisNode *parent, int index); /** * Inform the model we're done removing a node. */ virtual void nodeHasBeenRemoved(KisNode *parent, int index); /** * Inform the model we're about to start moving a node (which * includes removing and adding the same node) */ virtual void aboutToMoveNode(KisNode * node, int oldIndex, int newIndex); /** * Inform the model we're done moving the node: it has been * removed and added successfully */ virtual void nodeHasBeenMoved(KisNode * node, int oldIndex, int newIndex); virtual void nodeChanged(KisNode * node); virtual void invalidateAllFrames(); /** * Inform the model that one of the selections in the graph is * changed. The sender is not passed to the function (at least for * now) because the UI should decide itself whether it needs to * fetch new selection of not. */ virtual void notifySelectionChanged(); /** * Inform the model that a node has been changed (setDirty) */ virtual void requestProjectionUpdate(KisNode * node, const QVector &rects, bool resetAnimationCache); - virtual void invalidateFrames(const KisTimeRange &range, const QRect &rect); + virtual void invalidateFrames(const KisFrameSet &range, const QRect &rect); virtual void requestTimeSwitch(int time); virtual KisNode* graphOverlayNode() const; /** * Returns the sequence of the graph. * * Every time some operation performed, which might change the * hierarchy of the nodes, the sequence number grows by one. So * if you have any information about the graph which was acquired * when the sequence number was X and now it has become Y, it * means your information is outdated. * * It is used in the scheduler for checking whether queued walkers * should be regenerated. */ int graphSequenceNumber() const; private: struct Private; QScopedPointer m_d; }; #endif diff --git a/libs/image/kis_raster_keyframe_channel.cpp b/libs/image/kis_raster_keyframe_channel.cpp index 98c706f969..d17ff90fc9 100644 --- a/libs/image/kis_raster_keyframe_channel.cpp +++ b/libs/image/kis_raster_keyframe_channel.cpp @@ -1,346 +1,365 @@ /* * Copyright (c) 2015 Jouni Pentikäinen * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_raster_keyframe_channel.h" #include "kis_node.h" #include "kis_dom_utils.h" #include "kis_global.h" #include "kis_paint_device.h" #include "kis_paint_device_frames_interface.h" #include "kis_time_range.h" #include "kundo2command.h" #include "kis_onion_skin_compositor.h" #include "kis_keyframe_commands.h" class KisRasterKeyframe : public KisKeyframe { public: KisRasterKeyframe(KisRasterKeyframeChannel *channel, int time, int frameId) : KisKeyframe(channel, time) , frameId(frameId) {} KisRasterKeyframe(const KisRasterKeyframe *rhs, KisKeyframeChannel *channel) : KisKeyframe(rhs, channel) , frameId(rhs->frameId) {} int frameId; KisKeyframeSP cloneFor(KisKeyframeChannel *channel) const override { return toQShared(new KisRasterKeyframe(this, channel)); } bool hasContent() const override { KisRasterKeyframeChannel *channel = dynamic_cast(this->channel()); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(channel, true); return channel->keyframeHasContent(this); } }; struct KisRasterKeyframeChannel::Private { Private(KisPaintDeviceWSP paintDevice, const QString filenameSuffix) : paintDevice(paintDevice), filenameSuffix(filenameSuffix), onionSkinsEnabled(false) {} KisPaintDeviceWSP paintDevice; QMap framesByFilename; QMap frameFilenames; QMap> frameInstances; QString filenameSuffix; bool onionSkinsEnabled; }; KisRasterKeyframeChannel::KisRasterKeyframeChannel(const KoID &id, const KisPaintDeviceWSP paintDevice, KisDefaultBoundsBaseSP defaultBounds) : KisKeyframeChannel(id, defaultBounds), m_d(new Private(paintDevice, QString())) { } KisRasterKeyframeChannel::KisRasterKeyframeChannel(const KisRasterKeyframeChannel &rhs, KisNode *newParentNode, const KisPaintDeviceWSP newPaintDevice) : KisKeyframeChannel(rhs, newParentNode), m_d(new Private(newPaintDevice, rhs.m_d->filenameSuffix)) { KIS_ASSERT_RECOVER_NOOP(&rhs != this); m_d->frameFilenames = rhs.m_d->frameFilenames; m_d->onionSkinsEnabled = rhs.m_d->onionSkinsEnabled; } KisRasterKeyframeChannel::~KisRasterKeyframeChannel() { } KisKeyframeSP KisRasterKeyframeChannel::linkKeyframe(const KisKeyframeSP sourceKeyframe, int newTime, KUndo2Command *parentCommand) { - KisKeyframeSP newKeyframe = toQShared(new KisRasterKeyframe(this, newTime, frameId(sourceKeyframe))); + const int frame = frameId(sourceKeyframe); + KisKeyframeSP newKeyframe = toQShared(new KisRasterKeyframe(this, newTime, frame)); + m_d->frameInstances[frame].append(newKeyframe); KUndo2Command *cmd = new KisReplaceKeyframeCommand(this, newTime, newKeyframe, parentCommand); cmd->redo(); return newKeyframe; } int KisRasterKeyframeChannel::frameId(KisKeyframeSP keyframe) const { return frameId(keyframe.data()); } int KisRasterKeyframeChannel::frameId(const KisKeyframe *keyframe) const { const KisRasterKeyframe *key = dynamic_cast(keyframe); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(key, -1); return key->frameId; } int KisRasterKeyframeChannel::frameIdAt(int time) const { KisKeyframeSP activeKey = activeKeyframeAt(time); if (activeKey.isNull()) return -1; return frameId(activeKey); } void KisRasterKeyframeChannel::fetchFrame(KisKeyframeSP keyframe, KisPaintDeviceSP targetDevice) { m_d->paintDevice->framesInterface()->fetchFrame(frameId(keyframe), targetDevice); } void KisRasterKeyframeChannel::importFrame(int time, KisPaintDeviceSP sourceDevice, KUndo2Command *parentCommand) { KisKeyframeSP keyframe = addKeyframe(time, parentCommand); const int frame = frameId(keyframe); m_d->paintDevice->framesInterface()->uploadFrame(frame, sourceDevice); } QRect KisRasterKeyframeChannel::frameExtents(KisKeyframeSP keyframe) { return m_d->paintDevice->framesInterface()->frameBounds(frameId(keyframe)); } QString KisRasterKeyframeChannel::frameFilename(int frameId) const { return m_d->frameFilenames.value(frameId, QString()); } void KisRasterKeyframeChannel::setFilenameSuffix(const QString &suffix) { m_d->filenameSuffix = suffix; } void KisRasterKeyframeChannel::setFrameFilename(int frameId, const QString &filename) { KIS_SAFE_ASSERT_RECOVER_NOOP(!m_d->frameFilenames.contains(frameId)); m_d->frameFilenames.insert(frameId, filename); m_d->framesByFilename.insert(filename, frameId); } QString KisRasterKeyframeChannel::chooseFrameFilename(int frameId, const QString &layerFilename) { QString filename; if (m_d->frameFilenames.isEmpty()) { // Use legacy naming convention for first keyframe filename = layerFilename + m_d->filenameSuffix; } else { filename = layerFilename + m_d->filenameSuffix + ".f" + QString::number(frameId); } setFrameFilename(frameId, filename); return filename; } KisKeyframeSP KisRasterKeyframeChannel::createKeyframe(int time, const KisKeyframeSP copySrc, KUndo2Command *parentCommand) { KisRasterKeyframe *keyframe; - if (!copySrc) { - int frameId = m_d->paintDevice->framesInterface()->createFrame(false, 0, QPoint(), parentCommand); + const bool copy = !copySrc.isNull(); + const int srcFrameId = copy ? frameId(copySrc) : 0; + const int frameId = m_d->paintDevice->framesInterface()->createFrame(copy, srcFrameId, QPoint(), parentCommand); + + if (!copy) { keyframe = new KisRasterKeyframe(this, time, frameId); } else { - int srcFrame = frameId(copySrc); - int frameId = m_d->paintDevice->framesInterface()->createFrame(true, srcFrame, QPoint(), parentCommand); + const KisRasterKeyframe *srcKeyframe = dynamic_cast(copySrc.data()); + KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(srcKeyframe, KisKeyframeSP()); - KisRasterKeyframe *srcKeyframe = dynamic_cast(copySrc.data()); - Q_ASSERT(srcKeyframe); keyframe = new KisRasterKeyframe(srcKeyframe, this); - keyframe->setTime(time); keyframe->frameId = frameId; } - KisKeyframeSP key = toQShared(keyframe); + KisKeyframeSP keyframeSP = toQShared(keyframe); - m_d->frameInstances[keyframe->frameId].append(key); + m_d->frameInstances[keyframe->frameId].append(keyframeSP); - return key; + return keyframeSP; } -void KisRasterKeyframeChannel::destroyKeyframe(KisKeyframeSP key, KUndo2Command *parentCommand) +void KisRasterKeyframeChannel::destroyKeyframe(KisKeyframeSP keyframe, KUndo2Command *parentCommand) { - int id = frameId(key); + int id = frameId(keyframe); QVector &instances = m_d->frameInstances[id]; - instances.removeAll(key); + instances.removeAll(keyframe); if (instances.isEmpty()) { - m_d->paintDevice->framesInterface()->deleteFrame(id, parentCommand); m_d->frameInstances.remove(id); + m_d->paintDevice->framesInterface()->deleteFrame(id, parentCommand); } } void KisRasterKeyframeChannel::uploadExternalKeyframe(KisKeyframeChannel *srcChannel, int srcTime, KisKeyframeSP dstFrame) { KisRasterKeyframeChannel *srcRasterChannel = dynamic_cast(srcChannel); KIS_ASSERT_RECOVER_RETURN(srcRasterChannel); const int srcId = srcRasterChannel->frameIdAt(srcTime); const int dstId = frameId(dstFrame); m_d->paintDevice->framesInterface()-> uploadFrame(srcId, dstId, srcRasterChannel->m_d->paintDevice); } QRect KisRasterKeyframeChannel::affectedRect(KisKeyframeSP key) { KeyframesMap::iterator it = keys().find(key->time()); QRect rect; // Calculate changed area as the union of the current and previous keyframe. // This makes sure there are no artifacts left over from the previous frame // where the new one doesn't cover the area. if (it == keys().begin()) { // Using the *next* keyframe at the start of the timeline avoids artifacts // when deleting or moving the first key it++; } else { it--; } if (it != keys().end()) { rect = m_d->paintDevice->framesInterface()->frameBounds(frameId(it.value())); } rect |= m_d->paintDevice->framesInterface()->frameBounds(frameId(key)); if (m_d->onionSkinsEnabled) { const QRect dirtyOnionSkinsRect = KisOnionSkinCompositor::instance()->calculateFullExtent(m_d->paintDevice); rect |= dirtyOnionSkinsRect; } return rect; } QDomElement KisRasterKeyframeChannel::toXML(QDomDocument doc, const QString &layerFilename) { m_d->frameFilenames.clear(); return KisKeyframeChannel::toXML(doc, layerFilename); } void KisRasterKeyframeChannel::loadXML(const QDomElement &channelNode) { m_d->frameFilenames.clear(); KisKeyframeChannel::loadXML(channelNode); } void KisRasterKeyframeChannel::saveKeyframe(KisKeyframeSP keyframe, QDomElement keyframeElement, const QString &layerFilename) { int frame = frameId(keyframe); QString filename = frameFilename(frame); if (filename.isEmpty()) { filename = chooseFrameFilename(frame, layerFilename); } keyframeElement.setAttribute("frame", filename); QPoint offset = m_d->paintDevice->framesInterface()->frameOffset(frame); KisDomUtils::saveValue(&keyframeElement, "offset", offset); } KisKeyframeSP KisRasterKeyframeChannel::loadKeyframe(const QDomElement &keyframeNode) { int time = keyframeNode.attribute("time").toInt(); workaroundBrokenFrameTimeBug(&time); QPoint offset; KisDomUtils::loadValue(keyframeNode, "offset", &offset); QString frameFilename = keyframeNode.attribute("frame"); KisKeyframeSP keyframe; if (m_d->frameFilenames.isEmpty()) { // First keyframe loaded: use the existing frame KIS_SAFE_ASSERT_RECOVER_NOOP(keyframeCount() == 1); keyframe = constKeys().begin().value(); const int id = frameId(keyframe); setFrameFilename(id, frameFilename); // Remove from keys. It will get reinserted with new time once we return keys().remove(keyframe->time()); keyframe->setTime(time); m_d->paintDevice->framesInterface()->setFrameOffset(id, offset); } else { int frameId = m_d->framesByFilename.value(frameFilename, -1); if (frameId == -1) { KUndo2Command tempCommand; frameId = m_d->paintDevice->framesInterface()->createFrame(false, 0, offset, &tempCommand); setFrameFilename(frameId, frameFilename); } keyframe = toQShared(new KisRasterKeyframe(this, time, frameId)); m_d->frameInstances[frameId].append(keyframe); } return keyframe; } bool KisRasterKeyframeChannel::keyframeHasContent(const KisKeyframe *keyframe) const { return !m_d->paintDevice->framesInterface()->frameBounds(frameId(keyframe)).isEmpty(); } bool KisRasterKeyframeChannel::hasScalarValue() const { return false; } +KisFrameSet KisRasterKeyframeChannel::affectedFrames(int time) const +{ + const int frameId = frameIdAt(time); + KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(frameId >= 0, KisFrameSet()); + + KisFrameSet frames; + Q_FOREACH(KisKeyframeSP keyframe, m_d->frameInstances[frameId]) { + const KisKeyframeSP next = nextKeyframe(keyframe); + if (next.isNull()) { + frames |= KisFrameSet::infiniteFrom(keyframe->time()); + } else { + frames |= KisFrameSet::between(keyframe->time(), next->time() - 1); + } + } + return frames; +} + void KisRasterKeyframeChannel::setOnionSkinsEnabled(bool value) { m_d->onionSkinsEnabled = value; } bool KisRasterKeyframeChannel::onionSkinsEnabled() const { return m_d->onionSkinsEnabled; } diff --git a/libs/image/kis_raster_keyframe_channel.h b/libs/image/kis_raster_keyframe_channel.h index 8d10d844d5..cd948a9e97 100644 --- a/libs/image/kis_raster_keyframe_channel.h +++ b/libs/image/kis_raster_keyframe_channel.h @@ -1,97 +1,98 @@ /* * Copyright (c) 2015 Jouni Pentikäinen * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef _KIS_RASTER_KEYFRAME_CHANNEL_H #define _KIS_RASTER_KEYFRAME_CHANNEL_H #include "kis_keyframe_channel.h" class KRITAIMAGE_EXPORT KisRasterKeyframeChannel : public KisKeyframeChannel { Q_OBJECT public: KisRasterKeyframeChannel(const KoID& id, const KisPaintDeviceWSP paintDevice, KisDefaultBoundsBaseSP defaultBounds); KisRasterKeyframeChannel(const KisRasterKeyframeChannel &rhs, KisNode *newParentNode, const KisPaintDeviceWSP newPaintDevice); ~KisRasterKeyframeChannel() override; KisKeyframeSP linkKeyframe(const KisKeyframeSP keyframe, int newTime, KUndo2Command *parentCommand = 0) override; public: /** * Return the ID of the active frame at a given time. The active frame is * defined by the keyframe at the given time or the last keyframe before it. * @param time * @return active frame id */ int frameIdAt(int time) const; /** * Copy the active frame at given time to target device. * @param keyframe keyframe to copy from * @param targetDevice device to copy the frame to */ void fetchFrame(KisKeyframeSP keyframe, KisPaintDeviceSP targetDevice); /** * Copy the content of the sourceDevice into a new keyframe at given time * @param time position of new keyframe * @param sourceDevice source for content */ void importFrame(int time, KisPaintDeviceSP sourceDevice, KUndo2Command *parentCommand); QRect frameExtents(KisKeyframeSP keyframe); QString frameFilename(int frameId) const; /** * When choosing filenames for frames, this will be appended to the node filename */ void setFilenameSuffix(const QString &suffix); bool hasScalarValue() const override; + KisFrameSet affectedFrames(int time) const override; QDomElement toXML(QDomDocument doc, const QString &layerFilename) override; void loadXML(const QDomElement &channelNode) override; void setOnionSkinsEnabled(bool value); bool onionSkinsEnabled() const; protected: KisKeyframeSP createKeyframe(int time, const KisKeyframeSP copySrc, KUndo2Command *parentCommand) override; - void destroyKeyframe(KisKeyframeSP key, KUndo2Command *parentCommand) override; + void destroyKeyframe(KisKeyframeSP keyframe, KUndo2Command *parentCommand) override; void uploadExternalKeyframe(KisKeyframeChannel *srcChannel, int srcTime, KisKeyframeSP dstFrame) override; QRect affectedRect(KisKeyframeSP key) override; void saveKeyframe(KisKeyframeSP keyframe, QDomElement keyframeElement, const QString &layerFilename) override; KisKeyframeSP loadKeyframe(const QDomElement &keyframeNode) override; friend class KisRasterKeyframe; bool keyframeHasContent(const KisKeyframe *keyframe) const; private: void setFrameFilename(int frameId, const QString &filename); QString chooseFrameFilename(int frameId, const QString &layerFilename); int frameId(KisKeyframeSP keyframe) const; int frameId(const KisKeyframe *keyframe) const; struct Private; QScopedPointer m_d; }; #endif diff --git a/libs/image/kis_scalar_keyframe_channel.cpp b/libs/image/kis_scalar_keyframe_channel.cpp index ba73be65d7..2e9b9447c6 100644 --- a/libs/image/kis_scalar_keyframe_channel.cpp +++ b/libs/image/kis_scalar_keyframe_channel.cpp @@ -1,487 +1,487 @@ /* * Copyright (c) 2015 Jouni Pentikäinen * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_scalar_keyframe_channel.h" #include "kis_node.h" #include "kundo2command.h" #include "kis_time_range.h" #include #include struct KisScalarKeyframe : public KisKeyframe { KisScalarKeyframe(KisKeyframeChannel *channel, int time, qreal value) : KisKeyframe(channel, time) , value(value) {} KisScalarKeyframe(const KisScalarKeyframe *rhs, KisKeyframeChannel *channel) : KisKeyframe(rhs, channel) , value(rhs->value) {} qreal value; KisKeyframeSP cloneFor(KisKeyframeChannel *channel) const override { return toQShared(new KisScalarKeyframe(this, channel)); } }; KisScalarKeyframeChannel::AddKeyframeCommand::AddKeyframeCommand(KisScalarKeyframeChannel *channel, int time, qreal value, KUndo2Command *parentCommand) : KisReplaceKeyframeCommand(channel, time, channel->createKeyframe(time, value, parentCommand), parentCommand) {} struct KisScalarKeyframeChannel::Private { public: Private(qreal min, qreal max, KisKeyframe::InterpolationMode defaultInterpolation) : minValue(min), maxValue(max), firstFreeIndex(0), defaultInterpolation(defaultInterpolation) {} Private(const Private &rhs) : minValue(rhs.minValue), maxValue(rhs.maxValue), firstFreeIndex(rhs.firstFreeIndex), defaultInterpolation(rhs.defaultInterpolation) {} qreal minValue; qreal maxValue; int firstFreeIndex; KisKeyframe::InterpolationMode defaultInterpolation; struct SetValueCommand; struct SetTangentsCommand; struct SetInterpolationModeCommand; }; KisScalarKeyframeChannel::KisScalarKeyframeChannel(const KoID &id, qreal minValue, qreal maxValue, KisDefaultBoundsBaseSP defaultBounds, KisKeyframe::InterpolationMode defaultInterpolation) : KisKeyframeChannel(id, defaultBounds), m_d(new Private(minValue, maxValue, defaultInterpolation)) { } KisScalarKeyframeChannel::KisScalarKeyframeChannel(const KisScalarKeyframeChannel &rhs, KisNode *newParentNode) : KisKeyframeChannel(rhs, newParentNode), m_d(new Private(*rhs.m_d)) { } KisScalarKeyframeChannel::~KisScalarKeyframeChannel() {} bool KisScalarKeyframeChannel::hasScalarValue() const { return true; } qreal KisScalarKeyframeChannel::minScalarValue() const { return m_d->minValue; } qreal KisScalarKeyframeChannel::maxScalarValue() const { return m_d->maxValue; } qreal KisScalarKeyframeChannel::scalarValue(const KisKeyframeSP keyframe) const { KisScalarKeyframe *key = dynamic_cast(keyframe.data()); Q_ASSERT(key != 0); return key->value; } struct KisScalarKeyframeChannel::Private::SetValueCommand : public KUndo2Command { SetValueCommand(KisScalarKeyframeChannel *channel, KisKeyframeSP keyframe, qreal oldValue, qreal newValue, KUndo2Command *parentCommand) : KUndo2Command(parentCommand), m_channel(channel), m_keyframe(keyframe), m_oldValue(oldValue), m_newValue(newValue) { } void redo() override { setValue(m_newValue); } void undo() override { setValue(m_oldValue); } void setValue(qreal value) { KisScalarKeyframe *key = dynamic_cast(m_keyframe.data()); Q_ASSERT(key != 0); key->value = value; m_channel->notifyKeyframeChanged(m_keyframe); } private: KisScalarKeyframeChannel *m_channel; KisKeyframeSP m_keyframe; qreal m_oldValue; qreal m_newValue; }; struct KisScalarKeyframeChannel::Private::SetTangentsCommand : public KUndo2Command { SetTangentsCommand(KisScalarKeyframeChannel *channel, KisKeyframeSP keyframe, KisKeyframe::InterpolationTangentsMode oldMode, QPointF oldLeftTangent, QPointF oldRightTangent, KisKeyframe::InterpolationTangentsMode newMode, QPointF newLeftTangent, QPointF newRightTangent, KUndo2Command *parentCommand) : KUndo2Command(parentCommand), m_channel(channel), m_keyframe(keyframe), m_oldMode(oldMode), m_oldLeftTangent(oldLeftTangent), m_oldRightTangent(oldRightTangent), m_newMode(newMode), m_newLeftTangent(newLeftTangent), m_newRightTangent(newRightTangent) { } void redo() override { m_keyframe->setTangentsMode(m_newMode); m_keyframe->setInterpolationTangents(m_newLeftTangent, m_newRightTangent); m_channel->notifyKeyframeChanged(m_keyframe); } void undo() override { m_keyframe->setTangentsMode(m_oldMode); m_keyframe->setInterpolationTangents(m_oldLeftTangent, m_oldRightTangent); m_channel->notifyKeyframeChanged(m_keyframe); } private: KisScalarKeyframeChannel *m_channel; KisKeyframeSP m_keyframe; KisKeyframe::InterpolationTangentsMode m_oldMode; QPointF m_oldLeftTangent; QPointF m_oldRightTangent; KisKeyframe::InterpolationTangentsMode m_newMode; QPointF m_newLeftTangent; QPointF m_newRightTangent; }; struct KisScalarKeyframeChannel::Private::SetInterpolationModeCommand : public KUndo2Command { SetInterpolationModeCommand(KisScalarKeyframeChannel *channel, KisKeyframeSP keyframe, KisKeyframe::InterpolationMode oldMode, KisKeyframe::InterpolationMode newMode, KUndo2Command *parentCommand) : KUndo2Command(parentCommand), m_channel(channel), m_keyframe(keyframe), m_oldMode(oldMode), m_newMode(newMode) { } void redo() override { m_keyframe->setInterpolationMode(m_newMode); m_channel->notifyKeyframeChanged(m_keyframe); } void undo() override { m_keyframe->setInterpolationMode(m_oldMode); m_channel->notifyKeyframeChanged(m_keyframe); } private: KisScalarKeyframeChannel *m_channel; KisKeyframeSP m_keyframe; KisKeyframe::InterpolationMode m_oldMode; KisKeyframe::InterpolationMode m_newMode; }; void KisScalarKeyframeChannel::setScalarValue(KisKeyframeSP keyframe, qreal value, KUndo2Command *parentCommand) { QScopedPointer tempCommand; if (!parentCommand) { tempCommand.reset(new KUndo2Command()); parentCommand = tempCommand.data(); } qreal oldValue = scalarValue(keyframe); KUndo2Command *cmd = new Private::SetValueCommand(this, keyframe, oldValue, value, parentCommand); cmd->redo(); } void KisScalarKeyframeChannel::setInterpolationMode(KisKeyframeSP keyframe, KisKeyframe::InterpolationMode mode, KUndo2Command *parentCommand) { QScopedPointer tempCommand; if (!parentCommand) { tempCommand.reset(new KUndo2Command()); parentCommand = tempCommand.data(); } KisKeyframe::InterpolationMode oldMode = keyframe->interpolationMode(); KUndo2Command *cmd = new Private::SetInterpolationModeCommand(this, keyframe, oldMode, mode, parentCommand); cmd->redo(); } void KisScalarKeyframeChannel::setInterpolationTangents(KisKeyframeSP keyframe, KisKeyframe::InterpolationTangentsMode mode, QPointF leftTangent, QPointF rightTangent, KUndo2Command *parentCommand) { QScopedPointer tempCommand; if (!parentCommand) { tempCommand.reset(new KUndo2Command()); parentCommand = tempCommand.data(); } KisKeyframe::InterpolationTangentsMode oldMode = keyframe->tangentsMode(); QPointF oldLeftTangent = keyframe->leftTangent(); QPointF oldRightTangent = keyframe->rightTangent(); KUndo2Command *cmd = new Private::SetTangentsCommand(this, keyframe, oldMode, oldLeftTangent, oldRightTangent, mode, leftTangent, rightTangent, parentCommand); cmd->redo(); } qreal cubicBezier(qreal p0, qreal delta1, qreal delta2, qreal p3, qreal t) { qreal p1 = p0 + delta1; qreal p2 = p3 + delta2; qreal c = 1-t; return c*c*c * p0 + 3*c*c*t * p1 + 3*c*t*t * p2 + t*t*t * p3; } void normalizeTangents(const QPointF point1, QPointF &rightTangent, QPointF &leftTangent, const QPointF point2) { // To ensure that the curve is monotonic wrt time, // check that control points lie between the endpoints. // If not, force them into range by scaling down the tangents float interval = point2.x() - point1.x(); if (rightTangent.x() < 0) rightTangent *= 0; if (leftTangent.x() > 0) leftTangent *= 0; if (rightTangent.x() > interval) { rightTangent *= interval / rightTangent.x(); } if (leftTangent.x() < -interval) { leftTangent *= interval / -leftTangent.x(); } } QPointF KisScalarKeyframeChannel::interpolate(QPointF point1, QPointF rightTangent, QPointF leftTangent, QPointF point2, qreal t) { normalizeTangents(point1, rightTangent, leftTangent, point2); qreal x = cubicBezier(point1.x(), rightTangent.x(), leftTangent.x(), point2.x(), t); qreal y = cubicBezier(point1.y(), rightTangent.y(), leftTangent.y(), point2.y(), t); return QPointF(x,y); } qreal findCubicCurveParameter(int time0, qreal delta0, qreal delta1, int time1, int time) { if (time == time0) return 0.0; if (time == time1) return 1.0; qreal min_t = 0.0; qreal max_t = 1.0; while (true) { qreal t = (max_t + min_t) / 2; qreal time_t = cubicBezier(time0, delta0, delta1, time1, t); if (time_t < time - 0.05) { min_t = t; } else if (time_t > time + 0.05) { max_t = t; } else { // Close enough return t; } } } qreal KisScalarKeyframeChannel::interpolatedValue(int time) const { KisKeyframeSP activeKey = activeKeyframeAt(time); if (activeKey.isNull()) return qQNaN(); KisKeyframeSP nextKey = nextKeyframe(activeKey); qreal result = qQNaN(); if (time == activeKey->time() || nextKey.isNull()) { result = scalarValue(activeKey); } else { switch (activeKey->interpolationMode()) { case KisKeyframe::Constant: result = scalarValue(activeKey); break; case KisKeyframe::Linear: { int time0 = activeKey->time(); int time1 = nextKey->time(); qreal value0 = scalarValue(activeKey); qreal value1 = scalarValue(nextKey); result = value0 + (value1 - value0) * (time - time0) / (time1 - time0); } break; case KisKeyframe::Bezier: { QPointF point0 = QPointF(activeKey->time(), scalarValue(activeKey)); QPointF point1 = QPointF(nextKey->time(), scalarValue(nextKey)); QPointF tangent0 = activeKey->rightTangent(); QPointF tangent1 = nextKey->leftTangent(); normalizeTangents(point0, tangent0, tangent1, point1); qreal t = findCubicCurveParameter(point0.x(), tangent0.x(), tangent1.x(), point1.x(), time); result = interpolate(point0, tangent0, tangent1, point1, t).y(); } break; default: KIS_ASSERT_RECOVER_BREAK(false); break; } } if (result > m_d->maxValue) return m_d->maxValue; if (result < m_d->minValue) return m_d->minValue; return result; } qreal KisScalarKeyframeChannel::currentValue() const { return interpolatedValue(currentTime()); } KisKeyframeSP KisScalarKeyframeChannel::createKeyframe(int time, const KisKeyframeSP copySrc, KUndo2Command *parentCommand) { if (copySrc) { KisScalarKeyframe *srcKeyframe = dynamic_cast(copySrc.data()); Q_ASSERT(srcKeyframe); KisScalarKeyframe *keyframe = new KisScalarKeyframe(srcKeyframe, this); keyframe->setTime(time); return toQShared(keyframe); } else { return createKeyframe(time, 0, parentCommand); } } KisKeyframeSP KisScalarKeyframeChannel::createKeyframe(int time, qreal value, KUndo2Command *parentCommand) { Q_UNUSED(parentCommand); KisScalarKeyframe *keyframe = new KisScalarKeyframe(this, time, value); keyframe->setInterpolationMode(m_d->defaultInterpolation); return toQShared(keyframe); } void KisScalarKeyframeChannel::destroyKeyframe(KisKeyframeSP key, KUndo2Command *parentCommand) { Q_UNUSED(parentCommand); Q_UNUSED(key); } void KisScalarKeyframeChannel::uploadExternalKeyframe(KisKeyframeChannel *srcChannel, int srcTime, KisKeyframeSP dstFrame) { KisScalarKeyframeChannel *srcScalarChannel = dynamic_cast(srcChannel); KIS_ASSERT_RECOVER_RETURN(srcScalarChannel); KisKeyframeSP srcFrame = srcScalarChannel->keyframeAt(srcTime); KIS_ASSERT_RECOVER_RETURN(srcFrame); KisScalarKeyframe *dstKey = dynamic_cast(dstFrame.data()); dstKey->value = srcChannel->scalarValue(srcFrame); notifyKeyframeChanged(dstFrame); } QRect KisScalarKeyframeChannel::affectedRect(KisKeyframeSP key) { Q_UNUSED(key); if (node()) { return node()->extent(); } else { return QRect(); } } void KisScalarKeyframeChannel::saveKeyframe(KisKeyframeSP keyframe, QDomElement keyframeElement, const QString &layerFilename) { Q_UNUSED(layerFilename); keyframeElement.setAttribute("value", KisDomUtils::toString(scalarValue(keyframe))); QString interpolationMode; if (keyframe->interpolationMode() == KisKeyframe::Constant) interpolationMode = "constant"; if (keyframe->interpolationMode() == KisKeyframe::Linear) interpolationMode = "linear"; if (keyframe->interpolationMode() == KisKeyframe::Bezier) interpolationMode = "bezier"; QString tangentsMode; if (keyframe->tangentsMode() == KisKeyframe::Smooth) tangentsMode = "smooth"; if (keyframe->tangentsMode() == KisKeyframe::Sharp) tangentsMode = "sharp"; keyframeElement.setAttribute("interpolation", interpolationMode); keyframeElement.setAttribute("tangents", tangentsMode); KisDomUtils::saveValue(&keyframeElement, "leftTangent", keyframe->leftTangent()); KisDomUtils::saveValue(&keyframeElement, "rightTangent", keyframe->rightTangent()); } KisKeyframeSP KisScalarKeyframeChannel::loadKeyframe(const QDomElement &keyframeNode) { int time = keyframeNode.toElement().attribute("time").toInt(); workaroundBrokenFrameTimeBug(&time); qreal value = KisDomUtils::toDouble(keyframeNode.toElement().attribute("value")); KUndo2Command tempParentCommand; KisKeyframeSP keyframe = createKeyframe(time, KisKeyframeSP(), &tempParentCommand); setScalarValue(keyframe, value); QString interpolationMode = keyframeNode.toElement().attribute("interpolation"); if (interpolationMode == "constant") { keyframe->setInterpolationMode(KisKeyframe::Constant); } else if (interpolationMode == "linear") { keyframe->setInterpolationMode(KisKeyframe::Linear); } else if (interpolationMode == "bezier") { keyframe->setInterpolationMode(KisKeyframe::Bezier); } QString tangentsMode = keyframeNode.toElement().attribute("tangents"); if (tangentsMode == "smooth") { keyframe->setTangentsMode(KisKeyframe::Smooth); } else if (tangentsMode == "sharp") { keyframe->setTangentsMode(KisKeyframe::Sharp); } QPointF leftTangent; QPointF rightTangent; KisDomUtils::loadValue(keyframeNode, "leftTangent", &leftTangent); KisDomUtils::loadValue(keyframeNode, "rightTangent", &rightTangent); keyframe->setInterpolationTangents(leftTangent, rightTangent); return keyframe; } void KisScalarKeyframeChannel::notifyKeyframeChanged(KisKeyframeSP keyframe) { QRect rect = affectedRect(keyframe); - KisTimeRange range = affectedFrames(keyframe->time()); + KisFrameSet range = affectedFrames(keyframe->time()); requestUpdate(range, rect); emit sigKeyframeChanged(keyframe); } diff --git a/libs/image/kis_time_range.cpp b/libs/image/kis_time_range.cpp index 0274c644f6..d8afb9455b 100644 --- a/libs/image/kis_time_range.cpp +++ b/libs/image/kis_time_range.cpp @@ -1,376 +1,376 @@ /* * Copyright (c) 2015 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_time_range.h" #include #include "kis_keyframe_channel.h" #include "kis_node.h" #include "kis_layer_utils.h" +#include "kis_dom_utils.h" struct KisTimeRangeStaticRegistrar { KisTimeRangeStaticRegistrar() { qRegisterMetaType("KisTimeRange"); qRegisterMetaType("KisTimeSpan"); qRegisterMetaType("KisFrameSet"); } }; static KisTimeRangeStaticRegistrar __registrar; QDebug operator<<(QDebug dbg, const KisTimeRange &r) { dbg.nospace() << "KisTimeRange(" << r.start() << ", " << r.end() << ")"; return dbg.space(); } QDebug operator<<(QDebug dbg, const KisTimeSpan &r) { dbg.nospace() << "KisTimeSpan(" << r.start() << ", " << r.end() << ")"; return dbg.space(); } QDebug operator<<(QDebug dbg, const KisFrameSet &r) { const QVector &spans = r.finiteSpans(); dbg.nospace() << "KisFrameSet("; for (int i = 0; i < spans.size(); i++) { if (i > 0) dbg.nospace() << ", "; dbg.nospace() << spans[i].start() << ".." << spans[i].end(); } if (r.isInfinite()) dbg.nospace() << ", " << r.firstFrameOfInfinity() << "..."; dbg.nospace() << ")"; return dbg.space(); } KisFrameSet& KisFrameSet::operator|=(const KisFrameSet &rhs) { if (rhs.isEmpty()) return *this; if (isEmpty()) { *this = rhs; return *this; } QVector spans; int lIndex = 0, rIndex = 0; KisTimeSpan currentSpan; int firstOfInfinite = !isInfinite() ? rhs.m_firstFrameOfInfinity : (!rhs.isInfinite()) ? m_firstFrameOfInfinity : qMin(m_firstFrameOfInfinity, rhs.m_firstFrameOfInfinity); while (lIndex < m_spans.size() || rIndex < rhs.m_spans.size()) { const bool leftRemaining = (lIndex < m_spans.size()); const bool rightRemaining = (rIndex < rhs.m_spans.size()); const bool leftFirst = leftRemaining && (!rightRemaining || m_spans[lIndex].start() < rhs.m_spans[rIndex].start()); KisTimeSpan first; if (leftFirst) { first = m_spans[lIndex++]; } else { first = rhs.m_spans[rIndex++]; } if (isInfinite() && firstOfInfinite <= first.end()) { currentSpan = KisTimeSpan(); firstOfInfinite = qMin(first.start(), firstOfInfinite); break; } else if (first.start() <= currentSpan.end() || currentSpan.isEmpty()) { currentSpan = currentSpan | first; } else { spans.append(currentSpan); currentSpan = first; } } if (!currentSpan.isEmpty()) { spans.append(currentSpan); } m_spans = spans; m_firstFrameOfInfinity = firstOfInfinite; return *this; } void addIntersectionAgainstInfinity(const QVector &src, int firstIndex , QVector &dst, int firstFrameOfInfinity) { for (int index = firstIndex; index < src.size(); index++) { const KisTimeSpan span = src[index].truncateRight(firstFrameOfInfinity); if (!span.isEmpty()) dst.append(span); } } KisFrameSet& KisFrameSet::operator&=(const KisFrameSet &rhs) { if (isEmpty() || rhs.isEmpty()) { *this = KisFrameSet(); return *this; } QVector spans; int lIndex = 0, rIndex = 0; while (lIndex < m_spans.size() && rIndex < rhs.m_spans.size()) { KisTimeSpan span; const KisTimeSpan rSpan = rhs.m_spans[rIndex]; const KisTimeSpan lSpan = m_spans[lIndex]; span = lSpan & rSpan; if (!span.isEmpty()) { spans.append(span); } if (lSpan.start() < rSpan.start()) { lIndex++; } else { rIndex++; } } if (isInfinite()) addIntersectionAgainstInfinity(rhs.m_spans, rIndex, spans, m_firstFrameOfInfinity); if (rhs.isInfinite()) addIntersectionAgainstInfinity(m_spans, lIndex, spans, rhs.m_firstFrameOfInfinity); int firstOfInfinite = (isInfinite() && rhs.isInfinite()) ? qMax(m_firstFrameOfInfinity, rhs.m_firstFrameOfInfinity) : -1; m_spans = spans; m_firstFrameOfInfinity = firstOfInfinite; return *this; } KisFrameSet& KisFrameSet::operator-=(const KisFrameSet &rhs) { if (rhs.isEmpty()) return *this; if (isEmpty()) { *this = KisFrameSet(); return *this; } QVector spans; int firstOfInfinite = (isInfinite() && !rhs.isInfinite()) ? qMax(m_firstFrameOfInfinity, rhs.m_spans.last().end() + 1) : -1; KisTimeSpan currentSpan = m_spans.first(); int lIndex = 0, rIndex = 0; while (lIndex < m_spans.size() && rIndex < rhs.m_spans.size()) { const KisTimeSpan rSpan = rhs.m_spans[rIndex]; if (currentSpan.isEmpty() || currentSpan.end() < rSpan.start()) { if (!currentSpan.isEmpty()) { spans.append(currentSpan); } lIndex++; currentSpan = (lIndex < m_spans.size()) ? m_spans[lIndex] : KisTimeSpan(); } else { const KisTimeSpan tail = currentSpan.truncateRight(rSpan.end() + 1); const KisTimeSpan head = currentSpan.truncateLeft(rSpan.start() - 1); if (!head.isEmpty()) { spans.append(head); } currentSpan = tail; rIndex++; } } while (!currentSpan.isEmpty()) { if (rhs.isInfinite() && currentSpan.end() >= rhs.firstFrameOfInfinity()) { currentSpan = currentSpan.truncateLeft(rhs.firstFrameOfInfinity() - 1); if (!currentSpan.isEmpty()) spans.append(currentSpan); break; } spans.append(currentSpan); lIndex++; currentSpan = (lIndex < m_spans.size()) ? m_spans[lIndex] : KisTimeSpan(); } m_spans = spans; m_firstFrameOfInfinity = firstOfInfinite; return *this; } -KisTimeRange KisTimeRange::calculateIdenticalFramesRecursive(const KisNode *node, int time) +KisFrameSet calculateIdenticalFramesRecursive(const KisNode *node, int time) { - KisTimeRange range = KisTimeRange::infinite(0); + KisFrameSet frames = KisFrameSet::infiniteFrom(0); KisLayerUtils::recursiveApplyNodes(node, - [&range, time] (const KisNode *node) { + [&frames, time] (const KisNode *node) { if (node->visible()) { - range &= calculateNodeIdenticalFrames(node, time); + frames &= calculateNodeIdenticalFrames(node, time); } }); - return range; + return frames; } -KisTimeRange KisTimeRange::calculateAffectedFramesRecursive(const KisNode *node, int time) +KisFrameSet calculateAffectedFramesRecursive(const KisNode *node, int time) { - KisTimeRange range; + KisFrameSet frames; KisLayerUtils::recursiveApplyNodes(node, - [&range, time] (const KisNode *node) { + [&frames, time] (const KisNode *node) { if (node->visible()) { - range |= calculateNodeIdenticalFrames(node, time); + frames |= calculateNodeAffectedFrames(node, time); } }); - return range; + return frames; } int KisFrameSet::firstExcludedSince(int time) const { if (isEmpty()) return time; if (0 <= m_firstFrameOfInfinity && m_firstFrameOfInfinity <= time) return -1; if (time < start()) return time; if (time > m_spans.last().end()) return time; Q_FOREACH(const KisTimeSpan &span, m_spans) { if (span.start() > time) return time; if (span.end() >= time) return span.end() + 1; } KIS_SAFE_ASSERT_RECOVER_NOOP(false); return -1; } -KisTimeRange KisTimeRange::calculateNodeIdenticalFrames(const KisNode *node, int time) +KisFrameSet calculateNodeIdenticalFrames(const KisNode *node, int time) { - KisTimeRange range = KisTimeRange::infinite(0); + KisFrameSet range = KisFrameSet::infiniteFrom(0); const QMap channels = node->keyframeChannels(); Q_FOREACH (const KisKeyframeChannel *channel, channels) { // Intersection range &= channel->identicalFrames(time); } return range; } -KisTimeRange KisTimeRange::calculateNodeAffectedFrames(const KisNode *node, int time) +KisFrameSet calculateNodeAffectedFrames(const KisNode *node, int time) { - KisTimeRange range; + KisFrameSet range; if (!node->visible()) return range; const QMap channels = node->keyframeChannels(); // TODO: channels should report to the image which channel exactly has changed // to avoid the dirty range to be stretched into infinity! if (channels.isEmpty() || !channels.contains(KisKeyframeChannel::Content.id())) { - range = KisTimeRange::infinite(0); + range = KisFrameSet::infiniteFrom(0); return range; } Q_FOREACH (const KisKeyframeChannel *channel, channels) { // Union range |= channel->affectedFrames(time); } return range; } namespace KisDomUtils { void saveValue(QDomElement *parent, const QString &tag, const KisTimeRange &range) { QDomDocument doc = parent->ownerDocument(); QDomElement e = doc.createElement(tag); parent->appendChild(e); e.setAttribute("type", "timerange"); if (range.isValid()) { e.setAttribute("from", toString(range.start())); if (!range.isInfinite()) { e.setAttribute("to", toString(range.end())); } } } - bool loadValue(const QDomElement &parent, const QString &tag, KisTimeRange *range) { QDomElement e; if (!findOnlyElement(parent, tag, &e)) return false; if (!Private::checkType(e, "timerange")) return false; int start = toInt(e.attribute("from", "-1")); int end = toInt(e.attribute("to", "-1")); if (start == -1) { range = new KisTimeRange(); } else if (end == -1) { *range = KisTimeRange::infinite(start); } else { *range = KisTimeRange::fromTime(start, end); } return true; } void saveValue(QDomElement *parent, const QString &tag, const KisTimeSpan &range) { QDomDocument doc = parent->ownerDocument(); QDomElement e = doc.createElement(tag); parent->appendChild(e); e.setAttribute("type", "timerange"); if (!range.isEmpty()) { e.setAttribute("from", toString(range.start())); e.setAttribute("to", toString(range.end())); } } bool loadValue(const QDomElement &parent, const QString &tag, KisTimeSpan *range) { QDomElement e; if (!findOnlyElement(parent, tag, &e)) return false; if (!Private::checkType(e, "timerange")) return false; int start = toInt(e.attribute("from", "-1")); int end = toInt(e.attribute("to", "-1")); if (start < 0 || end < 0) { *range = KisTimeSpan(); } else { *range = KisTimeSpan(start, end); } return true; } } diff --git a/libs/image/kis_time_range.h b/libs/image/kis_time_range.h index 0b3b3aefb2..60b8954691 100644 --- a/libs/image/kis_time_range.h +++ b/libs/image/kis_time_range.h @@ -1,352 +1,353 @@ /* * Copyright (c) 2015 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __KIS_TIME_RANGE_H #define __KIS_TIME_RANGE_H #include "kritaimage_export.h" #include #include #include #include #include "kis_types.h" -#include + +class QDomElement; /** * Represents a finite, continuous span of time between two frames. * Start and end frames are both included in the span. */ class KRITAIMAGE_EXPORT KisTimeSpan : public boost::equality_comparable { public: inline KisTimeSpan() : m_start(-1) , m_end(-2) {} inline KisTimeSpan(int start, int end) : m_start(start), m_end(end) {} inline bool isEmpty() const { return m_end < m_start; } inline int start() const { return m_start; } inline int end() const { return m_end; } inline bool isValid() const { return isEmpty(); } inline int duration() const { return !isEmpty() ? (m_end - m_start + 1) : 0; } inline bool contains(int time) const { return !isEmpty() ? (m_start <= time && time <= m_end) : false; } inline bool contains(const KisTimeSpan rhs) const { return rhs.isEmpty() || (m_start <= rhs.start() && rhs.end() <= m_end); } inline bool overlaps(const KisTimeSpan &other) const { return m_start <= other.m_end && other.m_start <= m_end; } inline KisTimeSpan truncateLeft(int newEnd) const { if (newEnd < m_start) return KisTimeSpan(); if (m_end <= newEnd) return *this; return KisTimeSpan(m_start, newEnd); } inline KisTimeSpan truncateRight(int newStart) const { if (m_end < newStart) return KisTimeSpan(); if (newStart <= m_start) return *this; return KisTimeSpan(newStart, m_end); } bool operator==(const KisTimeSpan &rhs) const { return rhs.m_start == m_start && rhs.m_end == m_end; } KisTimeSpan operator|(const KisTimeSpan &rhs) const { if (isEmpty()) return rhs; if (rhs.isEmpty()) return *this; int start = qMin(m_start, rhs.m_start); int end = qMax(m_end, rhs.m_end); return KisTimeSpan(start, end); } KisTimeSpan operator&(const KisTimeSpan &rhs) const { if (isEmpty() || rhs.isEmpty()) return KisTimeSpan(); int start = qMax(m_start, rhs.m_start); int end = qMin(m_end, rhs.m_end); if (end < start) return KisTimeSpan(); return KisTimeSpan(start, end); } private: int m_start, m_end; }; /** * Represents an arbitrary set of frames, possibly stretching to infinity. * */ class KRITAIMAGE_EXPORT KisFrameSet : public boost::equality_comparable, public boost::andable, public boost::orable, public boost::subtractable { public: static KisFrameSet between(int start, int end) { return KisFrameSet(KisTimeSpan(start, end)); } static KisFrameSet infiniteFrom(int start) { return KisFrameSet({}, start); } KisFrameSet() = default; inline explicit KisFrameSet(QVector spans, int firstFrameOfInfinity = -1) : m_spans(spans) , m_firstFrameOfInfinity(firstFrameOfInfinity) { // Normalize std::sort(m_spans.begin(), m_spans.end(), [](const KisTimeSpan &a, const KisTimeSpan &b) { return a.start() < b.start(); } ); mergeOverlappingSpans(); } explicit KisFrameSet(const KisTimeSpan span) : m_spans({span}) {} inline bool isInfinite() const { return m_firstFrameOfInfinity >= 0; } bool isEmpty() const { return m_spans.isEmpty() && !isInfinite(); } int start() const { if (m_spans.isEmpty()) return m_firstFrameOfInfinity; return m_spans.first().start(); } /** * List of the finite, continuous spans this set consists of. * Note: this list does not contain the tail of infinite sets. See firstFrameOfInfinity(). */ inline const QVector finiteSpans() const { return m_spans; } /** * If the set is infinite, the frame from which the infinite tail begins. Returns -1 if the set is finite. */ inline int firstFrameOfInfinity() const { return m_firstFrameOfInfinity; } inline bool contains(int frame) const { if (0 <= m_firstFrameOfInfinity && m_firstFrameOfInfinity <= frame) return true; Q_FOREACH(const KisTimeSpan &span, m_spans) { if (span.contains(frame)) return true; } return false; } int firstExcludedSince(int time) const; bool operator==(const KisFrameSet &rhs) const { return rhs.m_firstFrameOfInfinity == m_firstFrameOfInfinity && rhs.m_spans == m_spans; } KisFrameSet& operator|=(const KisFrameSet &rhs); KisFrameSet& operator&=(const KisFrameSet &rhs); KisFrameSet& operator-=(const KisFrameSet &rhs); private: inline void mergeOverlappingSpans() { int dst = 0; for (int src = 1; src < m_spans.length(); src++) { if (isInfinite() && m_firstFrameOfInfinity <= m_spans[src].end()) { m_firstFrameOfInfinity = qMin(m_spans[src].start(), m_firstFrameOfInfinity); break; } if (m_spans[src].overlaps(m_spans[dst])) { m_spans[dst] = m_spans[dst] | m_spans[src]; } else { dst++; if (dst != src) { m_spans[dst] = m_spans[src]; } } } if (dst < m_spans.length() - 1) { m_spans.resize(dst - 1); } } QVector m_spans; int m_firstFrameOfInfinity = -1; }; class KRITAIMAGE_EXPORT KisTimeRange : public boost::equality_comparable { public: inline KisTimeRange() : m_start(0), m_end(-1) { } inline KisTimeRange(int start, int duration) : m_start(start), m_end(start + duration - 1) { } inline KisTimeRange(int start, int end, bool) : m_start(start), m_end(end) { } bool operator==(const KisTimeRange &rhs) const { return rhs.m_start == m_start && rhs.m_end == m_end; } KisTimeRange& operator|=(const KisTimeRange &rhs) { if (!isValid()) { m_start = rhs.start(); } else if (rhs.isValid()) { m_start = std::min(m_start, rhs.start()); } if (rhs.isInfinite() || isInfinite()) { m_end = std::numeric_limits::min(); } else if (!isValid()) { m_end = rhs.m_end; } else { m_end = std::max(m_end, rhs.m_end); } return *this; } KisTimeRange& operator&=(const KisTimeRange &rhs) { if (!isValid()) { return *this; } else if (!rhs.isValid()) { m_start = rhs.start(); m_end = rhs.m_end; return *this; } else { m_start = std::max(m_start, rhs.start()); } if (isInfinite()) { m_end = rhs.m_end; } else if (!rhs.isInfinite()) { m_end = std::min(m_end, rhs.m_end); } return *this; } inline int start() const { return m_start; } inline int end() const { return m_end; } inline int duration() const { return m_end >= m_start ? m_end - m_start + 1 : 0; } inline bool isInfinite() const { return m_end == std::numeric_limits::min(); } inline bool isValid() const { return (m_end >= m_start) || (m_end == std::numeric_limits::min() && m_start >= 0); } inline bool contains(int time) const { if (m_end == std::numeric_limits::min()) { return m_start <= time; } return m_start <= time && time <= m_end; } // Temporary adapter inline KisFrameSet asFrameSet() const { if (isInfinite()) return KisFrameSet::infiniteFrom(m_start); return KisFrameSet::between(m_start, m_end); } static inline KisTimeRange fromTime(int start, int end) { return KisTimeRange(start, end, true); } static inline KisTimeRange infinite(int start) { return KisTimeRange(start, std::numeric_limits::min(), true); } - static KisTimeRange calculateIdenticalFramesRecursive(const KisNode *node, int time); - static KisTimeRange calculateAffectedFramesRecursive(const KisNode *node, int time); - - static KisTimeRange calculateNodeIdenticalFrames(const KisNode *node, int time); - static KisTimeRange calculateNodeAffectedFrames(const KisNode *node, int time); - private: int m_start; int m_end; }; +KRITAIMAGE_EXPORT KisFrameSet calculateIdenticalFramesRecursive(const KisNode *node, int time); +KRITAIMAGE_EXPORT KisFrameSet calculateAffectedFramesRecursive(const KisNode *node, int time); + +KRITAIMAGE_EXPORT KisFrameSet calculateNodeIdenticalFrames(const KisNode *node, int time); +KRITAIMAGE_EXPORT KisFrameSet calculateNodeAffectedFrames(const KisNode *node, int time); + namespace KisDomUtils { void KRITAIMAGE_EXPORT saveValue(QDomElement *parent, const QString &tag, const KisTimeRange &range); bool KRITAIMAGE_EXPORT loadValue(const QDomElement &parent, const QString &tag, KisTimeRange *range); void KRITAIMAGE_EXPORT saveValue(QDomElement *parent, const QString &tag, const KisTimeSpan &range); bool KRITAIMAGE_EXPORT loadValue(const QDomElement &parent, const QString &tag, KisTimeSpan *range); } Q_DECLARE_METATYPE(KisTimeRange); Q_DECLARE_METATYPE(KisTimeSpan); Q_DECLARE_METATYPE(KisFrameSet); KRITAIMAGE_EXPORT QDebug operator<<(QDebug dbg, const KisTimeRange &r); KRITAIMAGE_EXPORT QDebug operator<<(QDebug dbg, const KisTimeSpan &r); KRITAIMAGE_EXPORT QDebug operator<<(QDebug dbg, const KisFrameSet &r); #endif /* __KIS_TIME_RANGE_H */ diff --git a/libs/image/tests/kis_image_animation_interface_test.cpp b/libs/image/tests/kis_image_animation_interface_test.cpp index 9cd46f8007..325201a0b8 100644 --- a/libs/image/tests/kis_image_animation_interface_test.cpp +++ b/libs/image/tests/kis_image_animation_interface_test.cpp @@ -1,309 +1,309 @@ /* * Copyright (c) 2015 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_image_animation_interface_test.h" #include #include #include #include "kundo2command.h" #include "kis_debug.h" #include "kis_image_animation_interface.h" #include "kis_signal_compressor_with_param.h" #include "kis_raster_keyframe_channel.h" #include "kis_time_range.h" void checkFrame(KisImageAnimationInterface *i, KisImageSP image, int frameId, bool externalFrameActive, const QRect &rc) { QCOMPARE(i->currentTime(), frameId); QCOMPARE(i->externalFrameActive(), externalFrameActive); QCOMPARE(image->projection()->exactBounds(), rc); } void KisImageAnimationInterfaceTest::testFrameRegeneration() { QRect refRect(QRect(0,0,512,512)); TestUtil::MaskParent p(refRect); KisPaintLayerSP layer2 = new KisPaintLayer(p.image, "paint2", OPACITY_OPAQUE_U8); p.image->addNode(layer2); const QRect rc1(101,101,100,100); const QRect rc2(102,102,100,100); const QRect rc3(103,103,100,100); const QRect rc4(104,104,100,100); KisImageAnimationInterface *i = p.image->animationInterface(); KisPaintDeviceSP dev1 = p.layer->paintDevice(); KisPaintDeviceSP dev2 = layer2->paintDevice(); p.layer->getKeyframeChannel(KisKeyframeChannel::Content.id(), true); layer2->getKeyframeChannel(KisKeyframeChannel::Content.id(), true); // check frame 0 { dev1->fill(rc1, KoColor(Qt::red, dev1->colorSpace())); QCOMPARE(dev1->exactBounds(), rc1); dev2->fill(rc2, KoColor(Qt::green, dev1->colorSpace())); QCOMPARE(dev2->exactBounds(), rc2); p.image->refreshGraph(); checkFrame(i, p.image, 0, false, rc1 | rc2); } // switch/create frame 10 i->switchCurrentTimeAsync(10); p.image->waitForDone(); KisKeyframeChannel *channel1 = dev1->keyframeChannel(); channel1->addKeyframe(10); KisKeyframeChannel *channel2 = dev2->keyframeChannel(); channel2->addKeyframe(10); // check frame 10 { QVERIFY(dev1->exactBounds().isEmpty()); QVERIFY(dev2->exactBounds().isEmpty()); dev1->fill(rc3, KoColor(Qt::red, dev2->colorSpace())); QCOMPARE(dev1->exactBounds(), rc3); dev2->fill(rc4, KoColor(Qt::green, dev2->colorSpace())); QCOMPARE(dev2->exactBounds(), rc4); p.image->refreshGraph(); checkFrame(i, p.image, 10, false, rc3 | rc4); } // check external frame (frame 0) { SignalToFunctionProxy proxy1(std::bind(checkFrame, i, p.image, 0, true, rc1 | rc2)); connect(i, SIGNAL(sigFrameReady(int)), &proxy1, SLOT(start()), Qt::DirectConnection); i->requestFrameRegeneration(0, QRegion(refRect)); QTest::qWait(200); } // current frame (flame 10) is still unchanged checkFrame(i, p.image, 10, false, rc3 | rc4); // switch back to frame 0 i->switchCurrentTimeAsync(0); p.image->waitForDone(); // check frame 0 { QCOMPARE(dev1->exactBounds(), rc1); QCOMPARE(dev2->exactBounds(), rc2); checkFrame(i, p.image, 0, false, rc1 | rc2); } // check external frame (frame 10) { SignalToFunctionProxy proxy2(std::bind(checkFrame, i, p.image, 10, true, rc3 | rc4)); connect(i, SIGNAL(sigFrameReady(int)), &proxy2, SLOT(start()), Qt::DirectConnection); i->requestFrameRegeneration(10, QRegion(refRect)); QTest::qWait(200); } // current frame is still unchanged checkFrame(i, p.image, 0, false, rc1 | rc2); } void KisImageAnimationInterfaceTest::testFramesChangedSignal() { QRect refRect(QRect(0,0,512,512)); TestUtil::MaskParent p(refRect); KisPaintLayerSP layer1 = p.layer; KisPaintLayerSP layer2 = new KisPaintLayer(p.image, "paint2", OPACITY_OPAQUE_U8); p.image->addNode(layer2); layer1->getKeyframeChannel(KisKeyframeChannel::Content.id(), true); layer2->getKeyframeChannel(KisKeyframeChannel::Content.id(), true); KisImageAnimationInterface *i = p.image->animationInterface(); KisPaintDeviceSP dev1 = p.layer->paintDevice(); KisPaintDeviceSP dev2 = layer2->paintDevice(); KisKeyframeChannel *channel = dev2->keyframeChannel(); channel->addKeyframe(10); channel->addKeyframe(20); // check switching a frame doesn't invalidate cache - QSignalSpy spy(i, SIGNAL(sigFramesChanged(KisTimeRange,QRect))); + QSignalSpy spy(i, SIGNAL(sigFramesChanged(KisFrameSet,QRect))); p.image->animationInterface()->switchCurrentTimeAsync(15); p.image->waitForDone(); QCOMPARE(spy.count(), 0); i->notifyNodeChanged(layer1.data(), QRect(), false); QCOMPARE(spy.count(), 1); QList arguments = spy.takeFirst(); - QCOMPARE(arguments.at(0).value(), KisTimeRange::infinite(0)); + QCOMPARE(arguments.at(0).value(), KisFrameSet::infiniteFrom(0)); i->notifyNodeChanged(layer2.data(), QRect(), false); QCOMPARE(spy.count(), 1); arguments = spy.takeFirst(); - QCOMPARE(arguments.at(0).value(), KisTimeRange(10, 10)); + QCOMPARE(arguments.at(0).value(), KisFrameSet::between(10, 19)); // Recursive channel = dev1->keyframeChannel(); channel->addKeyframe(13); spy.clear(); i->notifyNodeChanged(p.image->root().data(), QRect(), true); QCOMPARE(spy.count(), 1); arguments = spy.takeFirst(); - QEXPECT_FAIL("", "Infinite time range is expected to be (0, -2147483648), but is (1, -2147483648)", Continue); - QCOMPARE(arguments.at(0).value(), KisTimeRange::infinite(10)); + const KisFrameSet &value = arguments.at(0).value(); + QCOMPARE(value, KisFrameSet::infiniteFrom(0)); } void KisImageAnimationInterfaceTest::testAnimationCompositionBug() { QRect rect(QRect(0,0,512,512)); TestUtil::MaskParent p(rect); KUndo2Command parentCommand; KisPaintLayerSP layer1 = p.layer; KisPaintLayerSP layer2 = new KisPaintLayer(p.image, "paint2", OPACITY_OPAQUE_U8); p.image->addNode(layer2); layer1->getKeyframeChannel(KisKeyframeChannel::Content.id(), true); layer2->getKeyframeChannel(KisKeyframeChannel::Content.id(), true); layer1->paintDevice()->fill(rect, KoColor(Qt::red, layer1->paintDevice()->colorSpace())); layer2->paintDevice()->fill(QRect(128,128,128,128), KoColor(Qt::black, layer2->paintDevice()->colorSpace())); KisKeyframeChannel *rasterChannel = layer2->getKeyframeChannel(KisKeyframeChannel::Content.id()); rasterChannel->addKeyframe(10, &parentCommand); p.image->refreshGraph(); m_image = p.image; connect(p.image->animationInterface(), SIGNAL(sigFrameReady(int)), this, SLOT(slotFrameDone()), Qt::DirectConnection); p.image->animationInterface()->requestFrameRegeneration(5, rect); QTest::qWait(200); KisPaintDeviceSP tmpDevice = new KisPaintDevice(p.image->colorSpace()); tmpDevice->fill(rect, KoColor(Qt::red, tmpDevice->colorSpace())); tmpDevice->fill(QRect(128,128,128,128), KoColor(Qt::black, tmpDevice->colorSpace())); QImage expected = tmpDevice->createThumbnail(512, 512); QVERIFY(m_compositedFrame == expected); } void KisImageAnimationInterfaceTest::slotFrameDone() { m_compositedFrame = m_image->projection()->createThumbnail(512, 512); } void KisImageAnimationInterfaceTest::testSwitchFrameWithUndo() { QRect refRect(QRect(0,0,512,512)); TestUtil::MaskParent p(refRect); KisPaintLayerSP layer1 = p.layer; layer1->getKeyframeChannel(KisKeyframeChannel::Content.id(), true); KisImageAnimationInterface *i = p.image->animationInterface(); KisPaintDeviceSP dev1 = p.layer->paintDevice(); KisKeyframeChannel *channel = dev1->keyframeChannel(); channel->addKeyframe(10); channel->addKeyframe(20); QCOMPARE(i->currentTime(), 0); i->requestTimeSwitchWithUndo(15); QTest::qWait(100); p.image->waitForDone(); QCOMPARE(i->currentTime(), 15); i->requestTimeSwitchWithUndo(16); QTest::qWait(100); p.image->waitForDone(); QCOMPARE(i->currentTime(), 16); // the two commands have been merged! p.undoStore->undo(); QTest::qWait(100); p.image->waitForDone(); QCOMPARE(i->currentTime(), 0); p.undoStore->redo(); QTest::qWait(100); p.image->waitForDone(); QCOMPARE(i->currentTime(), 16); } #include "kis_processing_applicator.h" void KisImageAnimationInterfaceTest::testSwitchFrameHangup() { QRect refRect(QRect(0,0,512,512)); TestUtil::MaskParent p(refRect); KisPaintLayerSP layer1 = p.layer; layer1->getKeyframeChannel(KisKeyframeChannel::Content.id(), true); KisImageAnimationInterface *i = p.image->animationInterface(); KisPaintDeviceSP dev1 = p.layer->paintDevice(); KisKeyframeChannel *channel = dev1->keyframeChannel(); channel->addKeyframe(10); channel->addKeyframe(20); QCOMPARE(i->currentTime(), 0); i->requestTimeSwitchWithUndo(15); QTest::qWait(100); p.image->waitForDone(); QCOMPARE(i->currentTime(), 15); KisProcessingApplicator applicator(p.image, 0); i->requestTimeSwitchWithUndo(16); applicator.end(); QTest::qWait(100); p.image->waitForDone(); QCOMPARE(i->currentTime(), 16); } QTEST_MAIN(KisImageAnimationInterfaceTest) diff --git a/libs/image/tests/kis_keyframing_test.cpp b/libs/image/tests/kis_keyframing_test.cpp index 0e1919c516..715da19c88 100644 --- a/libs/image/tests/kis_keyframing_test.cpp +++ b/libs/image/tests/kis_keyframing_test.cpp @@ -1,528 +1,535 @@ /* * Copyright (c) 2015 Jouni Pentikäinen * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_keyframing_test.h" #include #include #include "kis_paint_device_frames_interface.h" #include "kis_keyframe_channel.h" #include "kis_scalar_keyframe_channel.h" #include "kis_raster_keyframe_channel.h" #include "kis_node.h" #include "kis_time_range.h" #include "kundo2command.h" #include #include "testing_timed_default_bounds.h" void KisKeyframingTest::initTestCase() { cs = KoColorSpaceRegistry::instance()->rgb8(); red = new quint8[cs->pixelSize()]; green = new quint8[cs->pixelSize()]; blue = new quint8[cs->pixelSize()]; cs->fromQColor(Qt::red, red); cs->fromQColor(Qt::green, green); cs->fromQColor(Qt::blue, blue); } void KisKeyframingTest::cleanupTestCase() { delete[] red; delete[] green; delete[] blue; } void KisKeyframingTest::testScalarChannel() { KisScalarKeyframeChannel *channel = new KisScalarKeyframeChannel(KoID(""), -17, 31, 0); KisKeyframeSP key; bool ok; QCOMPARE(channel->hasScalarValue(), true); QCOMPARE(channel->minScalarValue(), -17.0); QCOMPARE(channel->maxScalarValue(), 31.0); QVERIFY(channel->keyframeAt(0) == 0); // Adding new keyframe key = channel->addKeyframe(42); channel->setScalarValue(key, 7.0); key = channel->keyframeAt(42); QCOMPARE(channel->scalarValue(key), 7.0); // Copying a keyframe KisKeyframeSP key2 = channel->copyKeyframe(key, 13); QVERIFY(key2 != 0); QVERIFY(channel->keyframeAt(13) == key2); QCOMPARE(channel->scalarValue(key2), 7.0); // Adding a keyframe where one exists key2 = channel->addKeyframe(13); QVERIFY(key2 != key); QCOMPARE(channel->keyframeCount(), 2); // Moving keyframes ok = channel->moveKeyframe(key, 10); QCOMPARE(ok, true); QVERIFY(channel->keyframeAt(42) == 0); key = channel->keyframeAt(10); QCOMPARE(channel->scalarValue(key), 7.0); // Moving a keyframe where another one exists ok = channel->moveKeyframe(key, 13); QCOMPARE(ok, true); QVERIFY(channel->keyframeAt(13) != key2); // Deleting a keyframe channel->deleteKeyframe(key); QVERIFY(channel->keyframeAt(10) == 0); QCOMPARE(channel->keyframeCount(), 0); delete channel; } void KisKeyframingTest::testScalarChannelUndoRedo() { KisScalarKeyframeChannel *channel = new KisScalarKeyframeChannel(KoID(""), -17, 31, 0); KisKeyframeSP key; QCOMPARE(channel->hasScalarValue(), true); QCOMPARE(channel->minScalarValue(), -17.0); QCOMPARE(channel->maxScalarValue(), 31.0); QVERIFY(channel->keyframeAt(0) == 0); // Adding new keyframe KUndo2Command addCmd; key = channel->addKeyframe(42, &addCmd); channel->setScalarValue(key, 7.0, &addCmd); key = channel->keyframeAt(42); QCOMPARE(channel->scalarValue(key), 7.0); addCmd.undo(); KisKeyframeSP newKey; newKey = channel->keyframeAt(42); QVERIFY(!newKey); addCmd.redo(); newKey = channel->keyframeAt(42); QVERIFY(newKey); QCOMPARE(channel->scalarValue(key), 7.0); QCOMPARE(channel->scalarValue(newKey), 7.0); delete channel; } void KisKeyframingTest::testScalarInterpolation() { KisScalarKeyframeChannel *channel = new KisScalarKeyframeChannel(KoID(""), 0, 30, 0); KisKeyframeSP key1 = channel->addKeyframe(0); channel->setScalarValue(key1, 15); KisKeyframeSP key2 = channel->addKeyframe(10); channel->setScalarValue(key2, 30); // Constant key1->setInterpolationMode(KisKeyframe::Constant); QCOMPARE(channel->interpolatedValue(4), 15.0f); // Bezier key1->setInterpolationMode(KisKeyframe::Bezier); key1->setInterpolationTangents(QPointF(), QPointF(1,4)); key2->setInterpolationTangents(QPointF(-4,2), QPointF()); QVERIFY(qAbs(channel->interpolatedValue(4) - 24.9812f) < 0.1f); // Bezier, self-intersecting curve (auto-correct) channel->setScalarValue(key2, 15); key1->setInterpolationTangents(QPointF(), QPointF(13,10)); key2->setInterpolationTangents(QPointF(-13,10), QPointF()); QVERIFY(qAbs(channel->interpolatedValue(5) - 20.769f) < 0.1f); // Bezier, result outside allowed range (clamp) channel->setScalarValue(key2, 15); key1->setInterpolationTangents(QPointF(), QPointF(0, 50)); key2->setInterpolationTangents(QPointF(0, 50), QPointF()); QCOMPARE(channel->interpolatedValue(5), 30.0f); delete channel; } void KisKeyframingTest::testRasterChannel() { TestUtil::TestingTimedDefaultBounds *bounds = new TestUtil::TestingTimedDefaultBounds(); KisPaintDeviceSP dev = new KisPaintDevice(cs); dev->setDefaultBounds(bounds); KisRasterKeyframeChannel * channel = dev->createKeyframeChannel(KoID()); QCOMPARE(channel->hasScalarValue(), false); QCOMPARE(channel->keyframeCount(), 1); QCOMPARE(dev->framesInterface()->frames().count(), 1); QCOMPARE(channel->frameIdAt(0), 0); QVERIFY(channel->keyframeAt(0) != 0); KisKeyframeSP key_0 = channel->keyframeAt(0); // New keyframe KisKeyframeSP key_10 = channel->addKeyframe(10); QCOMPARE(channel->keyframeCount(), 2); QCOMPARE(dev->framesInterface()->frames().count(), 2); QVERIFY(channel->frameIdAt(10) != 0); dev->fill(0, 0, 512, 512, red); QImage thumb1a = dev->createThumbnail(50, 50); bounds->testingSetTime(10); dev->fill(0, 0, 512, 512, green); QImage thumb2a = dev->createThumbnail(50, 50); bounds->testingSetTime(0); QImage thumb1b = dev->createThumbnail(50, 50); QVERIFY(thumb2a != thumb1a); QVERIFY(thumb1b == thumb1a); // Duplicate keyframe KisKeyframeSP key_20 = channel->copyKeyframe(key_0, 20); bounds->testingSetTime(20); QImage thumb3a = dev->createThumbnail(50, 50); QVERIFY(thumb3a == thumb1b); dev->fill(0, 0, 512, 512, blue); QImage thumb3b = dev->createThumbnail(50, 50); bounds->testingSetTime(0); QImage thumb1c = dev->createThumbnail(50, 50); QVERIFY(thumb3b != thumb3a); QVERIFY(thumb1c == thumb1b); // Delete keyrame QCOMPARE(channel->keyframeCount(), 3); QCOMPARE(dev->framesInterface()->frames().count(), 3); channel->deleteKeyframe(key_20); QCOMPARE(channel->keyframeCount(), 2); QCOMPARE(dev->framesInterface()->frames().count(), 2); QVERIFY(channel->keyframeAt(20) == 0); channel->deleteKeyframe(key_10); QCOMPARE(channel->keyframeCount(), 1); QCOMPARE(dev->framesInterface()->frames().count(), 1); QVERIFY(channel->keyframeAt(10) == 0); channel->deleteKeyframe(key_0); QCOMPARE(channel->keyframeCount(), 1); QCOMPARE(dev->framesInterface()->frames().count(), 1); QVERIFY(channel->keyframeAt(0) != 0); } void KisKeyframingTest::testChannelSignals() { KisScalarKeyframeChannel *channel = new KisScalarKeyframeChannel(KoID(""), -17, 31, 0); KisKeyframeSP key; KisKeyframeSP resKey; qRegisterMetaType("KisKeyframeSP"); QSignalSpy spyPreAdd(channel, SIGNAL(sigKeyframeAboutToBeAdded(KisKeyframeSP))); QSignalSpy spyPostAdd(channel, SIGNAL(sigKeyframeAdded(KisKeyframeSP))); QSignalSpy spyPreRemove(channel, SIGNAL(sigKeyframeAboutToBeRemoved(KisKeyframeSP))); QSignalSpy spyPostRemove(channel, SIGNAL(sigKeyframeRemoved(KisKeyframeSP))); QSignalSpy spyPreMove(channel, SIGNAL(sigKeyframeAboutToBeMoved(KisKeyframeSP,int))); QSignalSpy spyPostMove(channel, SIGNAL(sigKeyframeMoved(KisKeyframeSP, int))); QVERIFY(spyPreAdd.isValid()); QVERIFY(spyPostAdd.isValid()); QVERIFY(spyPreRemove.isValid()); QVERIFY(spyPostRemove.isValid()); QVERIFY(spyPreMove.isValid()); QVERIFY(spyPostMove.isValid()); // Adding a keyframe QCOMPARE(spyPreAdd.count(), 0); QCOMPARE(spyPostAdd.count(), 0); key = channel->addKeyframe(10); QCOMPARE(spyPreAdd.count(), 1); QCOMPARE(spyPostAdd.count(), 1); resKey = spyPreAdd.at(0).at(0).value(); QVERIFY(resKey == key); resKey = spyPostAdd.at(0).at(0).value(); QVERIFY(resKey == key); // Moving a keyframe QCOMPARE(spyPreMove.count(), 0); QCOMPARE(spyPostMove.count(), 0); channel->moveKeyframe(key, 15); QCOMPARE(spyPreMove.count(), 1); QCOMPARE(spyPostMove.count(), 1); resKey = spyPreMove.at(0).at(0).value(); QVERIFY(resKey == key); QCOMPARE(spyPreMove.at(0).at(1).toInt(), 15); resKey = spyPostMove.at(0).at(0).value(); QVERIFY(resKey == key); // No-op move (no signals) channel->moveKeyframe(key, 15); QCOMPARE(spyPreMove.count(), 1); QCOMPARE(spyPostMove.count(), 1); // Deleting a keyframe QCOMPARE(spyPreRemove.count(), 0); QCOMPARE(spyPostRemove.count(), 0); channel->deleteKeyframe(key); QCOMPARE(spyPreRemove.count(), 1); QCOMPARE(spyPostRemove.count(), 1); delete channel; } void KisKeyframingTest::testRasterFrameFetching() { TestUtil::TestingTimedDefaultBounds *bounds = new TestUtil::TestingTimedDefaultBounds(); KisPaintDeviceSP dev = new KisPaintDevice(cs); KisPaintDeviceSP devTarget = new KisPaintDevice(cs); dev->setDefaultBounds(bounds); KisRasterKeyframeChannel * channel = dev->createKeyframeChannel(KoID()); channel->addKeyframe(0); channel->addKeyframe(10); channel->addKeyframe(50); bounds->testingSetTime(0); dev->fill(0, 0, 512, 512, red); QImage frame1 = dev->createThumbnail(50, 50); bounds->testingSetTime(10); dev->fill(0, 256, 512, 512, green); QImage frame2 = dev->createThumbnail(50, 50); bounds->testingSetTime(50); dev->fill(0, 0, 256, 512, blue); QImage frame3 = dev->createThumbnail(50, 50); bounds->testingSetTime(10); KisKeyframeSP keyframe = channel->activeKeyframeAt(0); channel->fetchFrame(keyframe, devTarget); QImage fetched1 = devTarget->createThumbnail(50, 50); keyframe = channel->activeKeyframeAt(10); channel->fetchFrame(keyframe, devTarget); QImage fetched2 = devTarget->createThumbnail(50, 50); keyframe = channel->activeKeyframeAt(50); channel->fetchFrame(keyframe, devTarget); QImage fetched3 = devTarget->createThumbnail(50, 50); QVERIFY(fetched1 == frame1); QVERIFY(fetched2 == frame2); QVERIFY(fetched3 == frame3); } void KisKeyframingTest::testDeleteFirstRasterChannel() { // Test Plan: // // delete // undo delete // move // undo move TestUtil::TestingTimedDefaultBounds *bounds = new TestUtil::TestingTimedDefaultBounds(); KisPaintDeviceSP dev = new KisPaintDevice(cs); dev->setDefaultBounds(bounds); KisRasterKeyframeChannel * channel = dev->createKeyframeChannel(KoID()); QCOMPARE(channel->hasScalarValue(), false); QCOMPARE(channel->keyframeCount(), 1); QCOMPARE(dev->framesInterface()->frames().count(), 1); QCOMPARE(channel->frameIdAt(0), 0); QVERIFY(channel->keyframeAt(0) != 0); KisKeyframeSP key_0 = channel->keyframeAt(0); { KUndo2Command cmd; bool deleteResult = channel->deleteKeyframe(key_0, &cmd); QVERIFY(deleteResult); QCOMPARE(dev->framesInterface()->frames().count(), 1); QVERIFY(channel->frameIdAt(0) != 0); QVERIFY(channel->keyframeAt(0)); QVERIFY(channel->keyframeAt(0) != key_0); cmd.undo(); QCOMPARE(dev->framesInterface()->frames().count(), 1); QVERIFY(channel->frameIdAt(0) == 0); QVERIFY(channel->keyframeAt(0)); QVERIFY(channel->keyframeAt(0) == key_0); } { KUndo2Command cmd; bool moveResult = channel->moveKeyframe(key_0, 1, &cmd); QVERIFY(moveResult); QCOMPARE(dev->framesInterface()->frames().count(), 2); QVERIFY(channel->frameIdAt(0) != 0); QVERIFY(channel->frameIdAt(1) == 0); QVERIFY(channel->keyframeAt(0)); QVERIFY(channel->keyframeAt(1)); QVERIFY(channel->keyframeAt(0) != key_0); QVERIFY(channel->keyframeAt(1) == key_0); cmd.undo(); QCOMPARE(dev->framesInterface()->frames().count(), 1); QVERIFY(channel->frameIdAt(0) == 0); QVERIFY(channel->keyframeAt(0)); QVERIFY(channel->keyframeAt(0) == key_0); } } void KisKeyframingTest::testAffectedFrames() { KisScalarKeyframeChannel *channel = new KisScalarKeyframeChannel(KoID(""), -17, 31, 0); - KisTimeRange range; + KisFrameSet frames; channel->addKeyframe(10); channel->addKeyframe(20); channel->addKeyframe(30); // At a keyframe - range = channel->affectedFrames(20); - QCOMPARE(range.start(), 20); - QCOMPARE(range.end(), 29); - QCOMPARE(range.isInfinite(), false); + frames = channel->affectedFrames(20); + QCOMPARE(frames, KisFrameSet::between(20, 29)); // Between frames - range = channel->affectedFrames(25); - QCOMPARE(range.start(), 20); - QCOMPARE(range.end(), 29); - QCOMPARE(range.isInfinite(), false); + frames = channel->affectedFrames(25); + QCOMPARE(frames, KisFrameSet::between(20, 29)); // Before first frame - range = channel->affectedFrames(5); - QCOMPARE(range.start(), 0); - QCOMPARE(range.end(), 9); - QCOMPARE(range.isInfinite(), false); + frames = channel->affectedFrames(5); + QCOMPARE(frames, KisFrameSet::between(0, 9)); // After last frame - range = channel->affectedFrames(35); - QCOMPARE(range.start(), 30); - QCOMPARE(range.isInfinite(), true); + frames = channel->affectedFrames(35); + QCOMPARE(frames, KisFrameSet::infiniteFrom(30)); + // Linked keyframes + + KisPaintDeviceSP dev = new KisPaintDevice(cs); + KisRasterKeyframeChannel * rasterChannel = dev->createKeyframeChannel(KoID()); + + auto key0 = rasterChannel->addKeyframe(0); + auto key5 = rasterChannel->addKeyframe(5); + auto key10 = rasterChannel->linkKeyframe(key0, 10, nullptr); + + QCOMPARE(rasterChannel->affectedFrames(5), KisFrameSet::between(5,9)); + KisFrameSet result = rasterChannel->affectedFrames(1); + QCOMPARE(result, KisFrameSet::between(0, 4) | KisFrameSet::infiniteFrom(10)); + result = rasterChannel->affectedFrames(15); + QCOMPARE(result, KisFrameSet::between(0, 4) | KisFrameSet::infiniteFrom(10)); } void KisKeyframingTest::testMovingFrames() { TestUtil::TestingTimedDefaultBounds *bounds = new TestUtil::TestingTimedDefaultBounds(); KisPaintDeviceSP dev = new KisPaintDevice(cs); dev->setDefaultBounds(bounds); KisRasterKeyframeChannel * srcChannel = dev->createKeyframeChannel(KoID()); srcChannel->addKeyframe(0); srcChannel->addKeyframe(10); srcChannel->addKeyframe(50); KisPaintDeviceSP dev2 = new KisPaintDevice(*dev, KritaUtils::CopyAllFrames); KisRasterKeyframeChannel * dstChannel = dev->keyframeChannel(); for (int i = 0; i < 1000; i++) { const int srcTime = 50 + i; const int dstTime = 60 + i; const int src2Time = 51 + i; { KUndo2Command parentCommand; KisKeyframeSP srcKeyframe = srcChannel->keyframeAt(srcTime); KIS_ASSERT(srcKeyframe); dstChannel->copyExternalKeyframe(srcChannel, srcTime, dstTime, &parentCommand); srcChannel->deleteKeyframe(srcKeyframe, &parentCommand); } for (int j = qMax(0, i-15); j < i+5; ++j) { bounds->testingSetTime(j); QRect rc1 = dev->extent(); QRect rc2 = dev2->extent(); Q_UNUSED(rc1); Q_UNUSED(rc2); } { KUndo2Command parentCommand; KisKeyframeSP dstKeyframe = dstChannel->keyframeAt(dstTime); KIS_ASSERT(dstKeyframe); srcChannel->copyExternalKeyframe(dstChannel, dstTime, src2Time, &parentCommand); dstChannel->deleteKeyframe(dstKeyframe, &parentCommand); } } } QTEST_MAIN(KisKeyframingTest) diff --git a/libs/ui/dialogs/KisAsyncAnimationCacheRenderDialog.cpp b/libs/ui/dialogs/KisAsyncAnimationCacheRenderDialog.cpp index e5d484c9de..0fd8fef604 100644 --- a/libs/ui/dialogs/KisAsyncAnimationCacheRenderDialog.cpp +++ b/libs/ui/dialogs/KisAsyncAnimationCacheRenderDialog.cpp @@ -1,101 +1,101 @@ /* * Copyright (c) 2017 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "KisAsyncAnimationCacheRenderDialog.h" #include "KisAsyncAnimationCacheRenderer.h" #include "kis_animation_frame_cache.h" #include #include #include namespace { QList calcDirtyFramesList(KisAnimationFrameCacheSP cache, const KisTimeSpan &playbackRange) { QList framesToRegenerate; KisImageSP image = cache->image(); if (!image) return framesToRegenerate; KisImageAnimationInterface *animation = image->animationInterface(); if (!animation->hasAnimation()) return framesToRegenerate; KisFrameSet dirtyFrames = cache->dirtyFramesWithin(playbackRange); while (!dirtyFrames.isEmpty()) { int first = dirtyFrames.start(); framesToRegenerate.append(first); - KisFrameSet duplicates = KisTimeRange::calculateIdenticalFramesRecursive(image->root(), first).asFrameSet(); + KisFrameSet duplicates = calculateIdenticalFramesRecursive(image->root(), first); dirtyFrames -= duplicates; } return framesToRegenerate; } } struct KisAsyncAnimationCacheRenderDialog::Private { Private(KisAnimationFrameCacheSP _cache, const KisTimeSpan &_range) : cache(_cache), range(_range) { } KisAnimationFrameCacheSP cache; KisTimeSpan range; }; KisAsyncAnimationCacheRenderDialog::KisAsyncAnimationCacheRenderDialog(KisAnimationFrameCacheSP cache, const KisTimeSpan &range, int busyWait) : KisAsyncAnimationRenderDialogBase(i18n("Regenerating cache..."), cache->image(), busyWait), m_d(new Private(cache, range)) { } KisAsyncAnimationCacheRenderDialog::~KisAsyncAnimationCacheRenderDialog() { } QList KisAsyncAnimationCacheRenderDialog::calcDirtyFrames() const { return calcDirtyFramesList(m_d->cache, m_d->range); } KisAsyncAnimationRendererBase *KisAsyncAnimationCacheRenderDialog::createRenderer(KisImageSP image) { Q_UNUSED(image); return new KisAsyncAnimationCacheRenderer(); } void KisAsyncAnimationCacheRenderDialog::initializeRendererForFrame(KisAsyncAnimationRendererBase *renderer, KisImageSP image, int frame) { Q_UNUSED(image); Q_UNUSED(frame); KisAsyncAnimationCacheRenderer *cacheRenderer = dynamic_cast(renderer); KIS_SAFE_ASSERT_RECOVER_RETURN(cacheRenderer); cacheRenderer->setFrameCache(m_d->cache); } diff --git a/libs/ui/kis_animation_cache_populator.cpp b/libs/ui/kis_animation_cache_populator.cpp index 228d962da5..ea6a3fab24 100644 --- a/libs/ui/kis_animation_cache_populator.cpp +++ b/libs/ui/kis_animation_cache_populator.cpp @@ -1,317 +1,317 @@ /* * Copyright (c) 2015 Jouni Pentikäinen * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_animation_cache_populator.h" #include #include #include #include #include "kis_config.h" #include "kis_config_notifier.h" #include "KisPart.h" #include "KisDocument.h" #include "kis_image.h" #include "kis_image_animation_interface.h" #include "kis_canvas2.h" #include "kis_time_range.h" #include "kis_animation_frame_cache.h" #include "kis_update_info.h" #include "kis_signal_auto_connection.h" #include "kis_idle_watcher.h" #include "KisViewManager.h" #include "kis_node_manager.h" #include "kis_keyframe_channel.h" #include "KisAsyncAnimationCacheRenderer.h" #include "dialogs/KisAsyncAnimationCacheRenderDialog.h" struct KisAnimationCachePopulator::Private { KisAnimationCachePopulator *q; KisPart *part; QTimer timer; /** * Counts up the number of subsequent times Krita has been detected idle. */ int idleCounter; static const int IDLE_COUNT_THRESHOLD = 4; static const int IDLE_CHECK_INTERVAL = 500; static const int BETWEEN_FRAMES_INTERVAL = 10; int requestedFrame; KisAnimationFrameCacheSP requestCache; KisOpenGLUpdateInfoSP requestInfo; KisSignalAutoConnectionsStore imageRequestConnections; QFutureWatcher infoConversionWatcher; KisAsyncAnimationCacheRenderer regenerator; bool calculateAnimationCacheInBackground = true; enum State { NotWaitingForAnything, WaitingForIdle, WaitingForFrame, BetweenFrames }; State state; Private(KisAnimationCachePopulator *_q, KisPart *_part) : q(_q), part(_part), idleCounter(0), requestedFrame(-1), state(WaitingForIdle) { timer.setSingleShot(true); } void timerTimeout() { switch (state) { case WaitingForIdle: case BetweenFrames: generateIfIdle(); break; case WaitingForFrame: KIS_ASSERT_RECOVER_NOOP(0 && "WaitingForFrame cannot have a timeout. Just skip this message and report a bug"); break; case NotWaitingForAnything: KIS_ASSERT_RECOVER_NOOP(0 && "NotWaitingForAnything cannot have a timeout. Just skip this message and report a bug"); break; } } void generateIfIdle() { if (part->idleWatcher()->isIdle()) { idleCounter++; if (idleCounter >= IDLE_COUNT_THRESHOLD) { if (!tryRequestGeneration()) { enterState(NotWaitingForAnything); } return; } } else { idleCounter = 0; } enterState(WaitingForIdle); } bool tryRequestGeneration() { // Prioritize the active document KisAnimationFrameCacheSP activeDocumentCache = KisAnimationFrameCacheSP(0); KisMainWindow *activeWindow = part->currentMainwindow(); if (activeWindow && activeWindow->activeView()) { KisCanvas2 *activeCanvas = activeWindow->activeView()->canvasBase(); if (activeCanvas && activeCanvas->frameCache()) { activeDocumentCache = activeCanvas->frameCache(); // Let's skip frames affected by changes to the active node (on the active document) // This avoids constant invalidation and regeneration while drawing KisNodeSP activeNode = activeCanvas->viewManager()->nodeManager()->activeNode(); KisFrameSet skipRange; if (activeNode) { int currentTime = activeCanvas->currentImage()->animationInterface()->currentUITime(); if (!activeNode->keyframeChannels().isEmpty()) { Q_FOREACH (const KisKeyframeChannel *channel, activeNode->keyframeChannels()) { - skipRange |= channel->affectedFrames(currentTime).asFrameSet(); + skipRange |= channel->affectedFrames(currentTime); } } else { skipRange = KisFrameSet::infiniteFrom(0); } } bool requested = tryRequestGeneration(activeDocumentCache, skipRange); if (requested) return true; } } QList caches = KisAnimationFrameCache::caches(); KisAnimationFrameCache *cache; Q_FOREACH (cache, caches) { if (cache == activeDocumentCache.data()) { // We already handled this one... continue; } bool requested = tryRequestGeneration(cache, KisFrameSet()); if (requested) return true; } return false; } bool tryRequestGeneration(KisAnimationFrameCacheSP cache, KisFrameSet skipRange) { KisImageSP image = cache->image(); if (!image) return false; KisImageAnimationInterface *animation = image->animationInterface(); KisTimeSpan currentRange = animation->fullClipRange(); const int frame = cache->firstDirtyFrameWithin(currentRange, &skipRange); if (frame >= 0) { return regenerate(cache, frame); } return false; } bool regenerate(KisAnimationFrameCacheSP cache, int frame) { if (state == WaitingForFrame) { // Already busy, deny request return false; } /** * We should enter the state before the frame is * requested. Otherwise the signal may come earlier than we * enter it. */ enterState(WaitingForFrame); regenerator.setFrameCache(cache); // if we ever decide to add ROI to background cache // regeneration, it should be added here :) regenerator.startFrameRegeneration(cache->image(), frame); return true; } QString debugStateToString(State newState) { QString str = ""; switch (newState) { case WaitingForIdle: str = "WaitingForIdle"; break; case WaitingForFrame: str = "WaitingForFrame"; break; case NotWaitingForAnything: str = "NotWaitingForAnything"; break; case BetweenFrames: str = "BetweenFrames"; break; } return str; } void enterState(State newState) { //ENTER_FUNCTION() << debugStateToString(state) << "->" << debugStateToString(newState); state = newState; int timerTimeout = -1; switch (state) { case WaitingForIdle: timerTimeout = IDLE_CHECK_INTERVAL; break; case WaitingForFrame: // the timeout is handled by the regenerator now timerTimeout = -1; break; case NotWaitingForAnything: // frame conversion cannot be cancelled, // so there is no timeout timerTimeout = -1; break; case BetweenFrames: timerTimeout = BETWEEN_FRAMES_INTERVAL; break; } if (timerTimeout >= 0) { timer.start(timerTimeout); } else { timer.stop(); } } }; KisAnimationCachePopulator::KisAnimationCachePopulator(KisPart *part) : m_d(new Private(this, part)) { connect(&m_d->timer, SIGNAL(timeout()), this, SLOT(slotTimer())); connect(&m_d->regenerator, SIGNAL(sigFrameCancelled(int)), SLOT(slotRegeneratorFrameCancelled())); connect(&m_d->regenerator, SIGNAL(sigFrameCompleted(int)), SLOT(slotRegeneratorFrameReady())); connect(KisConfigNotifier::instance(), SIGNAL(configChanged()), SLOT(slotConfigChanged())); slotConfigChanged(); } KisAnimationCachePopulator::~KisAnimationCachePopulator() {} bool KisAnimationCachePopulator::regenerate(KisAnimationFrameCacheSP cache, int frame) { return m_d->regenerate(cache, frame); } void KisAnimationCachePopulator::slotTimer() { m_d->timerTimeout(); } void KisAnimationCachePopulator::slotRequestRegeneration() { // skip if the user forbade background regeneration if (!m_d->calculateAnimationCacheInBackground) return; m_d->enterState(Private::WaitingForIdle); } void KisAnimationCachePopulator::slotRegeneratorFrameCancelled() { KIS_ASSERT_RECOVER_RETURN(m_d->state == Private::WaitingForFrame); m_d->enterState(Private::NotWaitingForAnything); } void KisAnimationCachePopulator::slotRegeneratorFrameReady() { m_d->enterState(Private::BetweenFrames); } void KisAnimationCachePopulator::slotConfigChanged() { KisConfig cfg(true); m_d->calculateAnimationCacheInBackground = cfg.calculateAnimationCacheInBackground(); QTimer::singleShot(1000, this, SLOT(slotRequestRegeneration())); } diff --git a/libs/ui/kis_animation_frame_cache.cpp b/libs/ui/kis_animation_frame_cache.cpp index 18fbc48705..606775e8f9 100644 --- a/libs/ui/kis_animation_frame_cache.cpp +++ b/libs/ui/kis_animation_frame_cache.cpp @@ -1,475 +1,475 @@ /* * Copyright (c) 2015 Jouni Pentikäinen * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_animation_frame_cache.h" #include #include #include "kis_debug.h" #include "kis_image.h" #include "kis_image_animation_interface.h" #include "kis_time_range.h" #include "KisPart.h" #include "kis_animation_cache_populator.h" #include #include "KisFrameCacheSwapper.h" #include "KisInMemoryFrameCacheSwapper.h" #include "kis_image_config.h" #include "kis_config_notifier.h" #include "opengl/kis_opengl_image_textures.h" #include #include struct KisAnimationFrameCache::Private { Private(KisOpenGLImageTexturesSP _textures) : textures(_textures) { image = textures->image(); } ~Private() { } KisOpenGLImageTexturesSP textures; KisImageWSP image; QScopedPointer swapper; int frameSizeLimit = 777; KisOpenGLUpdateInfoSP fetchFrameDataImpl(KisImageSP image, const QRect &requestedRect, int lod); struct CacheEntry { int frameId; int length; CacheEntry(int id, int length) : frameId(id), length(length) {} bool isInfinite() const { return length < 0; } }; int nextFreeId = 0; /// Map of cache entries by beginning time of the entry QMap cachedFrames; /// Maps a frame ID to a list of times, where entries with said frame ID begin QMap> entriesById; void addFrame(int start, int length, int id) { cachedFrames.insert(start, CacheEntry(id, length)); entriesById[id].append(start); } QMap::iterator iteratorFrom(int time) { if (cachedFrames.isEmpty()) return cachedFrames.end(); auto it = cachedFrames.upperBound(time); if (it != cachedFrames.begin()) { auto previous = it - 1; if (previous.value().isInfinite() || time <= previous.key() + previous.value().length) { return previous; } } return it; } QMap::const_iterator constIteratorFrom(int time) const { if (cachedFrames.isEmpty()) return cachedFrames.constEnd(); auto it = cachedFrames.upperBound(time); if (it != cachedFrames.constBegin()) { auto previous = it - 1; if (previous.value().isInfinite() || time <= previous.key() + previous.value().length) { return previous; } } return it; } int getFrameIdAtTime(int time) const { auto it = constIteratorFrom(time); if (it == cachedFrames.constEnd()) return -1; const int start = it.key(); const CacheEntry &frame = it.value(); bool foundFrameValid = false; if (frame.isInfinite()) { if (start <= time) { foundFrameValid = true; } } else { int end = start + frame.length - 1; if (start <= time && time <= end) { foundFrameValid = true; } } return foundFrameValid ? frame.frameId : -1; } bool hasFrame(int time) const { return getFrameIdAtTime(time) >= 0; } KisOpenGLUpdateInfoSP getFrame(int time) { const int frameId = getFrameIdAtTime(time); return frameId >= 0 ? swapper->loadFrame(frameId) : 0; } void addFrame(KisOpenGLUpdateInfoSP info, const KisFrameSet& targetFrames) { invalidate(targetFrames); const int id = nextFreeId++; swapper->saveFrame(id, info, image->bounds()); for (const KisTimeSpan span : targetFrames.finiteSpans()) { addFrame(span.start(), span.duration(), id); } if (targetFrames.isInfinite()) { addFrame(targetFrames.firstFrameOfInfinity(), -1, id); } } /** * Invalidate any cached frames within the given time range. * @param range * @return true if frames were invalidated, false if nothing was changed */ bool invalidate(const KisFrameSet& range) { if (cachedFrames.isEmpty()) return false; bool cacheChanged = false; for (const KisTimeSpan span : range.finiteSpans()) { cacheChanged |= invalidate(span.start(), span.end()); } if (range.isInfinite()) { cacheChanged |= invalidate(range.firstFrameOfInfinity(), -1); } return cacheChanged; } bool invalidate(int invalidateFrom, int invalidateTo) { const bool infinite = invalidateTo < 0; bool cacheChanged = false; auto it = iteratorFrom(invalidateFrom); while (it != cachedFrames.end()) { const int start = it.key(); const CacheEntry frame = it.value(); const bool frameIsInfinite = (frame.length == -1); const int end = start + frame.length - 1; if (start >= invalidateFrom) { if (!infinite) { if (start > invalidateTo) { break; } if (frameIsInfinite || end > invalidateTo) { // Shorten the entry from the beginning int newStart = invalidateTo + 1; int newLength = frameIsInfinite ? -1 : (end - newStart + 1); cachedFrames.insert(newStart, CacheEntry(frame.frameId, newLength)); addFrame(newStart, newLength, frame.frameId); } } QVector &instances = entriesById[frame.frameId]; instances.removeAll(it.key()); if (instances.isEmpty()) swapper->forgetFrame(frame.frameId); it = cachedFrames.erase(it); cacheChanged = true; continue; } else if (frameIsInfinite || end >= invalidateFrom) { const int newEnd = invalidateFrom - 1; const int newLength = newEnd - start + 1; *it = CacheEntry(frame.frameId, newLength); cacheChanged = true; } it++; } return cacheChanged; } int effectiveLevelOfDetail(const QRect &rc) const { if (!frameSizeLimit) return 0; const int maxDimension = KisAlgebra2D::maxDimension(rc); const qreal minLod = -std::log2(qreal(frameSizeLimit) / maxDimension); const int lodLimit = qMax(0, qCeil(minLod)); return lodLimit; } // TODO: verify that we don't have any leak here! typedef QMap CachesMap; static CachesMap caches; }; KisAnimationFrameCache::Private::CachesMap KisAnimationFrameCache::Private::caches; KisAnimationFrameCacheSP KisAnimationFrameCache::getFrameCache(KisOpenGLImageTexturesSP textures) { KisAnimationFrameCache *cache; Private::CachesMap::iterator it = Private::caches.find(textures); if (it == Private::caches.end()) { cache = new KisAnimationFrameCache(textures); Private::caches.insert(textures, cache); } else { cache = it.value(); } return cache; } const QList KisAnimationFrameCache::caches() { return Private::caches.values(); } KisAnimationFrameCache::KisAnimationFrameCache(KisOpenGLImageTexturesSP textures) : m_d(new Private(textures)) { // create swapping backend slotConfigChanged(); connect(m_d->image->animationInterface(), SIGNAL(sigFramesChanged(KisFrameSet, QRect)), this, SLOT(framesChanged(KisFrameSet, QRect))); connect(KisConfigNotifier::instance(), SIGNAL(configChanged()), SLOT(slotConfigChanged())); } KisAnimationFrameCache::~KisAnimationFrameCache() { Private::caches.remove(m_d->textures); } bool KisAnimationFrameCache::uploadFrame(int time) { KisOpenGLUpdateInfoSP info = m_d->getFrame(time); if (!info) { // Do nothing! // // Previously we were trying to start cache regeneration in this point, // but it caused even bigger slowdowns when scrubbing } else { m_d->textures->recalculateCache(info); } return bool(info); } bool KisAnimationFrameCache::shouldUploadNewFrame(int newTime, int oldTime) const { if (oldTime < 0) return true; const int oldFrameId = m_d->getFrameIdAtTime(oldTime); const int newFrameId = m_d->getFrameIdAtTime(newTime); return (oldFrameId < 0) || oldFrameId != newFrameId; } KisAnimationFrameCache::CacheStatus KisAnimationFrameCache::frameStatus(int time) const { return m_d->hasFrame(time) ? Cached : Uncached; } KisImageWSP KisAnimationFrameCache::image() { return m_d->image; } void KisAnimationFrameCache::framesChanged(const KisFrameSet &range, const QRect &rect) { Q_UNUSED(rect); if (range.isEmpty()) return; bool cacheChanged = m_d->invalidate(range); if (cacheChanged) { emit changed(); } } void KisAnimationFrameCache::slotConfigChanged() { m_d->cachedFrames.clear(); KisImageConfig cfg(true); if (cfg.useOnDiskAnimationCacheSwapping()) { m_d->swapper.reset(new KisFrameCacheSwapper(m_d->textures->updateInfoBuilder(), cfg.swapDir())); } else { m_d->swapper.reset(new KisInMemoryFrameCacheSwapper()); } m_d->frameSizeLimit = cfg.useAnimationCacheFrameSizeLimit() ? cfg.animationCacheFrameSizeLimit() : 0; emit changed(); } KisOpenGLUpdateInfoSP KisAnimationFrameCache::Private::fetchFrameDataImpl(KisImageSP image, const QRect &requestedRect, int lod) { if (lod > 0) { KisPaintDeviceSP tempDevice = new KisPaintDevice(image->projection()->colorSpace()); tempDevice->prepareClone(image->projection()); image->projection()->generateLodCloneDevice(tempDevice, image->projection()->extent(), lod); const QRect fetchRect = KisLodTransform::alignedRect(requestedRect, lod); return textures->updateInfoBuilder().buildUpdateInfo(fetchRect, tempDevice, image->bounds(), lod, true); } else { return textures->updateCache(requestedRect, image); } } KisOpenGLUpdateInfoSP KisAnimationFrameCache::fetchFrameData(int time, KisImageSP image, const QRegion &requestedRegion) const { if (time != image->animationInterface()->currentTime()) { qWarning() << "WARNING: KisAnimationFrameCache::frameReady image's time doesn't coincide with the requested time!"; qWarning() << " " << ppVar(image->animationInterface()->currentTime()) << ppVar(time); } // the frames are always generated at full scale KIS_SAFE_ASSERT_RECOVER_NOOP(image->currentLevelOfDetail() == 0); const int lod = m_d->effectiveLevelOfDetail(requestedRegion.boundingRect()); KisOpenGLUpdateInfoSP totalInfo; Q_FOREACH (const QRect &rc, requestedRegion.rects()) { KisOpenGLUpdateInfoSP info = m_d->fetchFrameDataImpl(image, rc, lod); if (!totalInfo) { totalInfo = info; } else { const bool result = totalInfo->tryMergeWith(*info); KIS_SAFE_ASSERT_RECOVER_NOOP(result); } } return totalInfo; } void KisAnimationFrameCache::addConvertedFrameData(KisOpenGLUpdateInfoSP info, int time) { - const KisFrameSet identicalFrames = KisTimeRange::calculateIdenticalFramesRecursive(m_d->image->root(), time).asFrameSet(); + const KisFrameSet identicalFrames = calculateIdenticalFramesRecursive(m_d->image->root(), time); m_d->addFrame(info, identicalFrames); emit changed(); } void KisAnimationFrameCache::dropLowQualityFrames(const KisTimeSpan &range, const QRect ®ionOfInterest, const QRect &minimalRect) { if (m_d->cachedFrames.isEmpty()) return; QVector framesToDrop; for (auto it = m_d->constIteratorFrom(range.start()); it != m_d->cachedFrames.constEnd() && it.key() <= range.end(); it++) { const Private::CacheEntry &frame = it.value(); const QRect frameRect = m_d->swapper->frameDirtyRect(frame.frameId); const int frameLod = m_d->swapper->frameLevelOfDetail(frame.frameId); if (frameLod > m_d->effectiveLevelOfDetail(regionOfInterest) || !frameRect.contains(minimalRect)) { if (!framesToDrop.contains(frame.frameId)) framesToDrop.append(frame.frameId); } } Q_FOREACH(int frameId, framesToDrop) { Q_FOREACH(int time, m_d->entriesById[frameId]) { m_d->cachedFrames.remove(time); } m_d->entriesById.remove(frameId); m_d->swapper->forgetFrame(frameId); } } KisFrameSet KisAnimationFrameCache::dirtyFramesWithin(const KisTimeSpan range) { auto it = m_d->constIteratorFrom(range.start()); if (it == m_d->cachedFrames.constEnd()) return KisFrameSet(range); QVector cachedSpans; int firstOfInfinite = -1; for (; it != m_d->cachedFrames.constEnd() && it.key() <= range.end(); it++) { const int start = it.key(); Private::CacheEntry entry = it.value(); if (entry.isInfinite()) { firstOfInfinite = start; } else { cachedSpans.append(KisTimeSpan(start, start + entry.length - 1)); } } return KisFrameSet(range) - KisFrameSet(cachedSpans, firstOfInfinite); } int KisAnimationFrameCache::firstDirtyFrameWithin(const KisTimeSpan range, const KisFrameSet *ignoredFrames) { int candidate = range.start(); for (auto it = m_d->constIteratorFrom(range.start()); it != m_d->cachedFrames.constEnd(); it++) { const int start = it.key(); const int end = start + it.value().length - 1; if (ignoredFrames) { candidate = ignoredFrames->firstExcludedSince(candidate); } if (candidate < start) { return candidate; } else if (candidate <= end) { candidate = end + 1; } } return -1; } diff --git a/libs/ui/tests/kis_animation_frame_cache_test.cpp b/libs/ui/tests/kis_animation_frame_cache_test.cpp index cf3eaf3798..17d620435b 100644 --- a/libs/ui/tests/kis_animation_frame_cache_test.cpp +++ b/libs/ui/tests/kis_animation_frame_cache_test.cpp @@ -1,103 +1,103 @@ /* * Copyright (c) 2015 Jouni Pentikäinen * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_animation_frame_cache_test.h" #include #include #include "kis_animation_frame_cache.h" #include "kis_image_animation_interface.h" #include "opengl/kis_opengl_image_textures.h" #include "kis_time_range.h" #include "kis_keyframe_channel.h" #include "kundo2command.h" void verifyRangeIsCachedStatus(KisAnimationFrameCacheSP cache, int start, int end, KisAnimationFrameCache::CacheStatus status) { for (int t = start; t <= end; t++) { QVERIFY2( cache->frameStatus(t) == status, qPrintable(QString("Expected status %1 for frame %2 in range %3 to %4").arg(status == KisAnimationFrameCache::Cached ? "Cached" : "Uncached").arg(t).arg(start).arg(end)) ); } } void KisAnimationFrameCacheTest::testCache() { TestUtil::MaskParent p; KisImageSP image = p.image; KisImageAnimationInterface *animation = image->animationInterface(); KisPaintLayerSP layer1 = p.layer; KisPaintLayerSP layer2 = new KisPaintLayer(p.image, "", OPACITY_OPAQUE_U8); KisPaintLayerSP layer3 = new KisPaintLayer(p.image, "", OPACITY_OPAQUE_U8); image->addNode(layer2); image->addNode(layer3); KUndo2Command parentCommand; KisKeyframeChannel *rasterChannel2 = layer2->getKeyframeChannel(KisKeyframeChannel::Content.id()); rasterChannel2->addKeyframe(10, &parentCommand); rasterChannel2->addKeyframe(20, &parentCommand); rasterChannel2->addKeyframe(30, &parentCommand); KisKeyframeChannel *rasterChannel3 = layer2->getKeyframeChannel(KisKeyframeChannel::Content.id()); rasterChannel3->addKeyframe(17, &parentCommand); KisOpenGLImageTexturesSP glTex = KisOpenGLImageTextures::getImageTextures(image, 0, KoColorConversionTransformation::IntentPerceptual, KoColorConversionTransformation::Empty); KisAnimationFrameCacheSP cache = new KisAnimationFrameCache(glTex); int t; animation->saveAndResetCurrentTime(11, &t); animation->notifyFrameReady(); QCOMPARE(cache->frameStatus(9), KisAnimationFrameCache::Uncached); verifyRangeIsCachedStatus(cache, 10, 16, KisAnimationFrameCache::Cached); QCOMPARE(cache->frameStatus(17), KisAnimationFrameCache::Uncached); animation->saveAndResetCurrentTime(30, &t); animation->notifyFrameReady(); QCOMPARE(cache->frameStatus(29), KisAnimationFrameCache::Uncached); verifyRangeIsCachedStatus(cache, 30, 40, KisAnimationFrameCache::Cached); QCOMPARE(cache->frameStatus(9999), KisAnimationFrameCache::Cached); - image->invalidateFrames(KisTimeRange::fromTime(10, 12), QRect()); + image->invalidateFrames(KisFrameSet::between(10, 12), QRect()); verifyRangeIsCachedStatus(cache, 10, 12, KisAnimationFrameCache::Uncached); verifyRangeIsCachedStatus(cache, 13, 16, KisAnimationFrameCache::Cached); - image->invalidateFrames(KisTimeRange::fromTime(15, 20), QRect()); + image->invalidateFrames(KisFrameSet::between(15, 20), QRect()); verifyRangeIsCachedStatus(cache, 13, 14, KisAnimationFrameCache::Cached); verifyRangeIsCachedStatus(cache, 15, 20, KisAnimationFrameCache::Uncached); - image->invalidateFrames(KisTimeRange::infinite(100), QRect()); + image->invalidateFrames(KisFrameSet::infiniteFrom(100), QRect()); verifyRangeIsCachedStatus(cache, 90, 99, KisAnimationFrameCache::Cached); verifyRangeIsCachedStatus(cache, 100, 110, KisAnimationFrameCache::Uncached); - image->invalidateFrames(KisTimeRange::fromTime(90, 100), QRect()); + image->invalidateFrames(KisFrameSet::between(90, 100), QRect()); verifyRangeIsCachedStatus(cache, 80, 89, KisAnimationFrameCache::Cached); verifyRangeIsCachedStatus(cache, 90, 100, KisAnimationFrameCache::Uncached); - image->invalidateFrames(KisTimeRange::infinite(14), QRect()); + image->invalidateFrames(KisFrameSet::infiniteFrom(14), QRect()); QCOMPARE(cache->frameStatus(13), KisAnimationFrameCache::Cached); verifyRangeIsCachedStatus(cache, 15, 100, KisAnimationFrameCache::Uncached); } QTEST_MAIN(KisAnimationFrameCacheTest) diff --git a/plugins/tools/tool_transform2/kis_animated_transform_parameters.cpp b/plugins/tools/tool_transform2/kis_animated_transform_parameters.cpp index b3ba975470..51af344c36 100644 --- a/plugins/tools/tool_transform2/kis_animated_transform_parameters.cpp +++ b/plugins/tools/tool_transform2/kis_animated_transform_parameters.cpp @@ -1,309 +1,309 @@ /* * Copyright (c) 2016 Jouni Pentikäinen * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_animated_transform_parameters.h" #include "kis_scalar_keyframe_channel.h" #include "kis_transform_args_keyframe_channel.h" #include "tool_transform_args.h" #include "kis_time_range.h" #include "kis_transform_mask.h" struct KisAnimatedTransformMaskParameters::Private { Private() : hidden(false), argsCache() {} KisTransformArgsKeyframeChannel *rawArgsChannel{0}; KisScalarKeyframeChannel *positionXchannel{0}; KisScalarKeyframeChannel *positionYchannel{0}; KisScalarKeyframeChannel *scaleXchannel{0}; KisScalarKeyframeChannel *scaleYchannel{0}; KisScalarKeyframeChannel *shearXchannel{0}; KisScalarKeyframeChannel *shearYchannel{0}; KisScalarKeyframeChannel *rotationXchannel{0}; KisScalarKeyframeChannel *rotationYchannel{0}; KisScalarKeyframeChannel *rotationZchannel{0}; bool hidden; - KisTimeRange validRange; + KisFrameSet validRange; ToolTransformArgs argsCache; ToolTransformArgs ¤tRawArgs() { if (!rawArgsChannel) return argsCache; KisKeyframeSP keyframe = rawArgsChannel->currentlyActiveKeyframe(); if (keyframe.isNull()) return argsCache; return rawArgsChannel->transformArgs(keyframe); } KisScalarKeyframeChannel *getChannel(KisScalarKeyframeChannel * Private::*field, const KoID &channelId, KisDefaultBoundsBaseSP defaultBounds) { KisScalarKeyframeChannel *channel = this->*field; if (!channel) { channel = this->*field = new KisScalarKeyframeChannel(channelId, -qInf(), qInf(), defaultBounds, KisKeyframe::Linear); } return channel; } }; KisAnimatedTransformMaskParameters::KisAnimatedTransformMaskParameters() : KisTransformMaskAdapter(ToolTransformArgs()), m_d(new Private()) { } KisAnimatedTransformMaskParameters::KisAnimatedTransformMaskParameters(const KisTransformMaskAdapter *staticTransform) : KisTransformMaskAdapter(staticTransform->transformArgs()), m_d(new Private()) { m_d->argsCache = staticTransform->transformArgs(); } KisAnimatedTransformMaskParameters::~KisAnimatedTransformMaskParameters() {} QPointF getInterpolatedPoint(QPointF def, KisScalarKeyframeChannel *xChannel, KisScalarKeyframeChannel *yChannel) { if (xChannel) { qreal x = xChannel->currentValue(); if (!qIsNaN(x)) def.setX(x); } if (yChannel) { qreal y = yChannel->currentValue(); if (!qIsNaN(y)) def.setY(y); } return def; } qreal getInterpolatedValue(KisScalarKeyframeChannel *channel, qreal defaultValue) { if (!channel) return defaultValue; qreal value = channel->currentValue(); if (qIsNaN(value)) return defaultValue; return value; } const ToolTransformArgs &KisAnimatedTransformMaskParameters::transformArgs() const { m_d->argsCache = m_d->currentRawArgs(); QPointF pos = getInterpolatedPoint(m_d->argsCache.transformedCenter(), m_d->positionXchannel, m_d->positionYchannel); m_d->argsCache.setTransformedCenter(pos); m_d->argsCache.setScaleX(getInterpolatedValue(m_d->scaleXchannel, m_d->argsCache.scaleX())); m_d->argsCache.setScaleY(getInterpolatedValue(m_d->scaleYchannel, m_d->argsCache.scaleY())); m_d->argsCache.setShearX(getInterpolatedValue(m_d->shearXchannel, m_d->argsCache.shearX())); m_d->argsCache.setShearY(getInterpolatedValue(m_d->shearYchannel, m_d->argsCache.shearY())); m_d->argsCache.setAX(normalizeAngle(getInterpolatedValue(m_d->rotationXchannel, m_d->argsCache.aX()))); m_d->argsCache.setAY(normalizeAngle(getInterpolatedValue(m_d->rotationYchannel, m_d->argsCache.aY()))); m_d->argsCache.setAZ(normalizeAngle(getInterpolatedValue(m_d->rotationZchannel, m_d->argsCache.aZ()))); return m_d->argsCache; } QString KisAnimatedTransformMaskParameters::id() const { return "animatedtransformparams"; } void KisAnimatedTransformMaskParameters::toXML(QDomElement *e) const { Q_UNUSED(e); } KisTransformMaskParamsInterfaceSP KisAnimatedTransformMaskParameters::fromXML(const QDomElement &e) { Q_UNUSED(e); return toQShared(new KisAnimatedTransformMaskParameters()); } KisTransformMaskParamsInterfaceSP KisAnimatedTransformMaskParameters::animate(KisTransformMaskParamsInterfaceSP params) { KisTransformMaskParamsInterface *animatedParams; KisTransformMaskAdapter *tma = dynamic_cast(params.data()); if (tma) { animatedParams = new KisAnimatedTransformMaskParameters(tma); } else { animatedParams = new KisAnimatedTransformMaskParameters(); } return toQShared(animatedParams); } void KisAnimatedTransformMaskParameters::translate(const QPointF &offset) { ToolTransformArgs &args = m_d->currentRawArgs(); args.translate(offset); } KisKeyframeChannel *KisAnimatedTransformMaskParameters::getKeyframeChannel(const QString &id, KisDefaultBoundsBaseSP defaultBounds) { if (id == KisKeyframeChannel::TransformArguments.id()) { if (!m_d->rawArgsChannel) { m_d->rawArgsChannel = new KisTransformArgsKeyframeChannel(KisKeyframeChannel::TransformArguments, defaultBounds, m_d->currentRawArgs()); } return m_d->rawArgsChannel; } else { KisScalarKeyframeChannel * Private::*field = 0; KoID channelId; if (id == KisKeyframeChannel::TransformPositionX.id()) { channelId = KisKeyframeChannel::TransformPositionX; field = &Private::positionXchannel; } else if (id == KisKeyframeChannel::TransformPositionY.id()) { channelId = KisKeyframeChannel::TransformPositionY; field = &Private::positionYchannel; } else if (id == KisKeyframeChannel::TransformScaleX.id()) { channelId = KisKeyframeChannel::TransformScaleX; field = &Private::scaleXchannel; } else if (id == KisKeyframeChannel::TransformScaleY.id()) { channelId = KisKeyframeChannel::TransformScaleY; field = &Private::scaleYchannel; } else if (id == KisKeyframeChannel::TransformShearX.id()) { channelId = KisKeyframeChannel::TransformShearX; field = &Private::shearXchannel; } else if (id == KisKeyframeChannel::TransformShearY.id()) { channelId = KisKeyframeChannel::TransformShearY; field = &Private::shearYchannel; } else if (id == KisKeyframeChannel::TransformRotationX.id()) { channelId = KisKeyframeChannel::TransformRotationX; field = &Private::rotationXchannel; } else if (id == KisKeyframeChannel::TransformRotationY.id()) { channelId = KisKeyframeChannel::TransformRotationY; field = &Private::rotationYchannel; } else if (id == KisKeyframeChannel::TransformRotationZ.id()) { channelId = KisKeyframeChannel::TransformRotationZ; field = &Private::rotationZchannel; } if (field) { return m_d->getChannel(field, channelId, defaultBounds); } } return 0; } bool KisAnimatedTransformMaskParameters::isHidden() const { return m_d->hidden; } void KisAnimatedTransformMaskParameters::setHidden(bool hidden) { m_d->hidden = hidden; } void KisAnimatedTransformMaskParameters::clearChangedFlag() { int currentTime = (m_d->rawArgsChannel) ? m_d->rawArgsChannel->currentTime() : 0; - KisTimeRange validRange = KisTimeRange::infinite(0); + KisFrameSet validRange = KisFrameSet::infiniteFrom(0); if (m_d->rawArgsChannel) validRange &= m_d->rawArgsChannel->identicalFrames(currentTime); if (m_d->positionXchannel) validRange &= m_d->positionXchannel->identicalFrames(currentTime); if (m_d->positionYchannel) validRange &= m_d->positionYchannel->identicalFrames(currentTime); m_d->validRange = validRange; } bool KisAnimatedTransformMaskParameters::hasChanged() const { int currentTime = (m_d->rawArgsChannel) ? m_d->rawArgsChannel->currentTime() : 0; bool valid = m_d->validRange.contains(currentTime); return !valid; } bool KisAnimatedTransformMaskParameters::isAnimated() const { return true; } void setScalarChannelValue(KisTransformMaskSP mask, const KoID &channelId, int time, qreal value, KUndo2Command *parentCommand) { KisScalarKeyframeChannel *channel = dynamic_cast(mask->getKeyframeChannel(channelId.id(), true)); KIS_ASSERT_RECOVER_RETURN(channel); new KisScalarKeyframeChannel::AddKeyframeCommand(channel, time, value, parentCommand); } void KisAnimatedTransformMaskParameters::addKeyframes(KisTransformMaskSP mask, int time, KisTransformMaskParamsInterfaceSP params, KUndo2Command *parentCommand) { KisTransformMaskParamsInterfaceSP currentParams = mask->transformParams(); if (dynamic_cast(currentParams.data()) == 0) { mask->setTransformParams(animate(currentParams)); } if (params.isNull()) { params = currentParams; } ToolTransformArgs args; auto *adapterParams = dynamic_cast(params.data()); if (adapterParams) { args = adapterParams->transformArgs(); } else { if (params->isHidden()) return; args.setOriginalCenter(mask->exactBounds().center()); args.setTransformedCenter(args.originalCenter()); // offset? } KisTransformArgsKeyframeChannel *rawArgsChannel = dynamic_cast(mask->getKeyframeChannel(KisKeyframeChannel::TransformArguments.id(), true)); if (rawArgsChannel) { new KisTransformArgsKeyframeChannel::AddKeyframeCommand(rawArgsChannel, time, args, parentCommand); } setScalarChannelValue(mask, KisKeyframeChannel::TransformPositionX, time, args.transformedCenter().x(), parentCommand); setScalarChannelValue(mask, KisKeyframeChannel::TransformPositionY, time, args.transformedCenter().y(), parentCommand); setScalarChannelValue(mask, KisKeyframeChannel::TransformScaleX, time, args.scaleX(), parentCommand); setScalarChannelValue(mask, KisKeyframeChannel::TransformScaleY, time, args.scaleY(), parentCommand); setScalarChannelValue(mask, KisKeyframeChannel::TransformShearX, time, args.shearX(), parentCommand); setScalarChannelValue(mask, KisKeyframeChannel::TransformShearY, time, args.shearY(), parentCommand); setScalarChannelValue(mask, KisKeyframeChannel::TransformRotationX, time, args.aX(), parentCommand); setScalarChannelValue(mask, KisKeyframeChannel::TransformRotationY, time, args.aY(), parentCommand); setScalarChannelValue(mask, KisKeyframeChannel::TransformRotationZ, time, args.aZ(), parentCommand); } #include "kis_transform_mask_params_factory_registry.h" struct AnimatedTransformParamsRegistrar { AnimatedTransformParamsRegistrar() { KisTransformMaskParamsFactory f(KisAnimatedTransformMaskParameters::fromXML); KisTransformMaskParamsFactoryRegistry::instance()->addFactory("animatedtransformparams", f); KisAnimatedTransformMaskParamsFactory a(KisAnimatedTransformMaskParameters::animate); KisTransformMaskParamsFactoryRegistry::instance()->setAnimatedParamsFactory(a); KisTransformMaskKeyframeFactory k(KisAnimatedTransformMaskParameters::addKeyframes); KisTransformMaskParamsFactoryRegistry::instance()->setKeyframeFactory(k); } }; static AnimatedTransformParamsRegistrar __animatedTransformParamsRegistrar;