Merge branch 'export-with-play-subtags' (#4173, #4211)

This commit is contained in:
David Capello 2024-02-07 10:41:03 -03:00
commit 9318ce4941
27 changed files with 1027 additions and 196 deletions

View File

@ -546,6 +546,7 @@
<option id="ani_dir" type="doc::AniDir" default="doc::AniDir::FORWARD" />
<option id="apply_pixel_ratio" type="bool" default="false" />
<option id="for_twitter" type="bool" default="false" />
<option id="play_subtags" type="bool" default="true" />
</section>
<section id="sprite_sheet">
<option id="defined" type="bool" default="false" />

View File

@ -657,6 +657,7 @@ area = Area:
layers = Layers:
frames = Frames:
anidir = Animation Direction:
play_subtags = Play Subtags && Repetitions
pixel_ratio = Apply pixel ratio
for_twitter = Export for Twitter
for_twitter_tooltip = Adjust the duration of the last frame to 1/4 so\nTwitter reproduces the animation correctly

View File

@ -37,6 +37,8 @@
<label id="anidir_label" text="@.anidir" />
<combobox id="anidir" text="" cell_align="horizontal" cell_hspan="2" />
<check id="play_subtags" text="@.play_subtags" cell_hspan="3" />
<check id="pixel_ratio" text="@.pixel_ratio" cell_hspan="3" />
<hbox cell_hspan="3">

View File

@ -31,7 +31,7 @@ FileOpROI CliOpenFile::roi() const
document->sprite()->bounds(),
slice,
tag,
selFrames,
FramesSequence(selFrames),
true);
}

View File

