diff --git a/data/gui.xml b/data/gui.xml index 2e93a7a2a..8af41d7f2 100644 --- a/data/gui.xml +++ b/data/gui.xml @@ -40,6 +40,7 @@ + @@ -593,6 +594,7 @@ + diff --git a/data/strings/en.ini b/data/strings/en.ini index db347f603..b6465a486 100644 --- a/data/strings/en.ini +++ b/data/strings/en.ini @@ -379,6 +379,7 @@ SliceProperties = Slice Properties SnapToGrid = Snap to Grid SpriteProperties = Sprite Properties SpriteSize = Sprite Size +Stroke = Stroke Selection Borders with Foreground Color SwitchColors = Switch Colors SwitchNonactiveLayersOpacity = Switch Nonactive Layers Opacity SymmetryMode = Symmetry Mode @@ -638,6 +639,7 @@ edit_copy_merged = Copy Mer&ged edit_paste = &Paste edit_clear = &Delete edit_fill = &Fill +edit_stroke = Stroke edit_rotate = R&otate edit_rotate_180 = &180 edit_rotate_90cw = &90 CW diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 2afb46968..472c8303d 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -196,7 +196,7 @@ if(ENABLE_UI) commands/cmd_exit.cpp commands/cmd_export_sprite_sheet.cpp commands/cmd_eyedropper.cpp - commands/cmd_fill.cpp + commands/cmd_fill_and_stroke.cpp commands/cmd_fit_screen.cpp commands/cmd_flatten_layers.cpp commands/cmd_flip.cpp @@ -391,7 +391,6 @@ if(ENABLE_UI) ui_context.cpp util/clipboard.cpp util/clipboard_native.cpp - util/fill_selection.cpp widget_loader.cpp) endif() diff --git a/src/app/cmd/clear_mask.cpp b/src/app/cmd/clear_mask.cpp index 30f6cda1c..491a3ff08 100644 --- a/src/app/cmd/clear_mask.cpp +++ b/src/app/cmd/clear_mask.cpp @@ -12,7 +12,7 @@ #include "app/cmd/clear_cel.h" #include "app/document.h" -#include "app/util/fill_selection.h" +#include "doc/algorithm/fill_selection.h" #include "doc/cel.h" #include "doc/image_impl.h" #include "doc/layer.h" @@ -89,7 +89,7 @@ void ClearMask::clear() app::Document* doc = static_cast(cel->document()); Mask* mask = doc->mask(); - fill_selection(image, m_offset, mask, m_bgcolor); + doc::algorithm::fill_selection(image, m_offset, mask, m_bgcolor); } void ClearMask::restore() diff --git a/src/app/commands/cmd_fill.cpp b/src/app/commands/cmd_fill_and_stroke.cpp similarity index 66% rename from src/app/commands/cmd_fill.cpp rename to src/app/commands/cmd_fill_and_stroke.cpp index 7fed484ae..7e08e17af 100644 --- a/src/app/commands/cmd_fill.cpp +++ b/src/app/commands/cmd_fill_and_stroke.cpp @@ -19,23 +19,28 @@ #include "app/transaction.h" #include "app/ui/editor/editor.h" #include "app/util/expand_cel_canvas.h" -#include "app/util/fill_selection.h" +#include "doc/algorithm/fill_selection.h" +#include "doc/algorithm/stroke_selection.h" #include "doc/mask.h" namespace app { class FillCommand : public Command { public: - FillCommand(); + enum Type { Fill, Stroke }; + FillCommand(Type type); Command* clone() const override { return new FillCommand(*this); } - protected: bool onEnabled(Context* ctx) override; void onExecute(Context* ctx) override; +private: + Type m_type; }; -FillCommand::FillCommand() - : Command(CommandId::Fill(), CmdUIOnlyFlag) +FillCommand::FillCommand(Type type) + : Command(type == Stroke ? CommandId::Stroke(): + CommandId::Fill(), CmdUIOnlyFlag) + , m_type(type) { } @@ -61,10 +66,11 @@ void FillCommand::onExecute(Context* ctx) Site site = *writer.site(); Document* doc = (app::Document*)site.document(); Sprite* sprite = site.sprite(); - Cel* cel = site.cel(); + Layer* layer = site.layer(); Mask* mask = doc->mask(); - if (!doc || !sprite || !cel || !mask || - !doc->isMaskVisible()) + if (!doc || !sprite || + !layer || !layer->isImage() || + !mask || !doc->isMaskVisible()) return; Preferences& pref = Preferences::instance(); @@ -74,7 +80,7 @@ void FillCommand::onExecute(Context* ctx) Transaction transaction(writer.context(), "Fill Selection with Foreground Color"); { ExpandCelCanvas expand( - site, cel->layer(), + site, layer, TiledMode::NONE, transaction, ExpandCelCanvas::None); @@ -84,19 +90,31 @@ void FillCommand::onExecute(Context* ctx) const gfx::Point offset = (mask->bounds().origin() - expand.getCel()->position()); + const doc::color_t docColor = + color_utils::color_for_layer( + color, layer); - fill_selection(expand.getDestCanvas(), - offset, - mask, - color_utils::color_for_layer(color, - cel->layer())); + if (m_type == Stroke) { + doc::algorithm::stroke_selection( + expand.getDestCanvas(), + offset, + mask, + docColor); + } + else { + doc::algorithm::fill_selection( + expand.getDestCanvas(), + offset, + mask, + docColor); + } expand.commit(); } // If the cel wasn't deleted by cmd::ClearMask, we trim it. - cel = ctx->activeSite().cel(); - if (cel && cel->layer()->isTransparent()) + Cel* cel = ctx->activeSite().cel(); + if (cel && layer->isTransparent()) transaction.execute(new cmd::TrimCel(cel)); transaction.commit(); @@ -107,7 +125,12 @@ void FillCommand::onExecute(Context* ctx) Command* CommandFactory::createFillCommand() { - return new FillCommand; + return new FillCommand(FillCommand::Fill); +} + +Command* CommandFactory::createStrokeCommand() +{ + return new FillCommand(FillCommand::Stroke); } } // namespace app diff --git a/src/app/commands/cmd_modify_selection.cpp b/src/app/commands/cmd_modify_selection.cpp index 80bfece02..288cda1e7 100644 --- a/src/app/commands/cmd_modify_selection.cpp +++ b/src/app/commands/cmd_modify_selection.cpp @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2015-2017 David Capello +// Copyright (C) 2015-2018 David Capello // // This program is distributed under the terms of // the End-User License Agreement for Aseprite. @@ -18,6 +18,7 @@ #include "app/pref/preferences.h" #include "app/transaction.h" #include "base/convert_to.h" +#include "doc/algorithm/modify_selection.h" #include "doc/brush_type.h" #include "doc/mask.h" #include "filters/neighboring_pixels.h" @@ -30,14 +31,13 @@ namespace app { using namespace doc; +typedef doc::algorithm::SelectionModifier Modifier; 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); } @@ -49,9 +49,6 @@ protected: 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; @@ -60,7 +57,7 @@ private: ModifySelectionCommand::ModifySelectionCommand() : Command(CommandId::ModifySelection(), CmdRecordableFlag) - , m_modifier(Expand) + , m_modifier(Modifier::Expand) , m_quantity(0) , m_brushType(doc::kCircleBrushType) { @@ -69,9 +66,9 @@ ModifySelectionCommand::ModifySelectionCommand() 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; + if (modifier == "border") m_modifier = Modifier::Border; + else if (modifier == "expand") m_modifier = Modifier::Expand; + else if (modifier == "contract") m_modifier = Modifier::Contract; const int quantity = params.get_as("quantity"); m_quantity = std::max(0, quantity); @@ -97,7 +94,7 @@ void ModifySelectionCommand::onExecute(Context* context) ModifySelectionWindow window; window.setText(getActionName() + " Selection"); - if (m_modifier == Border) + if (m_modifier == Modifier::Border) window.byLabel()->setText("Width:"); else window.byLabel()->setText(getActionName() + " By:"); @@ -131,11 +128,12 @@ void ModifySelectionCommand::onExecute(Context* context) Document* document(writer.document()); Sprite* sprite(writer.sprite()); - base::UniquePtr mask(new Mask()); + base::UniquePtr mask(new Mask); { mask->reserve(sprite->bounds()); mask->freeze(); - applyModifier(document->mask(), mask, quantity, brush); + doc::algorithm::modify_selection( + m_modifier, document->mask(), mask, quantity, brush); mask->unfreeze(); } @@ -164,81 +162,13 @@ std::string ModifySelectionCommand::onGetFriendlyName() const std::string ModifySelectionCommand::getActionName() const { switch (m_modifier) { - case Border: return Strings::commands_ModifySelection_Border(); - case Expand: return Strings::commands_ModifySelection_Expand(); - case Contract: return Strings::commands_ModifySelection_Contract(); + case Modifier::Border: return Strings::commands_ModifySelection_Border(); + case Modifier::Expand: return Strings::commands_ModifySelection_Expand(); + case Modifier::Contract: return Strings::commands_ModifySelection_Contract(); default: return Strings::commands_ModifySelection_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(); - 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 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; vgetPixel(u, v); - - for (int y=-radius; ygetPixel(x, y); - else - c = 0; - - int accum = 0; - for (int v=0; vgetPixel(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(dstImage, - srcMask->bounds().x+x, - srcMask->bounds().y+y, 1); - } - } -} - Command* CommandFactory::createModifySelectionCommand() { return new ModifySelectionCommand; diff --git a/src/app/commands/commands_list.h b/src/app/commands/commands_list.h index 137c42554..a981ac9fe 100644 --- a/src/app/commands/commands_list.h +++ b/src/app/commands/commands_list.h @@ -139,6 +139,7 @@ FOR_EACH_COMMAND(ShowSlices) FOR_EACH_COMMAND(SliceProperties) FOR_EACH_COMMAND(SnapToGrid) FOR_EACH_COMMAND(SpriteProperties) +FOR_EACH_COMMAND(Stroke) FOR_EACH_COMMAND(SwitchColors) FOR_EACH_COMMAND(SymmetryMode) FOR_EACH_COMMAND(TiledMode) diff --git a/src/app/util/fill_selection.h b/src/app/util/fill_selection.h deleted file mode 100644 index 6eaf27999..000000000 --- a/src/app/util/fill_selection.h +++ /dev/null @@ -1,28 +0,0 @@ -// Aseprite -// Copyright (C) 2018 David Capello -// -// This program is distributed under the terms of -// the End-User License Agreement for Aseprite. - -#ifndef APP_UTIL_FILL_SELECTION_H_INCLUDED -#define APP_UTIL_FILL_SELECTION_H_INCLUDED -#pragma once - -#include "doc/color.h" -#include "gfx/point.h" - -namespace doc { - class Image; - class Mask; -} - -namespace app { - - void fill_selection(doc::Image* image, - const gfx::Point& offset, - const doc::Mask* mask, - const doc::color_t color); - -} // namespace app - -#endif diff --git a/src/doc/CMakeLists.txt b/src/doc/CMakeLists.txt index 1414effb2..3d2c39862 100644 --- a/src/doc/CMakeLists.txt +++ b/src/doc/CMakeLists.txt @@ -7,14 +7,17 @@ endif() add_library(doc-lib algo.cpp + algorithm/fill_selection.cpp algorithm/flip_image.cpp algorithm/floodfill.cpp + algorithm/modify_selection.cpp algorithm/polygon.cpp algorithm/resize_image.cpp algorithm/rotate.cpp algorithm/rotsprite.cpp algorithm/shift_image.cpp algorithm/shrink_bounds.cpp + algorithm/stroke_selection.cpp anidir.cpp blend_funcs.cpp blend_mode.cpp diff --git a/src/app/util/fill_selection.cpp b/src/doc/algorithm/fill_selection.cpp similarity index 70% rename from src/app/util/fill_selection.cpp rename to src/doc/algorithm/fill_selection.cpp index 89e4b4d5b..0caf9191e 100644 --- a/src/app/util/fill_selection.cpp +++ b/src/doc/algorithm/fill_selection.cpp @@ -1,30 +1,30 @@ -// Aseprite -// Copyright (C) 2018 David Capello +// Aseprite Document Library +// Copyright (c) 2018 David Capello // -// This program is distributed under the terms of -// the End-User License Agreement for Aseprite. +// 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 "app/util/fill_selection.h" +#include "doc/algorithm/fill_selection.h" #include "doc/image_impl.h" #include "doc/mask.h" #include "doc/primitives.h" -namespace app { - -using namespace doc; +namespace doc { +namespace algorithm { void fill_selection(Image* image, const gfx::Point& offset, const Mask* mask, const color_t color) { + ASSERT(mask); ASSERT(mask->bitmap()); - if (!mask->bitmap()) + if (!mask || !mask->bitmap()) return; const LockImageBits maskBits(mask->bitmap()); @@ -45,4 +45,5 @@ void fill_selection(Image* image, ASSERT(it == maskBits.end()); } -} // namespace app +} // namespace algorithm +} // namespace doc diff --git a/src/doc/algorithm/fill_selection.h b/src/doc/algorithm/fill_selection.h new file mode 100644 index 000000000..237c230df --- /dev/null +++ b/src/doc/algorithm/fill_selection.h @@ -0,0 +1,27 @@ +// Aseprite Document Library +// Copyright (c) 2018 David Capello +// +// This file is released under the terms of the MIT license. +// Read LICENSE.txt for more information. + +#ifndef DOC_ALGORITHM_FILL_SELECTION_H_INCLUDED +#define DOC_ALGORITHM_FILL_SELECTION_H_INCLUDED +#pragma once + +#include "doc/color.h" +#include "gfx/point.h" + +namespace doc { + class Image; + class Mask; + namespace algorithm { + + void fill_selection(Image* image, + const gfx::Point& offset, + const Mask* mask, + const color_t color); + + } // namespace algorithm +} // namespace doc + +#endif diff --git a/src/doc/algorithm/modify_selection.cpp b/src/doc/algorithm/modify_selection.cpp new file mode 100644 index 000000000..1b01c97c4 --- /dev/null +++ b/src/doc/algorithm/modify_selection.cpp @@ -0,0 +1,96 @@ +// Aseprite Document Library +// Copyright (c) 2018 David Capello +// +// 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/algorithm/modify_selection.h" + +#include "doc/image_impl.h" +#include "doc/mask.h" +#include "doc/primitives.h" + +#include + +namespace doc { +namespace algorithm { + +// TODO create morphological operators/functions in "doc" namespace +// TODO the impl is not optimal, but is good enough as a first version +void modify_selection(const SelectionModifier modifier, + const Mask* srcMask, + Mask* dstMask, + const int radius, + const doc::BrushType brush) +{ + const doc::Image* srcImage = srcMask->bitmap(); + doc::Image* dstImage = dstMask->bitmap(); + const gfx::Point offset = + srcMask->bounds().origin() - + dstMask->bounds().origin(); + + // Image bounds to clip get/put pixels + const gfx::Rect srcBounds = srcImage->bounds(); + + // Create a kernel + const int size = 2*radius+1; + std::unique_ptr kernel(doc::Image::create(IMAGE_BITMAP, size, size)); + doc::clear_image(kernel.get(), 0); + if (brush == doc::kCircleBrushType) + doc::fill_ellipse(kernel.get(), 0, 0, size-1, size-1, 1); + else + doc::fill_rect(kernel.get(), 0, 0, size-1, size-1, 1); + doc::put_pixel(kernel.get(), radius, radius, 0); + + int total = 0; // Number of 1s in the kernel image + for (int v=0; vgetPixel(u, v); + + for (int y=-radius; ygetPixel(x, y); + else + c = 0; + + int accum = 0; + for (int v=0; vgetPixel(u, v)) { + if (srcBounds.contains(x+u-radius, y+v-radius)) + accum += srcImage->getPixel(x-radius+u, y-radius+v); + } + } + } + + switch (modifier) { + case SelectionModifier::Border: { + c = (c && accum < total) ? 1: 0; + break; + } + case SelectionModifier::Expand: { + c = (c || accum > 0) ? 1: 0; + break; + } + case SelectionModifier::Contract: { + c = (c && accum == total) ? 1: 0; + break; + } + } + + if (c) + doc::put_pixel(dstImage, + offset.x+x, + offset.y+y, 1); + } + } +} + +} // namespace algorithm +} // namespace app diff --git a/src/doc/algorithm/modify_selection.h b/src/doc/algorithm/modify_selection.h new file mode 100644 index 000000000..a61226197 --- /dev/null +++ b/src/doc/algorithm/modify_selection.h @@ -0,0 +1,34 @@ +// Aseprite Document Library +// Copyright (c) 2018 David Capello +// +// This file is released under the terms of the MIT license. +// Read LICENSE.txt for more information. + +#ifndef DOC_ALGORITHM_MODIFY_SELECTION_H_INCLUDED +#define DOC_ALGORITHM_MODIFY_SELECTION_H_INCLUDED +#pragma once + +#include "doc/brush_type.h" +#include "doc/color.h" +#include "gfx/point.h" + +namespace doc { + class Mask; + namespace algorithm { + + enum class SelectionModifier { + Border, + Expand, + Contract, + }; + + void modify_selection(const SelectionModifier modifier, + const Mask* srcMask, + Mask* dstMask, + const int radius, + const BrushType brush); + + } // namespace algorithm +} // namespace doc + +#endif diff --git a/src/doc/algorithm/stroke_selection.cpp b/src/doc/algorithm/stroke_selection.cpp new file mode 100644 index 000000000..8d134f223 --- /dev/null +++ b/src/doc/algorithm/stroke_selection.cpp @@ -0,0 +1,51 @@ +// Aseprite Document Library +// Copyright (c) 2018 David Capello +// +// 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/algorithm/stroke_selection.h" + +#include "doc/algorithm/fill_selection.h" +#include "doc/algorithm/modify_selection.h" +#include "doc/mask.h" + +namespace doc { +namespace algorithm { + +void stroke_selection(Image* image, + const gfx::Point& offset, + const Mask* origMask, + const color_t color) +{ + ASSERT(origMask); + ASSERT(origMask->bitmap()); + if (!origMask || !origMask->bitmap()) + return; + + gfx::Rect bounds = origMask->bounds(); + if (bounds.isEmpty()) + return; + + Mask mask; + mask.reserve(bounds); + mask.freeze(); + modify_selection( + SelectionModifier::Border, + origMask, &mask, 1, + BrushType::kCircleBrushType); + mask.unfreeze(); + + // Both mask must have the same bounds. + ASSERT(mask.bounds() == origMask->bounds()); + + if (mask.bitmap()) + fill_selection(image, offset, &mask, color); +} + +} // namespace algorithm +} // namespace app diff --git a/src/doc/algorithm/stroke_selection.h b/src/doc/algorithm/stroke_selection.h new file mode 100644 index 000000000..3eca85f6d --- /dev/null +++ b/src/doc/algorithm/stroke_selection.h @@ -0,0 +1,27 @@ +// Aseprite Document Library +// Copyright (c) 2018 David Capello +// +// This file is released under the terms of the MIT license. +// Read LICENSE.txt for more information. + +#ifndef DOC_ALGORITHM_STROKE_SELECTION_H_INCLUDED +#define DOC_ALGORITHM_STROKE_SELECTION_H_INCLUDED +#pragma once + +#include "doc/color.h" +#include "gfx/point.h" + +namespace doc { + class Image; + class Mask; + namespace algorithm { + + void stroke_selection(Image* image, + const gfx::Point& offset, + const Mask* mask, + const color_t color); + + } // namespace algorithm +} // namespace doc + +#endif diff --git a/src/doc/mask.h b/src/doc/mask.h index 6e0b94742..6e1744896 100644 --- a/src/doc/mask.h +++ b/src/doc/mask.h @@ -20,6 +20,8 @@ namespace doc { // Represents the selection (selected pixels, 0/1, 0=non-selected, 1=selected) + // + // TODO rename Mask -> Selection class Mask : public Object { public: Mask();