Add an option to trim areas outside the canvas bounds on Sprite > Canvas Size (fix #1111)

This commit is contained in:
David Capello 2019-04-23 14:40:26 -03:00
parent bbba80c809
commit e197a8670c
8 changed files with 243 additions and 77 deletions

View File

@ -332,6 +332,9 @@
<option id="files_with_profile" type="ColorProfileBehavior" default="ColorProfileBehavior::EMBEDDED" />
<option id="missing_profile" type="ColorProfileBehavior" default="ColorProfileBehavior::ASSIGN" />
</section>
<section id="canvas_size">
<option id="trim_outside" type="bool" default="false" />
</section>
</global>
<tool>

View File

@ -466,6 +466,11 @@ bottom_tooltip = <<<END
Rows to be added/removed in the bottom side.
Use a negative number to remove rows.
END
trim = &Trim content outside the canvas
trim_tooltip = <<<END
Remove pixels from image cels that will
be left outside the new canvas size.
END
[cel_properties]
title = Cel Properties

View File

@ -1,5 +1,6 @@
<!-- Aseprite -->
<!-- Copyright (C) 2001-2018 by David Capello -->
<!-- Copyright (C) 2019 Igara Studio S.A. -->
<!-- Copyright (C) 2001-2018 David Capello -->
<gui>
<window id="canvas_size" text="@.title">
<vbox>
@ -40,6 +41,10 @@
<expr text="0" id="bottom" suffix="px" tooltip="@.bottom_tooltip" />
</grid>
<separator horizontal="true" />
<check id="trim" text="@.trim" tooltip="@.trim_tooltip" />
<separator horizontal="true" />
<hbox>
<boxfiller />

2
laf

@ -1 +1 @@
Subproject commit a993de2650361217865d050eb004b7b6cebcac45
Subproject commit 96e084ff66aa8317a2892250e13e653f24a4a058

View File

@ -10,6 +10,7 @@
#endif
#include "app/commands/command.h"
#include "app/commands/new_params.h"
#include "app/context_access.h"
#include "app/doc_api.h"
#include "app/modules/editors.h"
@ -40,6 +41,16 @@ using namespace app::skin;
#pragma warning(disable:4355)
#endif
struct CanvasSizeParams : public NewParams {
Param<int> left { this, 0, "left" };
Param<int> right { this, 0, "right" };
Param<int> top { this, 0, "top" };
Param<int> bottom { this, 0, "bottom" };
Param<bool> trimOutside { this, false, "trimOutside" };
};
#ifdef ENABLE_UI
// Window used to show canvas parameters.
class CanvasSizeWindow : public app::gen::CanvasSize
, public SelectBoxDelegate
@ -47,7 +58,7 @@ class CanvasSizeWindow : public app::gen::CanvasSize
public:
enum class Dir { NW, N, NE, W, C, E, SW, S, SE };
CanvasSizeWindow()
CanvasSizeWindow(const CanvasSizeParams& params)
: m_editor(current_editor)
, m_rect(0, 0, current_editor->sprite()->width(), current_editor->sprite()->height())
, m_selectBoxState(
@ -74,6 +85,7 @@ public:
m_editor->setState(m_selectBoxState);
dir()->setSelectedItem((int)Dir::C);
trim()->setSelected(params.trimOutside());
updateIcons();
}
@ -89,6 +101,7 @@ public:
int getRight() { return right()->textInt(); }
int getTop() { return top()->textInt(); }
int getBottom() { return bottom()->textInt(); }
bool getTrimOutside() { return trim()->isSelected(); }
protected:
@ -270,31 +283,20 @@ private:
EditorStatePtr m_selectBoxState;
};
class CanvasSizeCommand : public Command {
#endif // ENABLE_UI
class CanvasSizeCommand : public CommandWithNewParams<CanvasSizeParams> {
public:
CanvasSizeCommand();
protected:
void onLoadParams(const Params& params) override;
bool onEnabled(Context* context) override;
void onExecute(Context* context) override;
private:
int m_left, m_right, m_top, m_bottom;
};
CanvasSizeCommand::CanvasSizeCommand()
: Command(CommandId::CanvasSize(), CmdRecordableFlag)
: CommandWithNewParams(CommandId::CanvasSize(), CmdRecordableFlag)
{
m_left = m_right = m_top = m_bottom = 0;
}
void CanvasSizeCommand::onLoadParams(const Params& params)
{
m_left = params.get_as<int>("left");
m_right = params.get_as<int>("right");
m_top = params.get_as<int>("top");
m_bottom = params.get_as<int>("bottom");
}
bool CanvasSizeCommand::onEnabled(Context* context)
@ -307,11 +309,16 @@ void CanvasSizeCommand::onExecute(Context* context)
{
const ContextReader reader(context);
const Sprite* sprite(reader.sprite());
auto& params = this->params();
#ifdef ENABLE_UI
if (context->isUIAvailable()) {
if (!params.trimOutside.isSet()) {
params.trimOutside(Preferences::instance().canvasSize.trimOutside());
}
// load the window widget
std::unique_ptr<CanvasSizeWindow> window(new CanvasSizeWindow());
std::unique_ptr<CanvasSizeWindow> window(new CanvasSizeWindow(params));
window->remapWindow();
@ -332,19 +339,22 @@ void CanvasSizeCommand::onExecute(Context* context)
if (!window->pressedOk())
return;
m_left = window->getLeft();
m_right = window->getRight();
m_top = window->getTop();
m_bottom = window->getBottom();
params.left(window->getLeft());
params.right(window->getRight());
params.top(window->getTop());
params.bottom(window->getBottom());
params.trimOutside(window->getTrimOutside());
Preferences::instance().canvasSize.trimOutside(params.trimOutside());
}
#endif
// Resize canvas
int x1 = -m_left;
int y1 = -m_top;
int x2 = sprite->width() + m_right;
int y2 = sprite->height() + m_bottom;
int x1 = -params.left();
int y1 = -params.top();
int x2 = sprite->width() + params.right();
int y2 = sprite->height() + params.bottom();
if (x2 <= x1) x2 = x1+1;
if (y2 <= y1) y2 = y1+1;
@ -359,7 +369,8 @@ void CanvasSizeCommand::onExecute(Context* context)
api.cropSprite(sprite,
gfx::Rect(x1, y1,
MID(1, x2-x1, DOC_SPRITE_MAX_WIDTH),
MID(1, y2-y1, DOC_SPRITE_MAX_HEIGHT)));
MID(1, y2-y1, DOC_SPRITE_MAX_HEIGHT)),
params.trimOutside());
tx.commit();
#ifdef ENABLE_UI

