mirror of
https://github.com/aseprite/aseprite.git
synced 2025-01-26 12:35:33 +00:00
Add "Select > Modify" commands to expand/contract/border the selection
This commit is contained in:
parent
e944ce0052
commit
5dc149d308
11
data/gui.xml
11
data/gui.xml
@ -667,6 +667,17 @@
|
||||
<item command="InvertMask" text="&Inverse" />
|
||||
<separator />
|
||||
<item command="MaskByColor" text="&Color Range" />
|
||||
<menu text="&Modify">
|
||||
<item command="ModifySelection" text="&Border">
|
||||
<param name="modifier" value="border" />
|
||||
</item>
|
||||
<item command="ModifySelection" text="&Expand">
|
||||
<param name="modifier" value="expand" />
|
||||
</item>
|
||||
<item command="ModifySelection" text="&Contract">
|
||||
<param name="modifier" value="contract" />
|
||||
</item>
|
||||
</menu>
|
||||
<separator />
|
||||
<item command="LoadMask" text="&Load from MSK file" />
|
||||
<item command="SaveMask" text="&Save to MSK file" />
|
||||
|
@ -136,6 +136,8 @@
|
||||
<option id="auto_opaque" type="bool" default="true" />
|
||||
<option id="transparent_color" type="app::Color" />
|
||||
<option id="rotation_algorithm" type="app::tools::RotationAlgorithm" default="app::tools::RotationAlgorithm::DEFAULT" />
|
||||
<option id="modify_selection_quantity" type="int" default="1" />
|
||||
<option id="modify_selection_brush" type="BrushType" default="BrushType::CIRCLE" />
|
||||
</section>
|
||||
<section id="quantization">
|
||||
<option id="with_alpha" type="bool" default="true" />
|
||||
|
25
data/widgets/modify_selection.xml
Normal file
25
data/widgets/modify_selection.xml
Normal file
@ -0,0 +1,25 @@
|
||||
<!-- Aseprite -->
|
||||
<!-- Copyright (C) 2015 by David Capello -->
|
||||
<gui>
|
||||
<window id="modify_selection" text="Modify Selection">
|
||||
<vbox>
|
||||
<grid columns="2">
|
||||
|
||||
<label id="by_label" text="By:" />
|
||||
<entry id="quantity" expansive="true" maxsize="4" magnet="true" suffix="px" />
|
||||
|
||||
<hbox />
|
||||
<vbox>
|
||||
<radio id="circle" text="Circle Brush" group="1" />
|
||||
<radio id="square" text="Square Brush" group="1" />
|
||||
</vbox>
|
||||
|
||||
</grid>
|
||||
<separator horizontal="true" cell_hspan="3" />
|
||||
<hbox homogeneous="true" cell_hspan="3" cell_align="right">
|
||||
<button text="&OK" closewindow="true" id="ok" magnet="true" minwidth="60" />
|
||||
<button text="&Cancel" closewindow="true" />
|
||||
</hbox>
|
||||
</vbox>
|
||||
</window>
|
||||
</gui>
|
@ -209,6 +209,7 @@ add_library(app-lib
|
||||
commands/cmd_mask_by_color.cpp
|
||||
commands/cmd_mask_content.cpp
|
||||
commands/cmd_merge_down_layer.cpp
|
||||
commands/cmd_modify_selection.cpp
|
||||
commands/cmd_move_cel.cpp
|
||||
commands/cmd_move_mask.cpp
|
||||
commands/cmd_new_brush.cpp
|
||||
|
255
src/app/commands/cmd_modify_selection.cpp
Normal file
255
src/app/commands/cmd_modify_selection.cpp
Normal file
@ -0,0 +1,255 @@
|
||||
// Aseprite
|
||||
// Copyright (C) 2015 David Capello
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License version 2 as
|
||||
// published by the Free Software Foundation.
|
||||
|
||||
#ifdef HAVE_CONFIG_H
|
||||
#include "config.h"
|
||||
#endif
|
||||
|
||||
#include "app/cmd/set_mask.h"
|
||||
#include "app/commands/command.h"
|
||||
#include "app/commands/params.h"
|
||||
#include "app/context_access.h"
|
||||
#include "app/document.h"
|
||||
#include "app/modules/gui.h"
|
||||
#include "app/pref/preferences.h"
|
||||
#include "app/transaction.h"
|
||||
#include "base/convert_to.h"
|
||||
#include "doc/brush_type.h"
|
||||
#include "doc/mask.h"
|
||||
#include "filters/neighboring_pixels.h"
|
||||
|
||||
#include "modify_selection.xml.h"
|
||||
|
||||
#include <limits>
|
||||
|
||||
namespace app {
|
||||
|
||||
using namespace doc;
|
||||
|
||||
class ModifySelectionWindow : public app::gen::ModifySelection {
|
||||
};
|
||||
|
||||
class ModifySelectionCommand : public Command {
|
||||
public:
|
||||
enum Modifier { Border, Expand, Contract };
|
||||
|
||||
ModifySelectionCommand();
|
||||
Command* clone() const override { return new ModifySelectionCommand(*this); }
|
||||
|
||||
protected:
|
||||
void onLoadParams(const Params& params) override;
|
||||
bool onEnabled(Context* context) override;
|
||||
void onExecute(Context* context) override;
|
||||
std::string onGetFriendlyName() const override;
|
||||
|
||||
private:
|
||||
std::string getActionName() const;
|
||||
void applyModifier(const Mask* srcMask, Mask* dstMask,
|
||||
const int brushRadius,
|
||||
const doc::BrushType brushType) const;
|
||||
|
||||
Modifier m_modifier;
|
||||
int m_quantity;
|
||||
doc::BrushType m_brushType;
|
||||
};
|
||||
|
||||
ModifySelectionCommand::ModifySelectionCommand()
|
||||
: Command("ModifySelection",
|
||||
"Modify Selection",
|
||||
CmdRecordableFlag)
|
||||
, m_modifier(Expand)
|
||||
, m_quantity(0)
|
||||
, m_brushType(doc::kCircleBrushType)
|
||||
{
|
||||
}
|
||||
|
||||
void ModifySelectionCommand::onLoadParams(const Params& params)
|
||||
{
|
||||
const std::string modifier = params.get("modifier");
|
||||
if (modifier == "border") m_modifier = Border;
|
||||
else if (modifier == "expand") m_modifier = Expand;
|
||||
else if (modifier == "contract") m_modifier = Contract;
|
||||
|
||||
const int quantity = params.get_as<int>("quantity");
|
||||
m_quantity = std::max<int>(0, quantity);
|
||||
|
||||
const std::string brush = params.get("brush");
|
||||
if (brush == "circle") m_brushType = doc::kCircleBrushType;
|
||||
else if (brush == "square") m_brushType = doc::kSquareBrushType;
|
||||
}
|
||||
|
||||
bool ModifySelectionCommand::onEnabled(Context* context)
|
||||
{
|
||||
return context->checkFlags(ContextFlags::ActiveDocumentIsWritable |
|
||||
ContextFlags::HasVisibleMask);
|
||||
}
|
||||
|
||||
void ModifySelectionCommand::onExecute(Context* context)
|
||||
{
|
||||
int quantity = m_quantity;
|
||||
doc::BrushType brush = m_brushType;
|
||||
|
||||
if (quantity == 0) {
|
||||
Preferences& pref = Preferences::instance();
|
||||
ModifySelectionWindow window;
|
||||
|
||||
window.setText(getActionName() + " Selection");
|
||||
if (m_modifier == Border)
|
||||
window.byLabel()->setText("Width:");
|
||||
else
|
||||
window.byLabel()->setText(getActionName() + " By:");
|
||||
|
||||
window.quantity()->setTextf("%d", pref.selection.modifySelectionQuantity());
|
||||
|
||||
brush = (pref.selection.modifySelectionBrush() == app::gen::BrushType::CIRCLE
|
||||
? doc::kCircleBrushType:
|
||||
doc::kSquareBrushType);
|
||||
window.circle()->setSelected(brush == doc::kCircleBrushType);
|
||||
window.square()->setSelected(brush == doc::kSquareBrushType);
|
||||
|
||||
window.openWindowInForeground();
|
||||
if (window.closer() != window.ok())
|
||||
return;
|
||||
|
||||
quantity = window.quantity()->textInt();
|
||||
quantity = MID(1, quantity, 100);
|
||||
|
||||
brush = (window.circle()->isSelected() ? doc::kCircleBrushType:
|
||||
doc::kSquareBrushType);
|
||||
|
||||
pref.selection.modifySelectionQuantity(quantity);
|
||||
pref.selection.modifySelectionBrush(
|
||||
(brush == doc::kCircleBrushType ? app::gen::BrushType::CIRCLE:
|
||||
app::gen::BrushType::SQUARE));
|
||||
}
|
||||
|
||||
// Lock sprite
|
||||
ContextWriter writer(context);
|
||||
Document* document(writer.document());
|
||||
Sprite* sprite(writer.sprite());
|
||||
|
||||
base::UniquePtr<Mask> mask(new Mask());
|
||||
{
|
||||
mask->reserve(sprite->bounds());
|
||||
mask->freeze();
|
||||
applyModifier(document->mask(), mask, quantity, brush);
|
||||
mask->unfreeze();
|
||||
}
|
||||
|
||||
// Set the new mask
|
||||
Transaction transaction(writer.context(),
|
||||
getActionName() + " Selection",
|
||||
DoesntModifyDocument);
|
||||
transaction.execute(new cmd::SetMask(document, mask));
|
||||
transaction.commit();
|
||||
|
||||
document->generateMaskBoundaries();
|
||||
update_screen_for_document(document);
|
||||
}
|
||||
|
||||
std::string ModifySelectionCommand::onGetFriendlyName() const
|
||||
{
|
||||
std::string text;
|
||||
|
||||
text += getActionName();
|
||||
text += " Selection";
|
||||
|
||||
if (m_quantity > 0) {
|
||||
text += " by ";
|
||||
text += base::convert_to<std::string>(m_quantity);
|
||||
text += " pixel";
|
||||
if (m_quantity > 1)
|
||||
text += "s";
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
std::string ModifySelectionCommand::getActionName() const
|
||||
{
|
||||
switch (m_modifier) {
|
||||
case Border: return "Border";
|
||||
case Expand: return "Expand";
|
||||
case Contract: return "Contract";
|
||||
default: return "Modify";
|
||||
}
|
||||
}
|
||||
|
||||
// TODO create morphological operators/functions in "doc" namespace
|
||||
// TODO the impl is not optimal, but is good enough as a first version
|
||||
void ModifySelectionCommand::applyModifier(const Mask* srcMask, Mask* dstMask,
|
||||
const int radius,
|
||||
const doc::BrushType brush) const
|
||||
{
|
||||
const doc::Image* srcImage = srcMask->bitmap();
|
||||
const doc::Image* dstImage = dstMask->bitmap();
|
||||
|
||||
// Image bounds to clip get/put pixels
|
||||
const gfx::Rect srcBounds = srcImage->bounds();
|
||||
|
||||
// Create a kernel
|
||||
const int size = 2*radius+1;
|
||||
base::UniquePtr<doc::Image> kernel(doc::Image::create(IMAGE_BITMAP, size, size));
|
||||
doc::clear_image(kernel, 0);
|
||||
if (brush == doc::kCircleBrushType)
|
||||
doc::fill_ellipse(kernel, 0, 0, size-1, size-1, 1);
|
||||
else
|
||||
doc::fill_rect(kernel, 0, 0, size-1, size-1, 1);
|
||||
doc::put_pixel(kernel, radius, radius, 0);
|
||||
|
||||
int total = 0; // Number of 1s in the kernel image
|
||||
for (int v=0; v<size; ++v)
|
||||
for (int u=0; u<size; ++u)
|
||||
total += kernel->getPixel(u, v);
|
||||
|
||||
for (int y=-radius; y<srcBounds.h+radius; ++y) {
|
||||
for (int x=-radius; x<srcBounds.w+radius; ++x) {
|
||||
doc::color_t c;
|
||||
if (srcBounds.contains(x, y))
|
||||
c = srcImage->getPixel(x, y);
|
||||
else
|
||||
c = 0;
|
||||
|
||||
int accum = 0;
|
||||
for (int v=0; v<size; ++v) {
|
||||
for (int u=0; u<size; ++u) {
|
||||
if (kernel->getPixel(u, v)) {
|
||||
if (srcBounds.contains(x+u-radius, y+v-radius))
|
||||
accum += srcImage->getPixel(x-radius+u, y-radius+v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch (m_modifier) {
|
||||
case Border: {
|
||||
c = (c && accum < total) ? 1: 0;
|
||||
break;
|
||||
}
|
||||
case Expand: {
|
||||
c = (c || accum > 0) ? 1: 0;
|
||||
break;
|
||||
}
|
||||
case Contract: {
|
||||
c = (c && accum == total) ? 1: 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (c)
|
||||
doc::put_pixel(dstMask->bitmap(),
|
||||
srcMask->bounds().x+x,
|
||||
srcMask->bounds().y+y, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Command* CommandFactory::createModifySelectionCommand()
|
||||
{
|
||||
return new ModifySelectionCommand;
|
||||
}
|
||||
|
||||
} // namespace app
|
@ -67,6 +67,7 @@ FOR_EACH_COMMAND(MaskAll)
|
||||
FOR_EACH_COMMAND(MaskByColor)
|
||||
FOR_EACH_COMMAND(MaskContent)
|
||||
FOR_EACH_COMMAND(MergeDownLayer)
|
||||
FOR_EACH_COMMAND(ModifySelection)
|
||||
FOR_EACH_COMMAND(MoveCel)
|
||||
FOR_EACH_COMMAND(MoveMask)
|
||||
FOR_EACH_COMMAND(NewBrush)
|
||||
|
Loading…
x
Reference in New Issue
Block a user