mirror of
https://github.com/aseprite/aseprite.git
synced 2025-03-29 19:20:09 +00:00
Add an option to trim areas outside the canvas bounds on Sprite > Canvas Size (fix #1111)
This commit is contained in:
parent
bbba80c809
commit
e197a8670c
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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
2
laf
@ -1 +1 @@
|
||||
Subproject commit a993de2650361217865d050eb004b7b6cebcac45
|
||||
Subproject commit 96e084ff66aa8317a2892250e13e653f24a4a058
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user