mirror of
https://github.com/aseprite/aseprite.git
synced 2025-03-01 10:13:22 +00:00
commit
9318ce4941
@ -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" />
|
||||
|
@ -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
|
||||
|
@ -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">
|
||||
|
@ -31,7 +31,7 @@ FileOpROI CliOpenFile::roi() const
|
||||
document->sprite()->bounds(),
|
||||
slice,
|
||||
tag,
|
||||
selFrames,
|
||||
FramesSequence(selFrames),
|
||||
true);
|
||||
}
|
||||
|
||||
|
@ -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 "
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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());
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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
145
src/doc/frames_iterators.h
Normal 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
207
src/doc/frames_sequence.cpp
Normal 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
69
src/doc/frames_sequence.h
Normal 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
|
352
src/doc/frames_sequence_tests.cpp
Normal file
352
src/doc/frames_sequence_tests.cpp
Normal 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();
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user