@ -126,7 +126,7 @@ void PreviewCliDelegate::saveFile(Context* ctx, const CliOpenFile& cof)
}
if (cof.hasFrameRange()) {
const auto& selFrames = cof.roi().selectedFrames();
const auto& selFrames = cof.roi().framesSequence();
if (!selFrames.empty()) {
if (selFrames.ranges() == 1)
std::cout << " - Frame range from "

View File

@ -100,11 +100,11 @@ void SaveFileBaseCommand::onLoadParams(const Params& params)
this->params().toFrame.isSet()) {
doc::frame_t fromFrame = this->params().fromFrame();
doc::frame_t toFrame = this->params().toFrame();
m_selFrames.insert(fromFrame, toFrame);
m_framesSeq.insert(fromFrame, toFrame);
m_adjustFramesByTag = true;
}
else {
m_selFrames.clear();
m_framesSeq.clear();
m_adjustFramesByTag = false;
}
}
@ -212,21 +212,6 @@ void SaveFileBaseCommand::saveDocumentInBackground(
}
#endif // ENABLE_UI
if (params().aniDir.isSet()) {
switch (params().aniDir()) {
case AniDir::REVERSE:
m_selFrames = m_selFrames.makeReverse();
break;
case AniDir::PING_PONG:
m_selFrames = m_selFrames.makePingPong();
break;
case AniDir::PING_PONG_REVERSE:
m_selFrames = m_selFrames.makePingPong();
m_selFrames = m_selFrames.makeReverse();
break;
}
}
gfx::Rect bounds;
if (params().bounds.isSet()) {
// Export the specific given bounds (e.g. the selection bounds)
@ -239,7 +224,7 @@ void SaveFileBaseCommand::saveDocumentInBackground(
FileOpROI roi(document, bounds,
params().slice(), params().tag(),
m_selFrames, m_adjustFramesByTag);
m_framesSeq, m_adjustFramesByTag);
std::unique_ptr<FileOp> fop(
FileOp::createSaveDocumentOperation(
@ -385,6 +370,7 @@ void SaveFileCopyAsCommand::onExecute(Context* context)
double scale = params().scale();
gfx::Rect bounds = params().bounds();
doc::AniDir aniDirValue = params().aniDir();
bool isPlaySubtags = params().playSubtags();
bool isForTwitter = false;
#if ENABLE_UI
@ -462,6 +448,7 @@ void SaveFileCopyAsCommand::onExecute(Context* context)
applyPixelRatio = win.applyPixelRatio();
aniDirValue = win.aniDirValue();
isForTwitter = win.isForTwitter();
isPlaySubtags = win.isPlaySubtags();
}
#endif
@ -518,14 +505,14 @@ void SaveFileCopyAsCommand::onExecute(Context* context)
// m_selFrames is not empty if fromFrame/toFrame parameters are
// specified.
if (m_selFrames.empty()) {
// Selected frames to export
SelectedFrames selFrames;
Tag* tag = calculate_selected_frames(
site, frames, selFrames);
if (m_framesSeq.empty()) {
// Frames sequence to export
FramesSequence framesSeq;
Tag* tag = calculate_frames_sequence(
site, frames, framesSeq, isPlaySubtags, aniDirValue);
if (tag)
params().tag(tag->name());
m_selFrames = selFrames;
m_framesSeq = framesSeq;
}
m_adjustFramesByTag = false;
}
@ -534,6 +521,7 @@ void SaveFileCopyAsCommand::onExecute(Context* context)
params().aniDir(aniDirValue);
if (!bounds.isEmpty())
params().bounds(bounds);
params().playSubtags(isPlaySubtags);
// TODO This should be set as options for the specific encoder
GifEncoderDurationFix fixGif(isForTwitter);

View File

@ -12,7 +12,7 @@
#include "app/commands/command.h"
#include "app/commands/new_params.h"
#include "doc/anidir.h"
#include "doc/selected_frames.h"
#include "doc/frames_sequence.h"
#include "gfx/point.h"
#include "gfx/rect.h"
@ -33,6 +33,7 @@ namespace app {
Param<bool> ignoreEmpty { this, false, "ignoreEmpty" };
Param<double> scale { this, 1.0, "scale" };
Param<gfx::Rect> bounds { this, gfx::Rect(), "bounds" };
Param<bool> playSubtags { this, false, "playSubtags" };
};
class SaveFileBaseCommand : public CommandWithNewParams<SaveFileParams> {
@ -62,7 +63,7 @@ namespace app {
const ResizeOnTheFly resizeOnTheFly = ResizeOnTheFly::Off,
const gfx::PointF& scale = gfx::PointF(1.0, 1.0));
doc::SelectedFrames m_selFrames;
doc::FramesSequence m_framesSeq;
bool m_adjustFramesByTag;
};

View File

@ -364,7 +364,7 @@ bool AseFormat::onSave(FileOp* fop)
// Write frames
int outputFrame = 0;
dio::AsepriteExternalFiles ext_files;
for (frame_t frame : fop->roi().selectedFrames()) {
for (frame_t frame : fop->roi().framesSequence()) {
// Prepare the frame header
dio::AsepriteFrameHeader frame_header;
ase_file_prepare_frame_header(f, &frame_header);
@ -1361,7 +1361,7 @@ static void ase_file_write_external_files_chunk(
layers.insert(layers.end(), childLayers.begin(), childLayers.end());
}
else if (layer->isImage()) {
for (frame_t frame : fop->roi().selectedFrames()) {
for (frame_t frame : fop->roi().framesSequence()) {
const Cel* cel = layer->cel(frame);
if (cel && !cel->link()) {
putExtentionIds(cel->data()->userData().propertiesMaps(), ext_files);

View File

@ -258,7 +258,7 @@ int save_document(Context* context, Doc* document)
FileOp::createSaveDocumentOperation(
context,
FileOpROI(document, document->sprite()->bounds(),
"", "", SelectedFrames(), false),
"", "", FramesSequence(), false),
document->filename(), "",
false));
if (!fop)
@ -307,13 +307,13 @@ FileOpROI::FileOpROI(const Doc* doc,
const gfx::Rect& bounds,
const std::string& sliceName,
const std::string& tagName,
const doc::SelectedFrames& selFrames,
const doc::FramesSequence& framesSeq,
const bool adjustByTag)
: m_document(doc)
, m_bounds(bounds)
, m_slice(nullptr)
, m_tag(nullptr)
, m_selFrames(selFrames)
, m_framesSeq(framesSeq)
{
if (doc) {
if (!sliceName.empty())
@ -324,18 +324,18 @@ FileOpROI::FileOpROI(const Doc* doc,
m_tag = doc->sprite()->tags().getByName(tagName);
if (m_tag) {
if (m_selFrames.empty())
m_selFrames.insert(m_tag->fromFrame(), m_tag->toFrame());
if (m_framesSeq.empty())
m_framesSeq.insert(m_tag->fromFrame(), m_tag->toFrame());
else if (adjustByTag)
m_selFrames.displace(m_tag->fromFrame());
m_framesSeq.displace(m_tag->fromFrame());
m_selFrames =
m_selFrames.filter(std::max(0, m_tag->fromFrame()),
m_framesSeq =
m_framesSeq.filter(std::max(0, m_tag->fromFrame()),
std::min(m_tag->toFrame(), doc->sprite()->lastFrame()));
}
// All frames if selected frames is empty
else if (m_selFrames.empty())
m_selFrames.insert(0, doc->sprite()->lastFrame());
else if (m_framesSeq.empty())
m_framesSeq.insert(0, doc->sprite()->lastFrame());
}
}
@ -360,7 +360,7 @@ gfx::Size FileOpROI::fileCanvasSize() const
{
if (m_slice) {
gfx::Size size;
for (auto frame : m_selFrames)
for (auto frame : m_framesSeq)
size |= frameBounds(frame).size();
return size;
}
@ -759,7 +759,7 @@ FileOp* FileOp::createSaveDocumentOperation(const Context* context,
frame_t outputFrame = 0;
for (frame_t frame : fop->m_roi.selectedFrames()) {
for (frame_t frame : fop->m_roi.framesSequence()) {
Tag* innerTag = (fop->m_roi.tag() ? fop->m_roi.tag(): sprite->tags().innerTag(frame));
Tag* outerTag = (fop->m_roi.tag() ? fop->m_roi.tag(): sprite->tags().outerTag(frame));
FilenameInfo fnInfo;
@ -1027,7 +1027,7 @@ void FileOp::operate(IFileOpProgress* progress)
render.setNewBlend(m_config.newBlend);
frame_t outputFrame = 0;
for (frame_t frame : m_roi.selectedFrames()) {
for (frame_t frame : m_roi.framesSequence()) {
gfx::Rect bounds = m_roi.frameBounds(frame);
if (bounds.isEmpty())
continue; // Skip frame because there is no slice key

View File

@ -18,7 +18,7 @@
#include "doc/frame.h"
#include "doc/image_ref.h"
#include "doc/pixel_format.h"
#include "doc/selected_frames.h"
#include "doc/frames_sequence.h"
#include "os/color_space.h"
#include <cstdio>
@ -75,18 +75,18 @@ namespace app {
const gfx::Rect& bounds,
const std::string& sliceName,
const std::string& tagName,
const doc::SelectedFrames& selFrames,
const doc::FramesSequence& frames,
const bool adjustByTag);
const Doc* document() const { return m_document; }
doc::Slice* slice() const { return m_slice; }
doc::Tag* tag() const { return m_tag; }
doc::frame_t fromFrame() const { return m_selFrames.firstFrame(); }
doc::frame_t toFrame() const { return m_selFrames.lastFrame(); }
const doc::SelectedFrames& selectedFrames() const { return m_selFrames; }
doc::frame_t fromFrame() const { return m_framesSeq.firstFrame(); }
doc::frame_t toFrame() const { return m_framesSeq.lastFrame(); }
const doc::FramesSequence& framesSequence() const { return m_framesSeq; }
doc::frame_t frames() const {
return (doc::frame_t)m_selFrames.size();
return (doc::frame_t)m_framesSeq.size();
}
// Returns an empty rectangle only when exporting a slice and the
@ -103,7 +103,7 @@ namespace app {
gfx::Rect m_bounds;
doc::Slice* m_slice;
doc::Tag* m_tag;
doc::SelectedFrames m_selFrames;
doc::FramesSequence m_framesSeq;
};
// Used by file formats with FILE_ENCODE_ABSTRACT_IMAGE flag, to

View File

@ -176,12 +176,12 @@ bool FliFormat::onLoad(FileOp* fop)
#ifdef ENABLE_SAVE
static int get_time_precision(const FileAbstractImage* sprite,
const doc::SelectedFrames& selFrames)
const doc::FramesSequence& framesSeq)
{
// Check if all frames have the same duration
bool constantFrameRate = true;
frame_t prevFrame = -1;
for (frame_t frame : selFrames) {
for (frame_t frame : framesSeq) {
if (prevFrame >= 0) {
if (sprite->frameDuration(prevFrame) != sprite->frameDuration(frame)) {
constantFrameRate = false;
@ -194,7 +194,7 @@ static int get_time_precision(const FileAbstractImage* sprite,
return sprite->frameDuration(0);
int precision = 1000;
for (frame_t frame : selFrames) {
for (frame_t frame : framesSeq) {
int len = sprite->frameDuration(frame);
while (len / precision == 0)
precision /= 10;
@ -218,7 +218,7 @@ bool FliFormat::onSave(FileOp* fop)
header.frames = 0;
header.width = sprite->width();
header.height = sprite->height();
header.speed = get_time_precision(sprite, fop->roi().selectedFrames());
header.speed = get_time_precision(sprite, fop->roi().framesSequence());
encoder.writeHeader(header);
// Create the bitmaps
@ -231,8 +231,8 @@ bool FliFormat::onSave(FileOp* fop)
fliFrame.pixels = bmp->getPixelAddress(0, 0);
fliFrame.rowstride = bmp->rowBytes();
auto frame_beg = fop->roi().selectedFrames().begin();
auto frame_end = fop->roi().selectedFrames().end();
auto frame_beg = fop->roi().framesSequence().begin();
auto frame_end = fop->roi().framesSequence().end();
auto frame_it = frame_beg;
frame_t nframes = fop->roi().frames();
for (frame_t f=0; f<=nframes; ++f, ++frame_it) {

View File

@ -1104,9 +1104,9 @@ public:
m_currentImage = m_images[1].get();
m_nextImage = m_images[2].get();
auto frame_beg = m_fop->roi().selectedFrames().begin();
auto frame_beg = m_fop->roi().framesSequence().begin();
#if _DEBUG
auto frame_end = m_fop->roi().selectedFrames().end();
auto frame_end = m_fop->roi().framesSequence().end();
#endif
auto frame_it = frame_beg;

View File

@ -319,7 +319,7 @@ bool WebPFormat::onSave(FileOp* fop)
WebPAnimEncoder* enc = WebPAnimEncoderNew(w, h, &enc_options);
int timestamp_ms = 0;
for (frame_t frame : fop->roi().selectedFrames()) {
for (frame_t frame : fop->roi().framesSequence()) {
// Render the frame in the bitmap
clear_image(image.get(), image->maskColor());
sprite->renderFrame(frame, fop->roi().frameBounds(frame), image.get());

View File

@ -59,12 +59,13 @@ ExportFileWindow::ExportFileWindow(const Doc* doc)
pixelRatio()->setSelected(m_docPref.saveCopy.applyPixelRatio());
forTwitter()->setSelected(m_docPref.saveCopy.forTwitter());
adjustResize()->setVisible(false);
playSubtags()->setSelected(m_docPref.saveCopy.playSubtags());
// Here we don't call updateAniDir() because it's already filled and
// set by the function fill_anidir_combobox(). So if the user
// exported a tag with a specific AniDir, we want to keep the option
// in the preference (instead of the tag's AniDir).
//updateAniDir();
updatePlaySubtags();
updateAdjustResizeButton();
@ -82,7 +83,10 @@ ExportFileWindow::ExportFileWindow(const Doc* doc)
});
resize()->Change.connect([this]{ updateAdjustResizeButton(); });
frames()->Change.connect([this]{ updateAniDir(); });
frames()->Change.connect([this]{
updateAniDir();
updatePlaySubtags();
});
forTwitter()->Click.connect([this]{ updateAdjustResizeButton(); });
adjustResize()->Click.connect([this]{ onAdjustResize(); });
ok()->Click.connect([this]{ onOK(); });
@ -105,6 +109,7 @@ void ExportFileWindow::savePref()
m_docPref.saveCopy.frameTag(framesValue());
m_docPref.saveCopy.applyPixelRatio(applyPixelRatio());
m_docPref.saveCopy.forTwitter(isForTwitter());
m_docPref.saveCopy.playSubtags(isPlaySubtags());
}
std::string ExportFileWindow::outputFilenameValue() const
@ -145,6 +150,11 @@ doc::AniDir ExportFileWindow::aniDirValue() const
return (doc::AniDir)anidir()->getSelectedItemIndex();
}
bool ExportFileWindow::isPlaySubtags() const
{
return playSubtags()->isSelected() && framesValue() != kSelectedFrames;
}
bool ExportFileWindow::applyPixelRatio() const
{
return pixelRatio()->isSelected();
@ -205,6 +215,13 @@ void ExportFileWindow::updateAniDir()
anidir()->setSelectedItemIndex(int(doc::AniDir::FORWARD));
}
void ExportFileWindow::updatePlaySubtags()
{
std::string framesValue = this->framesValue();
playSubtags()->setVisible(framesValue != kSelectedFrames);
layout();
}
void ExportFileWindow::updateAdjustResizeButton()
{
// Calculate a better size for Twitter

View File

@ -33,6 +33,7 @@ namespace app {
int layersIndex() const;
std::string framesValue() const;
doc::AniDir aniDirValue() const;
bool isPlaySubtags() const;
bool applyPixelRatio() const;
bool isForTwitter() const;
@ -47,6 +48,7 @@ namespace app {
void updateOutputFilenameEntry();
void onOutputFilenameEntryChange();
void updateAniDir();
void updatePlaySubtags();
void updateAdjustResizeButton();
void onAdjustResize();
void onOK();

View File

@ -11,16 +11,19 @@
#include "app/ui/layer_frame_comboboxes.h"
#include "app/doc.h"
#include "app/i18n/strings.h"
#include "app/restore_visible_layers.h"
#include "app/site.h"
#include "doc/anidir.h"
#include "doc/frames_sequence.h"
#include "doc/layer.h"
#include "doc/selected_frames.h"
#include "doc/selected_layers.h"
#include "doc/slice.h"
#include "doc/sprite.h"
#include "doc/tag.h"
#include "doc/playback.h"
#include "ui/combobox.h"
namespace app {
@ -180,6 +183,96 @@ void calculate_visible_layers(const Site& site,
}
}
doc::Tag* calculate_frames_sequence(const Site& site,
const std::string& framesValue,
doc::FramesSequence& framesSeq,
bool playSubtags,
AniDir aniDir)
{
doc::Tag* tag = nullptr;
if (!playSubtags || framesValue == kSelectedFrames) {
SelectedFrames selFrames;
tag = calculate_selected_frames(site, framesValue, selFrames);
framesSeq = FramesSequence(selFrames);
}
else {
frame_t start = 0;
AniDir origAniDir = aniDir;
int forward = 1;
if (framesValue == kAllFrames) {
tag = nullptr;
forward = (aniDir == AniDir::FORWARD || aniDir == AniDir::PING_PONG ? 1 : -1);
if (forward < 0)
start = site.sprite()->lastFrame();
// Look for a tag containing the first frame and determine the start frame
// according to the selected animation direction.
auto startTag = site.sprite()->tags().innerTag(start);
if (startTag) {
int startTagForward = (startTag->aniDir() == AniDir::FORWARD ||
startTag->aniDir() == AniDir::PING_PONG
? 1 : -1);
start = forward * startTagForward > 0
? startTag->fromFrame()
: startTag->toFrame();
}
}
else {
tag = site.sprite()->tags().getByName(framesValue);
// User selected a specific tag, then set its anidir to the selected
// direction. We save the original direction to restore it later.
if (tag) {
origAniDir = tag->aniDir();
tag->setAniDir(aniDir);
start = (aniDir == AniDir::FORWARD || aniDir == AniDir::PING_PONG
? tag->fromFrame()
: tag->toFrame());
}
}
auto playback = doc::Playback(
site.document()->sprite(),
site.document()->sprite()->tags().getInternalList(),
start,
doc::Playback::PlayAll,
tag,
forward);
framesSeq.insert(playback.frame());
auto frame = playback.nextFrame();
while(!playback.isStopped()) {
framesSeq.insert(frame);
frame = playback.nextFrame();
}
if (framesValue == kAllFrames) {
// If the user is playing all frames and selected some of the ping-pong
// animation direction, then modify the animation as needed.
if (aniDir == AniDir::PING_PONG || aniDir == AniDir::PING_PONG_REVERSE)
framesSeq = framesSeq.makePingPong();
}
else if (tag) {
// If exported tag is ping-pong, remove last frame of the sequence to
// avoid playing the first frame twice.
if (aniDir == AniDir::PING_PONG || aniDir == AniDir::PING_PONG_REVERSE) {
doc::FramesSequence newSeq;
int i = 0;
int frames = framesSeq.size()-1;
for (auto f : framesSeq) {
if (i < frames)
newSeq.insert(f);
++i;
}
framesSeq = newSeq;
}
// Restore tag's original animation direction.
tag->setAniDir(origAniDir);
}
}
return tag;
}
doc::Tag* calculate_selected_frames(const Site& site,
const std::string& framesValue,
doc::SelectedFrames& selFrames)

View File

@ -17,6 +17,7 @@
namespace doc {
class Layer;
class SelectedFrames;
class FramesSequence;
class SelectedLayers;
class Slice;
class Sprite;
@ -75,6 +76,11 @@ namespace app {
const int layersIndex,
RestoreVisibleLayers& layersVisibility);
doc::Tag* calculate_frames_sequence(const Site& site,
const std::string& framesValue,
doc::FramesSequence& selFrames,
bool playSubtags,
doc::AniDir aniDir);
doc::Tag* calculate_selected_frames(const Site& site,
const std::string& framesValue,
doc::SelectedFrames& selFrames);

View File

@ -38,6 +38,7 @@ add_library(doc-lib
file/gpl_file.cpp
file/hex_file.cpp
file/pal_file.cpp
frames_sequence.cpp
grid.cpp
grid_io.cpp
image.cpp

View File

@ -62,7 +62,7 @@ namespace doc {
private:
Cel* m_cel;
const SelectedFrames& m_selFrames;
SelectedFrames::const_iterator m_frameIterator;
frames::const_iterator m_frameIterator;
Flags m_flags;
std::set<ObjectId> m_visited;
};

145
src/doc/frames_iterators.h Normal file
View File

@ -0,0 +1,145 @@
// Aseprite Document Library
// Copyright (c) 2024 Igara Studio S.A.
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
#ifndef DOC_FRAMES_ITERATORS_H_INCLUDED
#define DOC_FRAMES_ITERATORS_H_INCLUDED
#pragma once
#include "doc/frame_range.h"
#include <iosfwd>
#include <vector>
namespace doc {
namespace frames {
typedef std::vector<FrameRange> Ranges;
class const_iterator {
static const int kNullFrame = -2;
public:
using iterator_category = std::forward_iterator_tag;
using value_type = frame_t;
using difference_type = std::ptrdiff_t;
using pointer = frame_t*;
using reference = frame_t&;
const_iterator(const frames::Ranges::const_iterator& it)
: m_it(it), m_frame(kNullFrame) {
}
const_iterator& operator++() {
if (m_frame == kNullFrame)
m_frame = m_it->fromFrame;
if (m_it->fromFrame <= m_it->toFrame) {
if (m_frame < m_it->toFrame)
++m_frame;
else {
m_frame = kNullFrame;
++m_it;
}
}
else {
if (m_frame > m_it->toFrame)
--m_frame;
else {
m_frame = kNullFrame;
++m_it;
}
}
return *this;
}
frame_t operator*() const {
if (m_frame == kNullFrame)
m_frame = m_it->fromFrame;
return m_frame;
}
bool operator==(const const_iterator& o) const {
return (m_it == o.m_it && m_frame == o.m_frame);
}
bool operator!=(const const_iterator& it) const {
return !operator==(it);
}
private:
mutable frames::Ranges::const_iterator m_it;
mutable frame_t m_frame;
};
class const_reverse_iterator {
public:
using iterator_category = std::forward_iterator_tag;
using value_type = frame_t;
using difference_type = std::ptrdiff_t;
using pointer = frame_t*;
using reference = frame_t&;
const_reverse_iterator(const frames::Ranges::const_reverse_iterator& it)
: m_it(it), m_frame(-1) {
}
const_reverse_iterator& operator++() {
if (m_frame < 0)
m_frame = m_it->toFrame;
int step = (m_it->fromFrame > m_it->toFrame) -
(m_it->fromFrame < m_it->toFrame);
if ((step < 0 && m_frame > m_it->fromFrame) ||
(step > 0 && m_frame < m_it->fromFrame))
m_frame += step;
else {
m_frame = -1;
++m_it;
}
return *this;
}
frame_t operator*() const {
if (m_frame < 0)
m_frame = m_it->toFrame;
return m_frame;
}
bool operator==(const const_reverse_iterator& o) const {
return (m_it == o.m_it && m_frame == o.m_frame);
}
bool operator!=(const const_reverse_iterator& it) const {
return !operator==(it);
}
private:
mutable frames::Ranges::const_reverse_iterator m_it;
mutable frame_t m_frame;
};
template<typename T>
class Reversed {
public:
typedef const_reverse_iterator const_iterator;
const_iterator begin() const { return m_frames.rbegin(); }
const_iterator end() const { return m_frames.rend(); }
Reversed(const T& frames)
: m_frames(frames) {
}
private:
const T& m_frames;
};
} // namespace frames
} // namespace doc
#endif // DOC_FRAMES_ITERATORS_H_INCLUDED

207
src/doc/frames_sequence.cpp Normal file
View File

@ -0,0 +1,207 @@
// Aseprite Document Library
// Copyright (c) 2023 Igara Studio S.A.
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "doc/frames_sequence.h"
#include "base/base.h"
#include "base/debug.h"
#include "base/serialization.h"
#include <algorithm>
#include <iostream>
namespace doc {
using namespace base::serialization;
using namespace base::serialization::little_endian;
FramesSequence::FramesSequence(const SelectedFrames& selectedFrames)
{
// TODO: This might be optimized by copying each range directly, but
// for this I would need that SelectedFrames make this class its friend.
for(auto frame : selectedFrames) {
this->insert(frame);
}
}
std::size_t FramesSequence::size() const
{
std::size_t size = 0;
for (auto& range : m_ranges)
size += ABS(range.toFrame - range.fromFrame) + 1;
return size;
}
void FramesSequence::clear()
{
m_ranges.clear();
}
void FramesSequence::insert(frame_t frame)
{
ASSERT(frame >= 0);
if (m_ranges.empty()) {
m_ranges.push_back(FrameRange(frame));
return;
}
auto& lastRange = m_ranges.back();
int rangeStep = (lastRange.fromFrame < lastRange.toFrame) -
(lastRange.fromFrame > lastRange.toFrame);
int step = (lastRange.toFrame < frame) - (lastRange.toFrame > frame);
if (step &&
(rangeStep == step || !rangeStep) &&
frame-lastRange.toFrame == step)
lastRange.toFrame = frame;
else
m_ranges.push_back(FrameRange(frame));
}
void FramesSequence::insert(frame_t fromFrame, frame_t toFrame)
{
int step = (fromFrame <= toFrame ? 1 : -1);
for (frame_t frame = fromFrame; frame != toFrame; frame+=step) {
insert(frame);
}
insert(toFrame);
}
FramesSequence FramesSequence::filter(frame_t fromFrame, frame_t toFrame) const
{
FramesSequence f;
if (fromFrame > toFrame)
std::swap(fromFrame, toFrame);
for (const auto& range : m_ranges) {
FrameRange r(range);
const bool isForward = (r.fromFrame <= r.toFrame);
if (isForward) {
if (r.fromFrame < fromFrame) r.fromFrame = fromFrame;
if (r.toFrame > toFrame) r.toFrame = toFrame;
}
else {
if (r.fromFrame > toFrame) r.fromFrame = toFrame;
if (r.toFrame < fromFrame) r.toFrame = fromFrame;
}
if (( isForward && r.fromFrame <= r.toFrame) ||
(!isForward && r.fromFrame >= r.toFrame))
f.m_ranges.push_back(r);
}
return f;
}
bool FramesSequence::contains(frame_t frame) const
{
return std::find_if(
m_ranges.begin(),
m_ranges.end(),
[frame](const FrameRange& r) -> bool {
return (r.fromFrame <= frame && frame <= r.toFrame) ||
(r.fromFrame >= frame && frame >= r.toFrame);
}) != m_ranges.end();
}
frame_t FramesSequence::lowestFrame() const
{
frame_t lof = this->firstFrame();
for (auto range : m_ranges) {
lof = std::min<frame_t>(range.fromFrame, std::min<frame_t>(range.toFrame, lof));
}
return lof;
}
void FramesSequence::displace(frame_t frameDelta)
{
// Avoid setting negative numbers in frame ranges
auto lof = lowestFrame();
if (lof+frameDelta < 0)
frameDelta = -lof;
for (auto& range : m_ranges) {
range.fromFrame += frameDelta;
range.toFrame += frameDelta;
ASSERT(range.fromFrame >= 0);
ASSERT(range.toFrame >= 0);
}
}
FramesSequence FramesSequence::makeReverse() const
{
FramesSequence newFrames;
for (const FrameRange& range : m_ranges)
newFrames.m_ranges.insert(
newFrames.m_ranges.begin(),
FrameRange(range.toFrame, range.fromFrame));
return newFrames;
}
FramesSequence FramesSequence::makePingPong() const
{
FramesSequence newFrames = *this;
const int n = m_ranges.size();
int i = 0;
int j = m_ranges.size()-1;
for (const FrameRange& range : m_ranges) {
// Discard first or last range if it contains just one frame.
if (range.toFrame == range.fromFrame && (i == 0 || j == 0)) {
++i;
--j;
continue;
}
FrameRange reversedRange(range.toFrame,
range.fromFrame);
int step = (range.fromFrame < range.toFrame) -
(range.fromFrame > range.toFrame);
if (i == 0) reversedRange.toFrame+=step;
if (j == 0) reversedRange.fromFrame-=step;
if (reversedRange.fromFrame != reversedRange.toFrame)
newFrames.m_ranges.insert(
newFrames.m_ranges.begin() + n,
reversedRange);
++i;
--j;
}
return newFrames;
}
bool FramesSequence::write(std::ostream& os) const
{
write32(os, size());
for (const frame_t frame : *this) {
write32(os, frame);
}
return os.good();
}
bool FramesSequence::read(std::istream& is)
{
clear();
int nframes = read32(is);
for (int i=0; i<nframes && is; ++i) {
frame_t frame = read32(is);
insert(frame);
}
return is.good();
}
} // namespace doc

69
src/doc/frames_sequence.h Normal file
View File

@ -0,0 +1,69 @@
// Aseprite Document Library
// Copyright (c) 2023 Igara Studio S.A.
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
#ifndef DOC_FRAMES_SEQUENCE_H_INCLUDED
#define DOC_FRAMES_SEQUENCE_H_INCLUDED
#pragma once
#include "doc/selected_frames.h"
using namespace doc::frames;
namespace doc {
// This class is based in several code of the SelectedFrames class.
// TODO: At some point we should remove the duplicated code between these
// classes.
class FramesSequence {
public:
FramesSequence() {}
FramesSequence(const SelectedFrames& selectedFrames);
const_iterator begin() const { return const_iterator(m_ranges.begin()); }
const_iterator end() const { return const_iterator(m_ranges.end()); }
const_reverse_iterator rbegin() const { return const_reverse_iterator(m_ranges.rbegin()); }
const_reverse_iterator rend() const { return const_reverse_iterator(m_ranges.rend()); }
std::size_t size() const;
std::size_t ranges() const { return m_ranges.size(); }
bool empty() const { return m_ranges.empty(); }
void clear();
void insert(frame_t frame);
void insert(frame_t fromFrame, frame_t toFrame);
FramesSequence filter(frame_t fromFrame, frame_t toFrame) const;
bool contains(frame_t frame) const;
frame_t firstFrame() const { return (!m_ranges.empty() ? m_ranges.front().fromFrame: -1); }
frame_t lastFrame() const { return (!m_ranges.empty() ? m_ranges.back().toFrame: -1); }
frame_t lowestFrame() const;
void displace(frame_t frameDelta);
Reversed<FramesSequence> reversed() const { return Reversed(*this); }
FramesSequence makeReverse() const;
FramesSequence makePingPong() const;
bool operator==(const FramesSequence& o) const {
return m_ranges == o.m_ranges;
}
bool operator!=(const FramesSequence& o) const {
return !operator==(o);
}
bool write(std::ostream& os) const;
bool read(std::istream& is);
private:
Ranges m_ranges;
};
} // namespace doc
#endif // DOC_FRAMES_SEQUENCE_H_INCLUDED

View File

@ -0,0 +1,352 @@
// Aseprite Document Library
// Copyright (c) 2023 Igara Studio S.A.
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <gtest/gtest.h>
#include "doc/frames_sequence.h"
#include <algorithm>
#include <iterator>
using namespace doc;
static std::vector<frame_t> to_vector(const FramesSequence& f)
{
std::vector<frame_t> v;
std::copy(f.begin(), f.end(), std::back_inserter(v));
return v;
}
TEST(FramesSequence, BasicOneRange)
{
FramesSequence f;
EXPECT_TRUE(f.empty());
f.insert(1);
f.insert(2);
f.insert(3);
EXPECT_FALSE(f.empty());
EXPECT_EQ(3, f.size());
EXPECT_EQ(1, f.ranges());
auto res = to_vector(f);
ASSERT_EQ(3, res.size());
EXPECT_EQ(1, res[0]);
EXPECT_EQ(2, res[1]);
EXPECT_EQ(3, res[2]);
}
TEST(FramesSequence, BasicThreeRanges)
{
FramesSequence f;
f.insert(1);
f.insert(3);
f.insert(5);
EXPECT_EQ(3, f.size());
EXPECT_EQ(3, f.ranges());
auto res = to_vector(f);
ASSERT_EQ(3, res.size());
EXPECT_EQ(1, res[0]);
EXPECT_EQ(3, res[1]);
EXPECT_EQ(5, res[2]);
}
TEST(FramesSequence, InsertDecreasingFramesSequenceAfterIncreasingSequence)
{
FramesSequence f;
f.insert(3);
f.insert(5, 8);
f.insert(7, 4);
f.insert(3);
f.insert(1);
f.insert(2);
EXPECT_EQ(12, f.size());
EXPECT_EQ(4, f.ranges());
EXPECT_EQ(3, f.firstFrame());
EXPECT_EQ(2, f.lastFrame());
EXPECT_EQ(1, f.lowestFrame());
auto res = to_vector(f);
ASSERT_EQ(12, res.size());
EXPECT_EQ(3, res[0]);
EXPECT_EQ(5, res[1]);
EXPECT_EQ(6, res[2]);
EXPECT_EQ(7, res[3]);
EXPECT_EQ(8, res[4]);
EXPECT_EQ(7, res[5]);
EXPECT_EQ(6, res[6]);
EXPECT_EQ(5, res[7]);
EXPECT_EQ(4, res[8]);
EXPECT_EQ(3, res[9]);
EXPECT_EQ(1, res[10]);
EXPECT_EQ(2, res[11]);
}
TEST(FramesSequence, Contains)
{
FramesSequence f;
f.insert(1);
f.insert(4, 5);
f.insert(9, 7);
EXPECT_EQ(6, f.size());
EXPECT_EQ(3, f.ranges());
EXPECT_FALSE(f.contains(0));
EXPECT_TRUE(f.contains(1));
EXPECT_FALSE(f.contains(2));
EXPECT_FALSE(f.contains(3));
EXPECT_TRUE(f.contains(4));
EXPECT_TRUE(f.contains(5));
EXPECT_FALSE(f.contains(6));
EXPECT_TRUE(f.contains(7));
EXPECT_TRUE(f.contains(8));
EXPECT_TRUE(f.contains(9));
EXPECT_FALSE(f.contains(10));
}
TEST(FramesSequence, ReverseIterators)
{
FramesSequence f;
f.insert(1);
f.insert(5, 7);
f.insert(8, 2);
EXPECT_EQ(11, f.size());
EXPECT_EQ(3, f.ranges());
std::vector<frame_t> res;
std::copy(f.rbegin(), f.rend(), std::back_inserter(res));
ASSERT_EQ(11, res.size());
EXPECT_EQ(2, res[0]);
EXPECT_EQ(3, res[1]);
EXPECT_EQ(4, res[2]);
EXPECT_EQ(5, res[3]);
EXPECT_EQ(6, res[4]);
EXPECT_EQ(7, res[5]);
EXPECT_EQ(8, res[6]);
EXPECT_EQ(7, res[7]);
EXPECT_EQ(6, res[8]);
EXPECT_EQ(5, res[9]);
EXPECT_EQ(1, res[10]);
std::vector<frame_t> res2;
for (frame_t frame : f.reversed())
res2.push_back(frame);
EXPECT_EQ(res, res2);
}
TEST(FramesSequence, MakeReverseSimple)
{
FramesSequence f;
f.insert(4, 9);
EXPECT_EQ(6, f.size());
EXPECT_EQ(1, f.ranges());
f = f.makeReverse();
EXPECT_EQ(6, f.size());
EXPECT_EQ(1, f.ranges());
auto res = to_vector(f);
ASSERT_EQ(6, res.size());
EXPECT_EQ(9, res[0]);
EXPECT_EQ(8, res[1]);
EXPECT_EQ(7, res[2]);
EXPECT_EQ(6, res[3]);
EXPECT_EQ(5, res[4]);
EXPECT_EQ(4, res[5]);
}
TEST(FramesSequence, MakeReverse)
{
FramesSequence f;
f.insert(1);
f.insert(4, 5);
f.insert(5, 1);
f.insert(7);
f.insert(9, 5);
EXPECT_EQ(14, f.size());
EXPECT_EQ(5, f.ranges());
f = f.makeReverse();
EXPECT_EQ(5, f.ranges());
auto res = to_vector(f);
ASSERT_EQ(14, res.size());
EXPECT_EQ(5, res[0]);
EXPECT_EQ(6, res[1]);
EXPECT_EQ(7, res[2]);
EXPECT_EQ(8, res[3]);
EXPECT_EQ(9, res[4]);
EXPECT_EQ(7, res[5]);
EXPECT_EQ(1, res[6]);
EXPECT_EQ(2, res[7]);
EXPECT_EQ(3, res[8]);
EXPECT_EQ(4, res[9]);
EXPECT_EQ(5, res[10]);
EXPECT_EQ(5, res[11]);
EXPECT_EQ(4, res[12]);
EXPECT_EQ(1, res[13]);
}
TEST(FramesSequence, MakePingPongAndFilter)
{
FramesSequence f;
f.insert(1);
f.insert(4, 5);
f.insert(9, 7);
f.insert(8);
EXPECT_EQ(7, f.size());
EXPECT_EQ(4, f.ranges());
f = f.makePingPong();
EXPECT_EQ(6, f.ranges());
auto res = to_vector(f);
ASSERT_EQ(12, res.size());
EXPECT_EQ(1, res[0]);
EXPECT_EQ(4, res[1]);
EXPECT_EQ(5, res[2]);
EXPECT_EQ(9, res[3]);
EXPECT_EQ(8, res[4]);
EXPECT_EQ(7, res[5]);
EXPECT_EQ(8, res[6]);
EXPECT_EQ(7, res[7]);
EXPECT_EQ(8, res[8]);
EXPECT_EQ(9, res[9]);
EXPECT_EQ(5, res[10]);
EXPECT_EQ(4, res[11]);
f = f.filter(5, 8);
EXPECT_EQ(5, f.ranges());
res = to_vector(f);
EXPECT_EQ(7, res.size());
EXPECT_EQ(5, res[0]);
EXPECT_EQ(8, res[1]);
EXPECT_EQ(7, res[2]);
EXPECT_EQ(8, res[3]);
EXPECT_EQ(7, res[4]);
EXPECT_EQ(8, res[5]);
EXPECT_EQ(5, res[6]);
f = f.filter(7, 7);
EXPECT_EQ(2, f.ranges());
res = to_vector(f);
EXPECT_EQ(2, res.size());
EXPECT_EQ(7, res[0]);
EXPECT_EQ(7, res[1]);
}
TEST(FramesSequence, RangeMerging)
{
FramesSequence f;
f.insert(1);
f.insert(2, 3);
f.insert(4);
f.insert(5, 7);
f.insert(8);
f.insert(9, 5);
f.insert(4);
f.insert(3, 2);
f.insert(1);
EXPECT_EQ(17, f.size());
EXPECT_EQ(2, f.ranges());
f.clear();
f.insert(10, 8);
f.insert(7);
f.insert(6, 4);
f.insert(3);
f.insert(2, 1);
f.insert(2);
f.insert(3, 5);
f.insert(6);
f.insert(7, 8);
f.insert(9, 10);
EXPECT_EQ(19, f.size());
EXPECT_EQ(2, f.ranges());
}
TEST(FramesSequence, SelectedFramesConversion)
{
SelectedFrames sf;
sf.insert(3);
sf.insert(5, 8);
sf.insert(7);
sf.insert(9);
EXPECT_EQ(6, sf.size());
EXPECT_EQ(2, sf.ranges());
EXPECT_EQ(3, sf.firstFrame());
EXPECT_EQ(9, sf.lastFrame());
FramesSequence f(sf);
EXPECT_EQ(sf.size(), f.size());
EXPECT_EQ(sf.ranges(), f.ranges());
EXPECT_EQ(sf.firstFrame(), f.firstFrame());
EXPECT_EQ(sf.lastFrame(), f.lastFrame());
auto ressf = to_vector(sf);
auto resf = to_vector(f);
EXPECT_EQ(ressf.size(), resf.size());
for (int i=0; i < resf.size(); ++i) {
EXPECT_EQ(ressf[i], resf[i]);
}
}
TEST(FramesSequence, Displace)
{
FramesSequence f;
f.insert(1);
f.insert(4, 5);
f.insert(9, 7);
f.insert(8);
EXPECT_EQ(7, f.size());
EXPECT_EQ(4, f.ranges());
f.displace(4);
auto res = to_vector(f);
ASSERT_EQ(7, res.size());
EXPECT_EQ(5, res[0]);
EXPECT_EQ(8, res[1]);
EXPECT_EQ(9, res[2]);
EXPECT_EQ(13, res[3]);
EXPECT_EQ(12, res[4]);
EXPECT_EQ(11, res[5]);
EXPECT_EQ(12, res[6]);
f.clear();
f.insert(3);
f.insert(4, 5);
f.insert(9, 7);
f.insert(2);
EXPECT_EQ(7, f.size());
EXPECT_EQ(3, f.ranges());
f.displace(-4);
// Check that it was displaced just -2 frames because it is the lowest frame
// in the sequence.
res = to_vector(f);
ASSERT_EQ(7, res.size());
EXPECT_EQ(1, res[0]);
EXPECT_EQ(2, res[1]);
EXPECT_EQ(3, res[2]);
EXPECT_EQ(7, res[3]);
EXPECT_EQ(6, res[4]);
EXPECT_EQ(5, res[5]);
EXPECT_EQ(0, res[6]);
}
int main(int argc, char** argv)
{
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

View File

@ -59,12 +59,14 @@ Playback::Playback(const Sprite* sprite,
const TagsList& tags,
const frame_t frame,
const Mode playMode,
const Tag* tag)
const Tag* tag,
const int forward)
: m_sprite(sprite)
, m_tags(tags)
, m_initialFrame(frame)
, m_frame(frame)
, m_playMode(playMode)
, m_forward(forward)
{
PLAY_TRACE("--Playback-- tag=", (tag ? tag->name(): ""), "mode=", mode_to_string(m_playMode));
@ -86,7 +88,7 @@ Playback::Playback(const Sprite* sprite,
if (tag) {
addTag(tag, false, 1);
// Loop the given tag in the constructor infite times
// Loop the given tag in the constructor infinite times
m_playing.back()->repeat = std::numeric_limits<int>::max();
}
}
@ -244,13 +246,23 @@ bool Playback::handleExitFrame(const frame_t frameDelta)
}
break;
}
else if (m_playMode == PlayAll)
break;
}
if (frameDelta > 0 && m_frame == m_sprite->lastFrame()) {
if (frameDelta > 0 &&
((m_frame == m_sprite->lastFrame() && m_forward > 0) ||
(m_frame == 0 && m_forward < 0))) {
if (m_playMode == PlayInLoop) {
PLAY_TRACE(" Going back to frame=0 (PlayInLoop)", m_frame,
m_sprite->lastFrame());
m_frame = 0;
if (m_forward > 0) {
PLAY_TRACE(" Going back to frame=0 (PlayInLoop)", m_frame,
m_sprite->lastFrame());
m_frame = 0;
}
else {
PLAY_TRACE(" Going back to frame=last frame (PlayInLoop)");
m_frame = m_sprite->lastFrame();
}
return false;
}
else {
@ -259,10 +271,18 @@ bool Playback::handleExitFrame(const frame_t frameDelta)
return false;
}
}
else if (frameDelta < 0 && m_frame == 0) {
else if (frameDelta < 0 &&
((m_frame == 0 && m_forward > 0) ||
(m_frame == m_sprite->lastFrame() && m_forward < 0))) {
if (m_playMode == PlayInLoop) {
PLAY_TRACE(" Going back to frame=last frame (PlayInLoop)");
m_frame = m_sprite->lastFrame();
if (m_forward > 0) {
PLAY_TRACE(" Going back to frame=last frame (PlayInLoop)");
m_frame = m_sprite->lastFrame();
}
else {
PLAY_TRACE(" Going back to frame=0 (PlayInLoop)");
m_frame = 0;
}
return false;
}
else {
@ -431,7 +451,7 @@ bool Playback::decrementRepeat(const frame_t frameDelta)
m_playing.pop_back();
// Forward direction of the parent tag
int forward = (m_playing.empty() ? +1: m_playing.back()->forward);
int forward = (m_playing.empty() ? m_forward: m_playing.back()->forward);
bool rewind = (m_playing.empty() ? false: m_playing.back()->rewind);
// New frame outside the tag
@ -520,7 +540,7 @@ void Playback::goToFirstTagFrame(const Tag* tag)
int Playback::getParentForward() const
{
if (m_playing.empty())
return 1;
return m_forward;
else
return m_playing.back()->forward;
}

View File

@ -50,7 +50,8 @@ namespace doc {
const TagsList& tagsList,
const frame_t frame,
const Mode playMode,
const Tag* tag);
const Tag* tag,
const int forward = 1);
Playback(const Sprite* sprite = nullptr,
const frame_t frame = 0,
@ -128,6 +129,7 @@ namespace doc {
frame_t m_initialFrame;
frame_t m_frame;
Mode m_playMode;
int m_forward;
// Queue of tags to play and tags that are being played
std::vector<std::unique_ptr<PlayTag>> m_playing;

View File

@ -9,133 +9,17 @@
#define DOC_SELECTED_FRAMES_H_INCLUDED
#pragma once
#include "doc/frame_range.h"
#include "doc/frames_iterators.h"
#include <iosfwd>
#include <vector>
using namespace doc::frames;
namespace doc {
// The FramesSequence class is based in several code of this class.
// TODO: At some point we should remove the duplicated code between these
// classes.
class SelectedFrames {
typedef std::vector<FrameRange> Ranges;
public:
class const_iterator {
static const int kNullFrame = -2;
public:
using iterator_category = std::forward_iterator_tag;
using value_type = frame_t;
using difference_type = std::ptrdiff_t;
using pointer = frame_t*;
using reference = frame_t&;
const_iterator(const Ranges::const_iterator& it)
: m_it(it), m_frame(kNullFrame) {
}
const_iterator& operator++() {
if (m_frame == kNullFrame)
m_frame = m_it->fromFrame;
if (m_it->fromFrame <= m_it->toFrame) {
if (m_frame < m_it->toFrame)
++m_frame;
else {
m_frame = kNullFrame;
++m_it;
}
}
else {
if (m_frame > m_it->toFrame)
--m_frame;
else {
m_frame = kNullFrame;
++m_it;
}
}
return *this;
}
frame_t operator*() const {
if (m_frame == kNullFrame)
m_frame = m_it->fromFrame;
return m_frame;
}
bool operator==(const const_iterator& o) const {
return (m_it == o.m_it && m_frame == o.m_frame);
}
bool operator!=(const const_iterator& it) const {
return !operator==(it);
}
private:
mutable Ranges::const_iterator m_it;
mutable frame_t m_frame;
};
class const_reverse_iterator {
public:
using iterator_category = std::forward_iterator_tag;
using value_type = frame_t;
using difference_type = std::ptrdiff_t;
using pointer = frame_t*;
using reference = frame_t&;
const_reverse_iterator(const Ranges::const_reverse_iterator& it)
: m_it(it), m_frame(-1) {
}
const_reverse_iterator& operator++() {
if (m_frame < 0)
m_frame = m_it->toFrame;
if (m_frame > m_it->fromFrame)
--m_frame;
else {
m_frame = -1;
++m_it;
}
return *this;
}
frame_t operator*() const {
if (m_frame < 0)
m_frame = m_it->toFrame;
return m_frame;
}
bool operator==(const const_reverse_iterator& o) const {
return (m_it == o.m_it && m_frame == o.m_frame);
}
bool operator!=(const const_reverse_iterator& it) const {
return !operator==(it);
}
private:
mutable Ranges::const_reverse_iterator m_it;
mutable frame_t m_frame;
};
class Reversed {
public:
typedef const_reverse_iterator const_iterator;
const_iterator begin() const { return m_selectedFrames.rbegin(); }
const_iterator end() const { return m_selectedFrames.rend(); }
Reversed(const SelectedFrames& selectedFrames)
: m_selectedFrames(selectedFrames) {
}
private:
const SelectedFrames& m_selectedFrames;
};
const_iterator begin() const { return const_iterator(m_ranges.begin()); }
const_iterator end() const { return const_iterator(m_ranges.end()); }
const_reverse_iterator rbegin() const { return const_reverse_iterator(m_ranges.rbegin()); }
@ -156,7 +40,7 @@ namespace doc {
frame_t lastFrame() const { return (!m_ranges.empty() ? m_ranges.back().toFrame: -1); }
void displace(frame_t frameDelta);
Reversed reversed() const { return Reversed(*this); }
Reversed<SelectedFrames> reversed() const { return Reversed(*this); }
SelectedFrames makeReverse() const;
SelectedFrames makePingPong() const;

View File

@ -211,6 +211,46 @@ TEST(SelectedFrames, MakePingPongAndFilter)
EXPECT_EQ(7, res[1]);
}
TEST(SelectedFrames, Displace)
{
SelectedFrames f;
f.insert(1);
f.insert(4, 5);
f.insert(7, 9);
EXPECT_EQ(6, f.size());
EXPECT_EQ(3, f.ranges());
f.displace(4);
auto res = to_vector(f);
ASSERT_EQ(6, res.size());
EXPECT_EQ(5, res[0]);
EXPECT_EQ(8, res[1]);
EXPECT_EQ(9, res[2]);
EXPECT_EQ(11, res[3]);
EXPECT_EQ(12, res[4]);
EXPECT_EQ(13, res[5]);
f.clear();
f.insert(3);
f.insert(4, 5);
f.insert(7, 9);
EXPECT_EQ(6, f.size());
EXPECT_EQ(2, f.ranges());
// Check that it was displaced just -3 frames because 3 is the first selected
// frame.
f.displace(-4);
res = to_vector(f);
ASSERT_EQ(6, res.size());
EXPECT_EQ(0, res[0]);
EXPECT_EQ(1, res[1]);
EXPECT_EQ(2, res[2]);
EXPECT_EQ(4, res[3]);
EXPECT_EQ(5, res[4]);
EXPECT_EQ(6, res[5]);
}
int main(int argc, char** argv)
{
::testing::InitGoogleTest(&argc, argv);