View File

@ -59,7 +59,13 @@
#include "doc/slice.h"
#include "render/render.h"
#include <algorithm>
#include <iterator>
#include <set>
#include <vector>
#include "gfx/rect_io.h"
#include "gfx/point_io.h"
#define TRACE_DOCAPI(...)
@ -81,55 +87,20 @@ void DocApi::setSpriteTransparentColor(Sprite* sprite, color_t maskColor)
m_transaction.execute(new cmd::SetTransparentColor(sprite, maskColor));
}
void DocApi::cropSprite(Sprite* sprite, const gfx::Rect& bounds)
void DocApi::cropSprite(Sprite* sprite,
const gfx::Rect& bounds,
const bool trimOutside)
{
ASSERT(m_document == static_cast<Doc*>(sprite->document()));
setSpriteSize(sprite, bounds.w, bounds.h);
Doc* doc = static_cast<Doc*>(sprite->document());
LayerList layers = sprite->allLayers();
for (Layer* layer : layers) {
if (!layer->isImage())
continue;
std::set<ObjectId> visited;
CelIterator it = ((LayerImage*)layer)->getCelBegin();
CelIterator end = ((LayerImage*)layer)->getCelEnd();
for (; it != end; ++it) {
Cel* cel = *it;
if (visited.find(cel->data()->id()) != visited.end())
continue;
visited.insert(cel->data()->id());
if (layer->isBackground()) {
Image* image = cel->image();
if (image && !cel->link()) {
ASSERT(cel->x() == 0);
ASSERT(cel->y() == 0);
// Create the new image through a crop
ImageRef new_image(
crop_image(image,
bounds.x, bounds.y,
bounds.w, bounds.h,
doc->bgColor(layer)));
// Replace the image in the stock that is pointed by the cel
replaceImage(sprite, cel->imageRef(), new_image);
}
}
else if (layer->isReference()) {
// Update the ref cel's bounds
gfx::RectF newBounds = cel->boundsF();
newBounds.x -= bounds.x;
newBounds.y -= bounds.y;
m_transaction.execute(new cmd::SetCelBoundsF(cel, newBounds));
}
else {
// Update the cel's position
setCelPosition(sprite, cel,
cel->x()-bounds.x, cel->y()-bounds.y);
}
}
cropImageLayer(static_cast<LayerImage*>(layer), bounds, trimOutside);
}
// Update mask position
@ -140,14 +111,30 @@ void DocApi::cropSprite(Sprite* sprite, const gfx::Rect& bounds)
// Update slice positions
if (bounds.origin() != gfx::Point(0, 0)) {
for (auto& slice : m_document->sprite()->slices()) {
for (auto& k : *slice) {
Slice::List::List keys;
std::copy(slice->begin(), slice->end(),
std::back_inserter(keys));
for (auto& k : keys) {
const SliceKey& key = *k.value();
if (key.isEmpty())
continue;
gfx::Rect newSliceBounds(key.bounds());
newSliceBounds.offset(-bounds.origin());
SliceKey newKey = key;
newKey.setBounds(
gfx::Rect(newKey.bounds()).offset(-bounds.origin()));
// If the slice is outside and the user doesn't want the out
// of canvas content, we delete the slice.
if (trimOutside) {
newSliceBounds &= gfx::Rect(bounds.size());
if (newSliceBounds.isEmpty())
newKey = SliceKey(); // An empty key (so we remove this key)
}
if (!newKey.isEmpty())
newKey.setBounds(newSliceBounds);
// As SliceKey::center() and pivot() properties are relative
// to the bounds(), we don't need to adjust them.
@ -159,6 +146,136 @@ void DocApi::cropSprite(Sprite* sprite, const gfx::Rect& bounds)
}
}
void DocApi::cropImageLayer(LayerImage* layer,
const gfx::Rect& bounds,
const bool trimOutside)
{
std::set<ObjectId> visited;
CelList cels, clearCels;
layer->getCels(cels);
for (Cel* cel : cels) {
if (visited.find(cel->data()->id()) != visited.end())
continue;
visited.insert(cel->data()->id());
if (!cropCel(layer, cel, bounds, trimOutside)) {
// Delete this cel and its links
clearCels.push_back(cel);
}
}
for (Cel* cel : clearCels)
clearCelAndAllLinks(cel);
}
// Returns false if the cel (and its links) must be deleted after this
bool DocApi::cropCel(LayerImage* layer,
Cel* cel,
const gfx::Rect& bounds,
const bool trimOutside)
{
if (layer->isBackground()) {
Image* image = cel->image();
if (image && !cel->link()) {
ASSERT(cel->x() == 0);
ASSERT(cel->y() == 0);
ImageRef newImage(
crop_image(image,
bounds.x, bounds.y,
bounds.w, bounds.h,
m_document->bgColor(layer)));
replaceImage(cel->sprite(),
cel->imageRef(),
newImage);
}
return true;
}
if (layer->isReference()) {
// Update the ref cel's bounds
gfx::RectF newBounds = cel->boundsF();
newBounds.x -= bounds.x;
newBounds.y -= bounds.y;
m_transaction.execute(new cmd::SetCelBoundsF(cel, newBounds));
return true;
}
gfx::Point newCelPos(cel->position() - bounds.origin());
// This is the complex case: we want to crop a transparent cel and
// remove the content that is outside the sprite canvas. This might
// generate one or two of the following Cmd:
// 1. Clear the cel ("return false" will generate a "cmd::ClearCel"
// then) if the cel bounds will be totally outside in the new
// canvas size
// 2. Replace the cel image if the cel must be cut in
// some edge because it's not totally contained
// 3. Just set the cel position (the most common case)
// if the cel image will be completely inside the new
// canvas
if (trimOutside) {
Image* image = cel->image();
if (image && !cel->link()) {
gfx::Rect newCelBounds = (bounds & cel->bounds());
if (newCelBounds.isEmpty())
return false;
newCelBounds.offset(-bounds.origin());
gfx::Point paintPos(newCelBounds.x - newCelPos.x,
newCelBounds.y - newCelPos.y);
newCelPos = newCelBounds.origin();
// Crop the image
ImageRef newImage(
crop_image(image,
paintPos.x, paintPos.y,
newCelBounds.w, newCelBounds.h,
m_document->bgColor(layer)));
// Try to shrink the image ignoring transparent borders
gfx::Rect frameBounds;
if (doc::algorithm::shrink_bounds(newImage.get(),
frameBounds,
newImage->maskColor())) {
// In this case the new cel image can be even smaller
if (frameBounds != newImage->bounds()) {
newImage = ImageRef(
crop_image(newImage.get(),
frameBounds.x, frameBounds.y,
frameBounds.w, frameBounds.h,
m_document->bgColor(layer)));
newCelPos += frameBounds.origin();
}
}
else {
// Delete this cel and its links
return false;
}
// If it's the same iamge, we can re-use the cel image and just
// move the cel position.
if (!is_same_image(cel->image(), newImage.get())) {
replaceImage(cel->sprite(),
cel->imageRef(),
newImage);
}
}
}
// Update the cel's position
setCelPosition(
cel->sprite(), cel,
newCelPos.x,
newCelPos.y);
return true;
}
void DocApi::trimSprite(Sprite* sprite, const bool byGrid)
{
gfx::Rect bounds;
@ -386,7 +503,8 @@ void DocApi::setCelPosition(Sprite* sprite, Cel* cel, int x, int y)
{
ASSERT(cel);
m_transaction.execute(new cmd::SetCelPosition(cel, x, y));
if (cel->x() != x || cel->y() != y)
m_transaction.execute(new cmd::SetCelPosition(cel, x, y));
}
void DocApi::setCelOpacity(Sprite* sprite, Cel* cel, int newOpacity)
@ -409,6 +527,20 @@ void DocApi::clearCel(Cel* cel)
m_transaction.execute(new cmd::ClearCel(cel));
}
void DocApi::clearCelAndAllLinks(Cel* cel)
{
ASSERT(cel);
ObjectId dataId = cel->data()->id();
CelList cels;
cel->layer()->getCels(cels);
for (Cel* cel2 : cels) {
if (cel2->data()->id() == dataId)
clearCel(cel2);
}
}
void DocApi::moveCel(
LayerImage* srcLayer, frame_t srcFrame,
LayerImage* dstLayer, frame_t dstFrame)

View File

@ -45,7 +45,9 @@ namespace app {
// Sprite API
void setSpriteSize(Sprite* sprite, int w, int h);
void setSpriteTransparentColor(Sprite* sprite, color_t maskColor);
void cropSprite(Sprite* sprite, const gfx::Rect& bounds);
void cropSprite(Sprite* sprite,
const gfx::Rect& bounds,
const bool trimOutside = false);
void trimSprite(Sprite* sprite, const bool byGrid);
// Frames API
@ -72,6 +74,7 @@ namespace app {
Cel* addCel(LayerImage* layer, frame_t frameNumber, const ImageRef& image);
void clearCel(LayerImage* layer, frame_t frame);
void clearCel(Cel* cel);
void clearCelAndAllLinks(Cel* cel);
void setCelPosition(Sprite* sprite, Cel* cel, int x, int y);
void setCelOpacity(Sprite* sprite, Cel* cel, int newOpacity);
void moveCel(
@ -113,6 +116,13 @@ namespace app {
void setPalette(Sprite* sprite, frame_t frame, const Palette* newPalette);
private:
void cropImageLayer(LayerImage* layer,
const gfx::Rect& bounds,
const bool trimOutside);
bool cropCel(LayerImage* layer,
Cel* cel,
const gfx::Rect& bounds,
const bool trimOutside);
void setCelFramePosition(Cel* cel, frame_t frame);
void moveFrameLayer(Layer* layer, frame_t frame, frame_t beforeFrame);
void adjustFrameTags(Sprite* sprite,

View File

@ -1,5 +1,6 @@
// Aseprite Document Library
// Copyright (c) 2017 David Capello
// Copyright (C) 2019 Igara Studio S.A.
// Copyright (C) 2017 David Capello
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
@ -47,9 +48,8 @@ namespace doc {
};
class Slice : public WithUserData {
typedef Keyframes<SliceKey> List;
public:
typedef Keyframes<SliceKey> List;
typedef List::iterator iterator;
typedef List::const_iterator const_iterator;