Fix several issues selecting specific layers to export (fix #2084)

Now if we choose a group to export, the children are not automatically
exported too, the original visibility state is kept. (Anyway we can
still include all those children doing something like "-layer
groupName/*" from the CLI.)
This commit is contained in:
David Capello 2019-08-29 17:04:58 -03:00
parent fe0563664d
commit 5b782dc27e
5 changed files with 339 additions and 20 deletions

View File

@ -34,6 +34,9 @@
#include "doc/slice.h"
#include "render/dithering_algorithm.h"
#include <queue>
#include <vector>
namespace app {
namespace {
@ -65,6 +68,8 @@ bool match_path(const std::string& filter,
return false;
}
const bool wildcard = (!a.empty() && a[a.size()-1] == "*");
// Exclude group itself when all children are excluded. This special
// case is only for exclusion because if we leave the group
// selected, the propagation of the selection will include all
@ -72,46 +77,97 @@ bool match_path(const std::string& filter,
if (exclude &&
a.size() > 1 &&
a.size() == b.size()+1 &&
a[a.size()-1] == "*")
wildcard) {
return true;
}
return (a.size() <= b.size());
if (exclude || wildcard)
return (a.size() <= b.size());
else {
// Include filters need exact match when there is no wildcard
return (a.size() == b.size());
}
}
bool filter_layer(const Layer* layer,
const std::string& layer_path,
bool filter_layer(const std::string& layer_path,
const std::vector<std::string>& filters,
const bool result)
{
for (const auto& filter : filters) {
if (layer->name() == filter ||
match_path(filter, layer_path, !result))
if (match_path(filter, layer_path, !result))
return result;
}
return !result;
}
void filter_layers(const LayerList& layers,
const CliOpenFile& cof,
SelectedLayers& filteredLayers)
// If there is one layer with the given name "filter", we can convert
// the filter to a full path to the layer (e.g. to match child layers
// of a group).
std::string convert_filter_to_layer_path_if_possible(
const Sprite* sprite,
const std::string& filter)
{
for (Layer* layer : layers) {
std::string fullName;
std::queue<Layer*> layers;
layers.push(sprite->root());
while (!layers.empty()) {
const Layer* layer = layers.front();
layers.pop();
if (layer != sprite->root() &&
layer->name() == filter) {
if (fullName.empty()) {
fullName = get_layer_path(layer);
}
else {
// Two or more layers with the same name (use "filter" as a
// general filter, not a specific layer name)
return filter;
}
}
if (layer->isGroup()) {
for (auto child : static_cast<const LayerGroup*>(layer)->layers())
layers.push(child);
}
}
if (!fullName.empty())
return fullName;
else
return filter;
}
} // anonymous namespace
// static
void CliProcessor::FilterLayers(const Sprite* sprite,
std::vector<std::string> includes,
std::vector<std::string> excludes,
SelectedLayers& filteredLayers)
{
// Convert filters to full paths for the sprite layers if there are
// just one layer with the given name.
for (auto& include : includes)
include = convert_filter_to_layer_path_if_possible(sprite, include);
for (auto& exclude : excludes)
exclude = convert_filter_to_layer_path_if_possible(sprite, exclude);
for (Layer* layer : sprite->allLayers()) {
auto layer_path = get_layer_path(layer);
if ((cof.includeLayers.empty() && !layer->isVisibleHierarchy()) ||
(!cof.includeLayers.empty() && !filter_layer(layer, layer_path, cof.includeLayers, true)))
if ((includes.empty() && !layer->isVisibleHierarchy()) ||
(!includes.empty() && !filter_layer(layer_path, includes, true)))
continue;
if (!cof.excludeLayers.empty() &&
!filter_layer(layer, layer_path, cof.excludeLayers, false))
if (!excludes.empty() &&
!filter_layer(layer_path, excludes, false))
continue;
filteredLayers.insert(layer);
}
}
} // anonymous namespace
CliProcessor::CliProcessor(CliDelegate* delegate,
const AppOptions& options)
: m_delegate(delegate)
@ -568,7 +624,7 @@ bool CliProcessor::openFile(Context* ctx, CliOpenFile& cof)
if (cof.hasLayersFilter()) {
SelectedLayers filteredLayers;
filter_layers(doc->sprite()->allLayers(), cof, filteredLayers);
filterLayers(doc->sprite(), cof, filteredLayers);
if (cof.splitLayers) {
for (Layer* layer : filteredLayers.toAllLayersList()) {
@ -638,7 +694,6 @@ void CliProcessor::saveFile(Context* ctx, const CliOpenFile& cof)
}
SelectedLayers filteredLayers;
LayerList allLayers = doc->sprite()->allLayers();
LayerList layers;
// --save-as with --split-layers or --split-tags
if (cof.splitLayers) {
@ -648,7 +703,7 @@ void CliProcessor::saveFile(Context* ctx, const CliOpenFile& cof)
else {
// Filter layers
if (cof.hasLayersFilter())
filter_layers(allLayers, cof, filteredLayers);
filterLayers(doc->sprite(), cof, filteredLayers);
// All visible layers
layers.push_back(nullptr);

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2019 Igara Studio S.A.
// Copyright (C) 2016-2018 David Capello
//
// This program is distributed under the terms of
@ -10,11 +11,16 @@
#include "app/cli/cli_delegate.h"
#include "app/cli/cli_open_file.h"
#include "doc/selected_layers.h"
#include <memory>
#include <string>
#include <vector>
namespace doc {
class Sprite;
}
namespace app {
class AppOptions;
@ -27,10 +33,27 @@ namespace app {
const AppOptions& options);
void process(Context* ctx);
// Public so it can be tested
static void FilterLayers(const doc::Sprite* sprite,
// By value because these vectors will be modified inside
std::vector<std::string> includes,
std::vector<std::string> excludes,
doc::SelectedLayers& filteredLayers);
private:
bool openFile(Context* ctx, CliOpenFile& cof);
void saveFile(Context* ctx, const CliOpenFile& cof);
void filterLayers(const doc::Sprite* sprite,
const CliOpenFile& cof,
doc::SelectedLayers& filteredLayers) {
CliProcessor::FilterLayers(
sprite,
cof.includeLayers,
cof.excludeLayers,
filteredLayers);
}
CliDelegate* m_delegate;
const AppOptions& m_options;
std::unique_ptr<DocExporter> m_exporter;

View File

@ -0,0 +1,217 @@
// Aseprite
// Copyright (C) 2019 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#include "tests/test.h"
#include "app/cli/cli_processor.h"
#include "doc/layer.h"
#include "doc/sprite.h"
using namespace app;
using namespace doc;
class FilterLayers : public ::testing::Test {
public:
FilterLayers()
: sprite(ImageSpec(ColorMode::RGB, 32, 32))
{
a->setName("a");
b->setName("b");
aa->setName("a");
ab->setName("b");
ba->setName("a");
bb->setName("b");
sprite.root()->addLayer(a);
sprite.root()->addLayer(b);
a->addLayer(aa);
a->addLayer(ab);
b->addLayer(ba);
b->addLayer(bb);
}
void filter(
std::vector<std::string> includes,
std::vector<std::string> excludes)
{
CliProcessor::FilterLayers(
&sprite, std::move(includes), std::move(excludes), sel);
}
Sprite sprite;
LayerGroup* a = new LayerGroup(&sprite);
LayerGroup* b = new LayerGroup(&sprite);
LayerImage* aa = new LayerImage(&sprite);
LayerImage* ab = new LayerImage(&sprite);
LayerImage* ba = new LayerImage(&sprite);
LayerImage* bb = new LayerImage(&sprite);
SelectedLayers sel;
};
TEST_F(FilterLayers, Default)
{
filter({}, {});
EXPECT_EQ(6, sel.size());
EXPECT_TRUE(sel.contains(a));
EXPECT_TRUE(sel.contains(b));
EXPECT_TRUE(sel.contains(aa));
EXPECT_TRUE(sel.contains(ab));
EXPECT_TRUE(sel.contains(ba));
EXPECT_TRUE(sel.contains(bb));
}
TEST_F(FilterLayers, DefaultWithHiddenChild)
{
aa->setVisible(false);
filter({}, {});
EXPECT_EQ(5, sel.size());
EXPECT_TRUE(sel.contains(a));
EXPECT_TRUE(sel.contains(b));
EXPECT_FALSE(sel.contains(aa));
EXPECT_TRUE(sel.contains(ab));
EXPECT_TRUE(sel.contains(ba));
EXPECT_TRUE(sel.contains(bb));
}
TEST_F(FilterLayers, DefaultWithHiddenGroup)
{
b->setVisible(false);
filter({}, {});
EXPECT_EQ(3, sel.size());
EXPECT_TRUE(sel.contains(a));
EXPECT_FALSE(sel.contains(b));
EXPECT_TRUE(sel.contains(aa));
EXPECT_TRUE(sel.contains(ab));
EXPECT_FALSE(sel.contains(ba));
EXPECT_FALSE(sel.contains(bb));
}
TEST_F(FilterLayers, All)
{
filter({ "*" }, {});
EXPECT_EQ(6, sel.size());
EXPECT_TRUE(sel.contains(a));
EXPECT_TRUE(sel.contains(b));
EXPECT_TRUE(sel.contains(aa));
EXPECT_TRUE(sel.contains(ab));
EXPECT_TRUE(sel.contains(ba));
EXPECT_TRUE(sel.contains(bb));
}
TEST_F(FilterLayers, AllWithHiddenChild)
{
ab->setVisible(false);
filter({ "*" }, {});
EXPECT_EQ(6, sel.size());
EXPECT_TRUE(sel.contains(a));
EXPECT_TRUE(sel.contains(b));
EXPECT_TRUE(sel.contains(aa));
EXPECT_TRUE(sel.contains(ab));
EXPECT_TRUE(sel.contains(ba));
EXPECT_TRUE(sel.contains(bb));
}
TEST_F(FilterLayers, IncludeGroupAmbiguousNameSelectTopLevel)
{
filter({ "a" }, {});
EXPECT_EQ(1, sel.size());
EXPECT_TRUE(sel.contains(a));
}
TEST_F(FilterLayers, IncludeChild)
{
filter({ "a/a" }, {});
EXPECT_EQ(1, sel.size());
EXPECT_TRUE(sel.contains(aa));
EXPECT_FALSE(sel.contains(a));
}
TEST_F(FilterLayers, IncludeGroupWithHiddenChild)
{
aa->setVisible(false);
filter({ "a" }, {});
EXPECT_EQ(1, sel.size());
EXPECT_TRUE(sel.contains(a));
}
TEST_F(FilterLayers, IncludeChildrenEvenHiddenOne)
{
aa->setVisible(false);
filter({ "a/*" }, {});
EXPECT_EQ(2, sel.size());
EXPECT_TRUE(sel.contains(aa));
EXPECT_TRUE(sel.contains(ab));
}
TEST_F(FilterLayers, IncludeHiddenChild)
{
aa->setName("aa");
aa->setVisible(false);
filter({ "aa" }, {});
EXPECT_EQ(1, sel.size());
EXPECT_TRUE(sel.contains(aa));
}
TEST_F(FilterLayers, IncludeHiddenChildWithFullPath)
{
aa->setVisible(false);
filter({ "a/a" }, {});
EXPECT_EQ(1, sel.size());
EXPECT_TRUE(sel.contains(aa));
}
TEST_F(FilterLayers, ExcludeAll)
{
filter({}, { "*" });
EXPECT_TRUE(sel.empty());
}
TEST_F(FilterLayers, ExcludeChild)
{
filter({}, { "a/a" });
EXPECT_EQ(5, sel.size());
EXPECT_TRUE(sel.contains(a));
EXPECT_TRUE(sel.contains(b));
EXPECT_FALSE(sel.contains(aa));
EXPECT_TRUE(sel.contains(ab));
EXPECT_TRUE(sel.contains(ba));
EXPECT_TRUE(sel.contains(bb));
}
TEST_F(FilterLayers, ExcludeGroup)
{
filter({}, { "b" });
EXPECT_EQ(3, sel.size());
EXPECT_TRUE(sel.contains(a));
EXPECT_TRUE(sel.contains(aa));
EXPECT_TRUE(sel.contains(ab));
EXPECT_FALSE(sel.contains(b));
EXPECT_FALSE(sel.contains(ba));
EXPECT_FALSE(sel.contains(ba));
}
TEST_F(FilterLayers, IncludeOneGroupAndExcludeOtherGroup)
{
filter({ "a" }, { "b" });
EXPECT_EQ(1, sel.size());
EXPECT_TRUE(sel.contains(a));
}
TEST_F(FilterLayers, IncludeAllExcludeOneGroup)
{
filter({ "*" }, { "b" });
EXPECT_EQ(3, sel.size());
EXPECT_TRUE(sel.contains(a));
EXPECT_TRUE(sel.contains(aa));
EXPECT_TRUE(sel.contains(ab));
}

View File

@ -17,6 +17,7 @@
#include "app/doc_exporter.h"
#include "app/file/file.h"
#include "base/fs.h"
#include "doc/layer.h"
#include "doc/sprite.h"
#include <iostream>
@ -111,6 +112,8 @@ void PreviewCliDelegate::saveFile(Context* ctx, const CliOpenFile& cof)
<< cof.document->sprite()->height() << "\n";
showLayersFilter(cof);
std::cout << " - Visible Layer:\n";
showLayerVisibility(cof.document->sprite()->root(), " ");
if (cof.hasFrameTag()) {
std::cout << " - Frame tag: '" << cof.frameTag << "'\n";
@ -241,4 +244,17 @@ void PreviewCliDelegate::showLayersFilter(const CliOpenFile& cof)
}
}
void PreviewCliDelegate::showLayerVisibility(const doc::LayerGroup* group,
const std::string& indent)
{
for (auto layer : group->layers()) {
if (!layer->isVisible())
continue;
std::cout << indent << "- " << layer->name() << "\n";
if (layer->isGroup())
showLayerVisibility(static_cast<const LayerGroup*>(layer),
indent + " ");
}
}
} // namespace app

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018 Igara Studio S.A.
// Copyright (C) 2018-2019 Igara Studio S.A.
// Copyright (C) 2016-2018 David Capello
//
// This program is distributed under the terms of
@ -11,6 +11,12 @@
#include "app/cli/cli_delegate.h"
#include <string>
namespace doc {
class LayerGroup;
}
namespace app {
class PreviewCliDelegate : public CliDelegate {
@ -34,6 +40,8 @@ namespace app {
private:
void showLayersFilter(const CliOpenFile& cof);
void showLayerVisibility(const doc::LayerGroup* group,
const std::string& indent);
};
} // namespace app