Add skew transformation (fix #71)

This is the first version of the feature, it still needs some
fixes (e.g. avoid skew transform when the pivot is in the same side of
the skew handle which can calculate a division by zero).
This commit is contained in:
David Capello 2020-09-18 19:29:43 -03:00
parent 98d06c31e3
commit 3fbdd40f24
27 changed files with 863 additions and 389 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -423,6 +423,14 @@
<part id="tiles_manual" x="150" y="208" w="6" h="6" />
<part id="tiles_auto" x="156" y="208" w="6" h="6" />
<part id="tiles_stack" x="162" y="208" w="6" h="6" />
<part id="cursor_skew_n" x="272" y="192" w="16" h="16" focusx="8" focusy="8" />
<part id="cursor_skew_s" x="288" y="192" w="16" h="16" focusx="8" focusy="8" />
<part id="cursor_skew_sw" x="272" y="208" w="16" h="16" focusx="8" focusy="8" />
<part id="cursor_skew_se" x="288" y="208" w="16" h="16" focusx="8" focusy="8" />
<part id="cursor_skew_w" x="272" y="176" w="16" h="16" focusx="8" focusy="8" />
<part id="cursor_skew_e" x="288" y="176" w="16" h="16" focusx="8" focusy="8" />
<part id="cursor_skew_nw" x="272" y="160" w="16" h="16" focusx="8" focusy="8" />
<part id="cursor_skew_ne" x="288" y="160" w="16" h="16" focusx="8" focusy="8" />
</parts>
<styles>
<style id="box" />

View File

@ -590,6 +590,12 @@
locks the movement to one axis (X or Y) -->
<key action="LockAxis" shortcut="Shift" />
<!-- When we are moving the selection, pressing this shortcut
moves the selection to a subpixel level (floating point
coordinates). Useful for anti-aliasing effects. -->
<key action="FineControl" shortcut="Ctrl" context="TranslatingSelection" />
<key action="FineControl" shortcut="Ctrl" context="ScalingSelection" />
<!-- When you rotate the selection, pressing this
keyboard shortcut you activate angle snap -->
<key action="AngleSnap" shortcut="Shift" />

View File

@ -58,4 +58,11 @@ void ExtraCel::create(const TilemapMode tilemapMode,
m_cel->setFrame(frame);
}
void ExtraCel::reset()
{
m_type = render::ExtraType::NONE;
m_image.reset();
m_cel.reset();
}
} // namespace app

View File

@ -37,6 +37,7 @@ namespace app {
const gfx::Size& imageSize,
const doc::frame_t frame,
const int opacity);
void reset();
render::ExtraType type() const { return m_type; }
void setType(render::ExtraType type) { m_type = type; }

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2020 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -10,7 +11,6 @@
#include "app/transformation.h"
#include "fixmath/fixmath.h"
#include "gfx/point.h"
#include "gfx/size.h"
@ -22,77 +22,66 @@ using namespace gfx;
Transformation::Transformation()
{
m_bounds.x = 0;
m_bounds.y = 0;
m_bounds.w = 0;
m_bounds.h = 0;
m_angle = 0.0;
m_pivot.x = 0;
m_pivot.y = 0;
}
Transformation::Transformation(const RectF& bounds)
: m_bounds(bounds)
{
m_angle = 0.0;
m_pivot.x = bounds.x + bounds.w/2;
m_pivot.y = bounds.y + bounds.h/2;
}
void Transformation::transformBox(Corners& corners) const
Transformation::Corners Transformation::transformedCorners() const
{
Corners corners(m_bounds);
// TODO We could create a composed 4x4 matrix with all
// transformation and apply the same matrix to avoid calling
// rotatePoint/cos/sin functions 4 times, anyway, it's not
// critical at this point.
corners = m_bounds;
for (std::size_t c=0; c<corners.size(); ++c)
corners[c] = Transformation::rotatePoint(corners[c], m_pivot, m_angle);
corners[c] = Transformation::rotatePoint(corners[c], m_pivot,
m_angle, m_skew);
return corners;
}
void Transformation::displacePivotTo(const PointF& newPivot)
{
// Calculate the rotated corners
Corners corners;
transformBox(corners);
Corners corners = transformedCorners();
// Rotate-back (-angle) the position of the rotated origin (corners[0])
// using the new pivot.
PointF newBoundsOrigin =
rotatePoint(corners.leftTop(),
newPivot,
-m_angle);
// Rotate-back the position of the rotated origin (corners[0]) using
// the new pivot.
PointF pt = corners.leftTop();
pt = rotatePoint(pt, newPivot, -m_angle, 0.0);
pt = rotatePoint(pt, newPivot, 0.0, -m_skew);
// Change the new pivot.
m_pivot = newPivot;
m_bounds = RectF(newBoundsOrigin, m_bounds.size());
m_bounds = RectF(pt, m_bounds.size());
}
PointF Transformation::rotatePoint(
const PointF& point,
const PointF& pivot,
double angle)
const double angle,
const double skew)
{
using namespace fixmath;
fixed fixangle = ftofix(-angle);
fixangle = fixmul(fixangle, radtofix_r);
fixed cos = fixcos(fixangle);
fixed sin = fixsin(fixangle);
fixed dx = fixsub(ftofix(point.x), ftofix(pivot.x));
fixed dy = fixsub(ftofix(point.y), ftofix(pivot.y));
return PointF(
fixtof(fixadd(ftofix(pivot.x), fixsub(fixmul(dx, cos), fixmul(dy, sin)))),
fixtof(fixadd(ftofix(pivot.y), fixadd(fixmul(dy, cos), fixmul(dx, sin)))));
double cos = std::cos(-angle);
double sin = std::sin(-angle);
double tan = std::tan(skew);
double dx = point.x - pivot.x;
double dy = point.y - pivot.y;
dx += dy*tan;
return PointF(pivot.x + dx*cos - dy*sin,
pivot.y + dx*sin + dy*cos);
}
RectF Transformation::transformedBounds() const
{
// Get transformed corners
Corners corners;
transformBox(corners);
Corners corners = transformedCorners();
// Create a union of all corners
RectF bounds;

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2020 Igara Studio S.A.
// Copyright (C) 2001-2016 David Capello
//
// This program is distributed under the terms of
@ -29,6 +30,16 @@ public:
};
Corners() : m_corners(NUM_OF_CORNERS) { }
Corners(const gfx::RectF bounds) : m_corners(NUM_OF_CORNERS) {
m_corners[LEFT_TOP].x = bounds.x;
m_corners[LEFT_TOP].y = bounds.y;
m_corners[RIGHT_TOP].x = bounds.x2();
m_corners[RIGHT_TOP].y = bounds.y;
m_corners[RIGHT_BOTTOM].x = bounds.x2();
m_corners[RIGHT_BOTTOM].y = bounds.y2();
m_corners[LEFT_BOTTOM].x = bounds.x;
m_corners[LEFT_BOTTOM].y = bounds.y2();
}
std::size_t size() const { return m_corners.size(); }
@ -45,18 +56,6 @@ public:
void rightBottom(const gfx::PointF& pt) { m_corners[RIGHT_BOTTOM] = pt; }
void leftBottom(const gfx::PointF& pt) { m_corners[LEFT_BOTTOM] = pt; }
Corners& operator=(const gfx::RectF bounds) {
m_corners[LEFT_TOP].x = bounds.x;
m_corners[LEFT_TOP].y = bounds.y;
m_corners[RIGHT_TOP].x = bounds.x + bounds.w;
m_corners[RIGHT_TOP].y = bounds.y;
m_corners[RIGHT_BOTTOM].x = bounds.x + bounds.w;
m_corners[RIGHT_BOTTOM].y = bounds.y + bounds.h;
m_corners[LEFT_BOTTOM].x = bounds.x;
m_corners[LEFT_BOTTOM].y = bounds.y + bounds.h;
return *this;
}
gfx::RectF bounds() const {
gfx::RectF bounds;
for (int i=0; i<Corners::NUM_OF_CORNERS; ++i)
@ -76,14 +75,16 @@ public:
const gfx::RectF& bounds() const { return m_bounds; }
const gfx::PointF& pivot() const { return m_pivot; }
double angle() const { return m_angle; }
double skew() const { return m_skew; }
void bounds(const gfx::RectF& bounds) { m_bounds = bounds; }
void pivot(const gfx::PointF& pivot) { m_pivot = pivot; }
void angle(double angle) { m_angle = angle; }
void skew(double angle) { m_skew = angle; }
// Applies the transformation (rotation with angle/pivot) to the
// current bounds (m_bounds).
void transformBox(Corners& corners) const;
Corners transformedCorners() const;
// Changes the pivot to another location, adjusting the bounds to
// keep the current rotated-corners in the same location.
@ -94,12 +95,14 @@ public:
// Static helper method to rotate points.
static gfx::PointF rotatePoint(const gfx::PointF& point,
const gfx::PointF& pivot,
double angle);
const double angle,
const double skew);
private:
gfx::RectF m_bounds;
gfx::PointF m_pivot;
double m_angle;
gfx::RectF m_bounds = gfx::RectF(0.0, 0.0, 0.0, 0.0);
gfx::PointF m_pivot = gfx::PointF(0.0, 0.0);
double m_angle = 0.0;
double m_skew = 0.0;
};
} // namespace app

View File

@ -41,6 +41,7 @@
#include "app/ui/dithering_selector.h"
#include "app/ui/dynamics_popup.h"
#include "app/ui/editor/editor.h"
#include "app/ui/expr_entry.h"
#include "app/ui/icon_button.h"
#include "app/ui/keyboard_shortcuts.h"
#include "app/ui/selection_mode_field.h"
@ -48,6 +49,7 @@
#include "app/ui_context.h"
#include "base/clamp.h"
#include "base/fs.h"
#include "base/pi.h"
#include "base/scoped_value.h"
#include "doc/brush.h"
#include "doc/image.h"
@ -55,6 +57,7 @@
#include "doc/remap.h"
#include "doc/selected_objects.h"
#include "doc/slice.h"
#include "fmt/format.h"
#include "obs/connection.h"
#include "os/surface.h"
#include "os/system.h"
@ -969,6 +972,156 @@ private:
bool m_lockChange;
};
class ContextBar::TransformationFields : public HBox {
public:
class CustomEntry : public ExprEntry {
public:
CustomEntry() {
setDecimals(1);
}
private:
bool onProcessMessage(Message* msg) override {
switch (msg->type()) {
case kKeyDownMessage:
if (ExprEntry::onProcessMessage(msg))
return true;
else if (hasFocus() && manager()->processFocusMovementMessage(msg))
return true;
return false;
case kFocusLeaveMessage: {
bool res = ExprEntry::onProcessMessage(msg);
deselectText();
setCaretPos(0);
return res;
}
}
return ExprEntry::onProcessMessage(msg);
}
void onVisible(bool visible) override {
if (!visible && hasFocus()) {
releaseFocus();
}
ExprEntry::onVisible(visible);
}
void onFormatExprFocusLeave(std::string& buf) override {
buf = formatDec(onGetTextDouble());
}
};
TransformationFields() {
m_angle.setSuffix("°");
m_skew.setSuffix("°");
addChild(new Label("P:"));
addChild(&m_x);
addChild(&m_y);
addChild(&m_w);
addChild(&m_h);
addChild(new Label("R:"));
addChild(&m_angle);
addChild(&m_skew);
InitTheme.connect(
[this]{
gfx::Size sz(
font()->textLength("8")*4 + m_x.border().width(),
std::numeric_limits<int>::max());
setChildSpacing(0);
m_x.setMaxSize(sz);
m_y.setMaxSize(sz);
m_w.setMaxSize(sz);
m_h.setMaxSize(sz);
m_angle.setMaxSize(sz);
m_skew.setMaxSize(sz);
});
initTheme();
m_x.Change.connect([this]{ auto rc = bounds(); rc.x = m_x.textDouble(); onChangePos(rc); });
m_y.Change.connect([this]{ auto rc = bounds(); rc.y = m_y.textDouble(); onChangePos(rc); });
m_w.Change.connect([this]{ auto rc = bounds(); rc.w = m_w.textDouble(); onChangeSize(rc); });
m_h.Change.connect([this]{ auto rc = bounds(); rc.h = m_h.textDouble(); onChangeSize(rc); });
m_angle.Change.connect([this]{ onChangeAngle(); });
m_skew.Change.connect([this]{ onChangeSkew(); });
}
void update(const Transformation& t) {
auto rc = t.bounds();
m_x.setText(formatDec(rc.x));
m_y.setText(formatDec(rc.y));
m_w.setText(formatDec(rc.w));
m_h.setText(formatDec(rc.h));
m_angle.setText(formatDec(180.0 * t.angle() / PI));
m_skew.setText(formatDec(180.0 * t.skew() / PI));
m_t = t;
}
private:
static std::string formatDec(const double x) {
std::string s = fmt::format("{:0.1f}", x);
if (s.size() > 2 &&
s[s.size()-1] == '0' &&
s[s.size()-2] == '.') {
s.erase(s.size()-2, 2);
}
return s;
}
gfx::RectF bounds() const {
return m_t.bounds();
}
void onChangePos(gfx::RectF newBounds) {
// Adjust new pivot position depending on the new bounds origin
gfx::RectF bounds = m_t.bounds();
gfx::PointF pivot = m_t.pivot();
if (!bounds.isEmpty()) {
pivot.x = (pivot.x - bounds.x) / bounds.w;
pivot.y = (pivot.y - bounds.y) / bounds.h;
pivot.x = newBounds.x + pivot.x*newBounds.w;
pivot.y = newBounds.y + pivot.y*newBounds.h;
m_t.pivot(pivot);
}
m_t.bounds(newBounds);
updateEditor();
}
void onChangeSize(gfx::RectF newBounds) {
// Adjust bounds origin depending on the new size and the current pivot
gfx::RectF bounds = m_t.bounds();
gfx::PointF pivot = m_t.pivot();
if (!bounds.isEmpty()) {
pivot.x = (pivot.x - bounds.x) / bounds.w;
pivot.y = (pivot.y - bounds.y) / bounds.h;
newBounds.x -= (newBounds.w-bounds.w)*pivot.x;
newBounds.y -= (newBounds.h-bounds.h)*pivot.y;
m_x.setText(formatDec(newBounds.x));
m_y.setText(formatDec(newBounds.y));
}
m_t.bounds(newBounds);
updateEditor();
}
void onChangeAngle() {
m_t.angle(PI * m_angle.textDouble() / 180.0);
updateEditor();
}
void onChangeSkew() {
m_t.skew(PI * m_skew.textDouble() / 180.0);
updateEditor();
}
void updateEditor() {
if (current_editor)
current_editor->updateTransformation(m_t);
}
CustomEntry m_x, m_y, m_w, m_h;
CustomEntry m_angle;
CustomEntry m_skew;
Transformation m_t;
};
class ContextBar::DynamicsField : public ButtonSet
, public DynamicsPopup::Delegate {
public:
@ -1529,10 +1682,13 @@ private:
ContextBar::ContextBar(TooltipManager* tooltipManager,
ColorBar* colorBar)
{
auto& pref = Preferences::instance();
addChild(m_selectionOptionsBox = new HBox());
m_selectionOptionsBox->addChild(m_dropPixels = new DropPixelsField());
m_selectionOptionsBox->addChild(m_selectionMode = new SelectionModeField);
m_selectionOptionsBox->addChild(m_transparentColor = new TransparentColorField(this, tooltipManager));
m_selectionOptionsBox->addChild(m_transformation = new TransformationFields);
m_selectionOptionsBox->addChild(m_pivot = new PivotField);
m_selectionOptionsBox->addChild(m_rotAlgo = new RotAlgorithmField());
@ -1573,7 +1729,7 @@ ContextBar::ContextBar(TooltipManager* tooltipManager,
m_freehandBox->addChild(m_freehandAlgo = new FreehandAlgorithmField());
addChild(m_symmetry = new SymmetryField());
m_symmetry->setVisible(Preferences::instance().symmetryMode.enabled());
m_symmetry->setVisible(pref.symmetryMode.enabled());
addChild(m_sliceFields = new SliceFields);
@ -1582,7 +1738,6 @@ ContextBar::ContextBar(TooltipManager* tooltipManager,
App::instance()->activeToolManager()->add_observer(this);
UIContext::instance()->add_observer(this);
auto& pref = Preferences::instance();
pref.symmetryMode.enabled.AfterChange.connect(
[this]{ onSymmetryModeChange(); });
pref.colorBar.fgColor.AfterChange.connect(
@ -1927,6 +2082,7 @@ void ContextBar::updateForTool(tools::Tool* tool)
m_selectionMode->setVisible(true);
m_pivot->setVisible(true);
m_dropPixels->setVisible(false);
m_transformation->setVisible(false);
m_selectBoxHelp->setVisible(false);
m_symmetry->setVisible(
@ -1943,7 +2099,7 @@ void ContextBar::updateForTool(tools::Tool* tool)
layout();
}
void ContextBar::updateForMovingPixels()
void ContextBar::updateForMovingPixels(const Transformation& t)
{
tools::Tool* tool = App::instance()->toolBox()->getToolById(
tools::WellKnownTools::RectangularMarquee);
@ -1953,6 +2109,8 @@ void ContextBar::updateForMovingPixels()
m_dropPixels->deselectItems();
m_dropPixels->setVisible(true);
m_selectionMode->setVisible(false);
m_transformation->setVisible(true);
m_transformation->update(t);
layout();
}

View File

@ -53,6 +53,7 @@ namespace app {
class ColorBar;
class DitheringSelector;
class GradientTypeSelector;
class Transformation;
class ContextBar : public DocObserverWidget<ui::HBox>
, public obs::observable<ContextBarObserver>
@ -64,7 +65,7 @@ namespace app {
void updateForActiveTool();
void updateForTool(tools::Tool* tool);
void updateForMovingPixels();
void updateForMovingPixels(const Transformation& t);
void updateForSelectingBox(const std::string& text);
void updateToolLoopModifiersIndicators(tools::ToolLoopModifiers modifiers);
void updateAutoSelectLayer(bool state);
@ -148,6 +149,7 @@ namespace app {
class TransparentColorField;
class PivotField;
class RotAlgorithmField;
class TransformationFields;
class DynamicsField;
class FreehandAlgorithmField;
class BrushPatternField;
@ -187,6 +189,7 @@ namespace app {
TransparentColorField* m_transparentColor;
PivotField* m_pivot;
RotAlgorithmField* m_rotAlgo;
TransformationFields* m_transformation = nullptr;
DropPixelsField* m_dropPixels;
doc::BrushRef m_activeBrush;
ui::Label* m_selectBoxHelp;

View File

@ -116,8 +116,8 @@ protected:
case kKeyUpMessage:
if (static_cast<KeyMessage*>(msg)->repeat() == 0) {
KeyboardShortcuts* keys = KeyboardShortcuts::instance();
KeyPtr lmb = keys->action(KeyAction::LeftMouseButton);
KeyPtr rmb = keys->action(KeyAction::RightMouseButton);
KeyPtr lmb = keys->action(KeyAction::LeftMouseButton, KeyContext::Any);
KeyPtr rmb = keys->action(KeyAction::RightMouseButton, KeyContext::Any);
// Convert action keys into mouse messages.
if (lmb->isPressed(msg, *keys) ||

View File

@ -681,7 +681,8 @@ void Editor::drawOneSpriteUnclippedRect(ui::Graphics* g, const gfx::Rect& sprite
}
ExtraCelRef extraCel = m_document->extraCel();
if (extraCel && extraCel->type() != render::ExtraType::NONE) {
if (extraCel &&
extraCel->type() != render::ExtraType::NONE) {
m_renderEngine->setExtraImage(
extraCel->type(),
extraCel->cel(),
@ -1307,7 +1308,7 @@ void Editor::flashCurrentLayer()
return;
Site site = getSite();
if (const Cel* src_cel = site.cel()) {
if (site.cel()) {
// Hide and destroy the extra cel used by the brush preview
// because we'll need to use the extra cel now for the flashing
// layer.
@ -2514,7 +2515,7 @@ void Editor::pasteImage(const Image* image, const Mask* mask)
void Editor::startSelectionTransformation(const gfx::Point& move, double angle)
{
if (auto movingPixels = dynamic_cast<MovingPixelsState*>(m_state.get())) {
movingPixels->translate(move);
movingPixels->translate(gfx::PointF(move));
if (std::fabs(angle) > 1e-5)
movingPixels->rotate(angle);
}
@ -2531,6 +2532,12 @@ void Editor::startFlipTransformation(doc::algorithm::FlipType flipType)
standby->startFlipTransformation(this, flipType);
}
void Editor::updateTransformation(const Transformation& transform)
{
if (auto movingPixels = dynamic_cast<MovingPixelsState*>(m_state.get()))
movingPixels->updateTransformation(transform);
}
void Editor::notifyScrollChanged()
{
m_observers.notifyScrollChanged(this);

View File

@ -60,6 +60,7 @@ namespace app {
class EditorRender;
class PixelsMovement;
class Site;
class Transformation;
namespace tools {
class Ink;
@ -242,8 +243,8 @@ namespace app {
void pasteImage(const Image* image, const Mask* mask = nullptr);
void startSelectionTransformation(const gfx::Point& move, double angle);
void startFlipTransformation(doc::algorithm::FlipType flipType);
void updateTransformation(const Transformation& transform);
// Used by EditorView to notify changes in the view's scroll
// position.

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2020 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello
//
// This program is distributed under the terms of
@ -22,10 +23,12 @@ namespace app {
ScaleNWHandle, ScaleNHandle, ScaleNEHandle,
ScaleWHandle, ScaleEHandle,
ScaleSWHandle, ScaleSHandle, ScaleSEHandle,
// One of the region's corders to rotate.
RotateNWHandle, RotateNHandle, RotateNEHandle,
RotateWHandle, RotateEHandle,
RotateSWHandle, RotateSHandle, RotateSEHandle,
// Rotate from corners
RotateNWHandle, RotateNEHandle,
RotateSWHandle, RotateSEHandle,
// Skew from sides
SkewNHandle, SkewWHandle,
SkewEHandle, SkewSHandle,
// Handle used to move the pivot
PivotHandle,
};

View File

@ -26,6 +26,7 @@
#include "app/pref/preferences.h"
#include "app/tools/ink.h"
#include "app/tools/tool.h"
#include "app/transformation.h"
#include "app/ui/context_bar.h"
#include "app/ui/editor/editor.h"
#include "app/ui/editor/editor_customization_delegate.h"
@ -63,9 +64,9 @@ MovingPixelsState::MovingPixelsState(Editor* editor, MouseMessage* msg, PixelsMo
, m_observingEditor(false)
, m_discarded(false)
, m_renderTimer(50)
, m_oldSpritePos(std::numeric_limits<int>::min(),
std::numeric_limits<int>::min())
{
m_pixelsMovement->setDelegate(this);
// MovingPixelsState needs a selection tool to avoid problems
// sharing the extra cel between the drawing cursor preview and the
// pixels movement/transformation preview.
@ -75,7 +76,7 @@ MovingPixelsState::MovingPixelsState(Editor* editor, MouseMessage* msg, PixelsMo
if (handle != NoHandle) {
gfx::Point pt = editor->screenToEditor(msg->position());
m_pixelsMovement->catchImage(pt, handle);
m_pixelsMovement->catchImage(gfx::PointF(pt), handle);
editor->captureMouse();
}
@ -111,7 +112,7 @@ MovingPixelsState::MovingPixelsState(Editor* editor, MouseMessage* msg, PixelsMo
m_observingEditor = true;
ContextBar* contextBar = App::instance()->contextBar();
contextBar->updateForMovingPixels();
contextBar->updateForMovingPixels(getTransformation(editor));
contextBar->add_observer(this);
}
@ -131,12 +132,12 @@ MovingPixelsState::~MovingPixelsState()
m_editor->document()->generateMaskBoundaries();
}
void MovingPixelsState::translate(const gfx::Point& delta)
void MovingPixelsState::translate(const gfx::PointF& delta)
{
if (m_pixelsMovement->isDragging())
m_pixelsMovement->dropImageTemporarily();
m_pixelsMovement->catchImageAgain(gfx::Point(0, 0), MovePixelsHandle);
m_pixelsMovement->catchImageAgain(gfx::PointF(0, 0), MovePixelsHandle);
m_pixelsMovement->moveImage(delta, PixelsMovement::NormalMovement);
m_pixelsMovement->dropImageTemporarily();
m_editor->updateStatusBar();
@ -160,6 +161,12 @@ void MovingPixelsState::shift(int dx, int dy)
m_editor->updateStatusBar();
}
void MovingPixelsState::updateTransformation(const Transformation& t)
{
m_pixelsMovement->setTransformation(t);
m_editor->updateStatusBar();
}
void MovingPixelsState::onEnterState(Editor* editor)
{
StandbyState::onEnterState(editor);
@ -174,7 +181,7 @@ void MovingPixelsState::onEditorGotFocus(Editor* editor)
// when we are back to an editor in MovingPixelsState. Without this
// we would see the SelectionModeField instead which doesn't make
// sense on MovingPixelsState).
contextBar->updateForMovingPixels();
contextBar->updateForMovingPixels(getTransformation(editor));
}
EditorState::LeaveAction MovingPixelsState::onLeaveState(Editor* editor, EditorState* newState)
@ -255,7 +262,7 @@ void MovingPixelsState::onActiveToolChange(Editor* editor, tools::Tool* tool)
else if (tool->getInk(0)->isSelection() ||
tool->getInk(1)->isSelection()) {
ContextBar* contextBar = App::instance()->contextBar();
contextBar->updateForMovingPixels();
contextBar->updateForMovingPixels(getTransformation(editor));
}
}
}
@ -273,7 +280,7 @@ bool MovingPixelsState::onMouseDown(Editor* editor, MouseMessage* msg)
ctx->setActiveView(editor->getDocView());
ContextBar* contextBar = App::instance()->contextBar();
contextBar->updateForMovingPixels();
contextBar->updateForMovingPixels(getTransformation(editor));
// Start scroll loop
if (editor->checkForScroll(msg) ||
@ -307,8 +314,8 @@ bool MovingPixelsState::onMouseDown(Editor* editor, MouseMessage* msg)
return true;
// Re-catch the image
m_pixelsMovement->catchImageAgain(
editor->screenToEditor(msg->position()), handle);
gfx::Point pt = editor->screenToEditor(msg->position());
m_pixelsMovement->catchImageAgain(gfx::PointF(pt), handle);
editor->captureMouse();
return true;
@ -332,7 +339,7 @@ bool MovingPixelsState::onMouseDown(Editor* editor, MouseMessage* msg)
// Re-catch the image
m_pixelsMovement->catchImageAgain(
editor->screenToEditor(msg->position()), MovePixelsHandle);
editor->screenToEditorF(msg->position()), MovePixelsHandle);
editor->captureMouse();
return true;
@ -373,17 +380,7 @@ bool MovingPixelsState::onMouseMove(Editor* editor, MouseMessage* msg)
gfx::Point mousePos = editor->autoScroll(msg, AutoScroll::MouseDir);
// Get the position of the mouse in the sprite
gfx::Point spritePos = editor->screenToEditor(mousePos);
if (spritePos == m_oldSpritePos) {
// Avoid redrawing everything if the position in the canvas didn't change.
// TODO remove this if we add support for anti-aliasing in the
// transformations
return true;
}
m_oldSpritePos = spritePos;
m_renderTimer.start();
m_pixelsMovement->setFastMode(true);
gfx::PointF spritePos = editor->screenToEditorF(mousePos);
// Get the customization for the pixels movement (snap to grid, angle snap, etc.).
KeyContext keyContext = KeyContext::Normal;
@ -402,15 +399,17 @@ bool MovingPixelsState::onMouseMove(Editor* editor, MouseMessage* msg)
keyContext = KeyContext::ScalingSelection;
break;
case RotateNWHandle:
case RotateNHandle:
case RotateNEHandle:
case RotateWHandle:
case RotateEHandle:
case RotateSWHandle:
case RotateSHandle:
case RotateSEHandle:
keyContext = KeyContext::RotatingSelection;
break;
case SkewNHandle:
case SkewWHandle:
case SkewEHandle:
case SkewSHandle:
keyContext = KeyContext::ScalingSelection;
break;
}
PixelsMovement::MoveModifier moveModifier = PixelsMovement::NormalMovement;
@ -432,14 +431,25 @@ bool MovingPixelsState::onMouseMove(Editor* editor, MouseMessage* msg)
if (int(action & KeyAction::LockAxis))
moveModifier |= PixelsMovement::LockAxisMovement;
if (int(action & KeyAction::FineControl))
moveModifier |= PixelsMovement::FineControl;
m_renderTimer.start();
m_pixelsMovement->setFastMode(true);
// Invalidate handles
Decorator* decorator = static_cast<Decorator*>(editor->decorator());
TransformHandles* transfHandles = decorator->getTransformHandles(editor);
transfHandles->invalidateHandles(editor, m_pixelsMovement->getTransformation());
const Transformation& transformation = m_pixelsMovement->getTransformation();
transfHandles->invalidateHandles(editor, transformation);
// Drag the image to that position
m_pixelsMovement->moveImage(spritePos, moveModifier);
// Update context bar and status bar
ContextBar* contextBar = App::instance()->contextBar();
contextBar->updateForMovingPixels(transformation);
editor->updateStatusBar();
return true;
}
@ -586,7 +596,7 @@ void MovingPixelsState::onBeforeCommandExecution(CommandExecutionEvent& ev)
m_pixelsMovement->shift(delta.x, delta.y);
}
else {
translate(delta);
translate(gfx::PointF(delta));
}
// We've processed the selection content movement right here.
ev.cancel();
@ -751,6 +761,12 @@ void MovingPixelsState::onDropPixels(ContextBarObserver::DropAction action)
}
}
void MovingPixelsState::onPivotChange()
{
ContextBar* contextBar = App::instance()->contextBar();
contextBar->updateForMovingPixels(getTransformation(m_editor));
}
void MovingPixelsState::setTransparentColor(bool opaque, const app::Color& color)
{
ASSERT(m_pixelsMovement);

View File

@ -29,7 +29,8 @@ namespace app {
class MovingPixelsState
: public StandbyState
, EditorObserver
, ContextBarObserver {
, ContextBarObserver
, PixelsMovementDelegate {
public:
MovingPixelsState(Editor* editor, ui::MouseMessage* msg, PixelsMovementPtr pixelsMovement, HandleType handle);
virtual ~MovingPixelsState();
@ -38,11 +39,13 @@ namespace app {
return m_pixelsMovement->canHandleFrameChange();
}
void translate(const gfx::Point& delta);
void translate(const gfx::PointF& delta);
void rotate(double angle);
void flip(doc::algorithm::FlipType flipType);
void shift(int dx, int dy);
void updateTransformation(const Transformation& t);
// EditorState
virtual void onEnterState(Editor* editor) override;
virtual void onEditorGotFocus(Editor* editor) override;
@ -66,6 +69,9 @@ namespace app {
// ContextBarObserver
virtual void onDropPixels(ContextBarObserver::DropAction action) override;
// PixelsMovementDelegate
virtual void onPivotChange() override;
virtual Transformation getTransformation(Editor* editor) override;
private:
@ -98,7 +104,7 @@ namespace app {
// Position of the mouse in the canvas to avoid redrawing when the
// mouse position changes (only we redraw when the canvas position
// changes).
gfx::Point m_oldSpritePos;
gfx::PointF m_oldSpritePos;
obs::connection m_ctxConn;
obs::connection m_opaqueConn;

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2020 Igara Studio S.A.
// Copyright (C) 2001-2016 David Capello
//
// This program is distributed under the terms of
@ -17,8 +18,7 @@ namespace app {
void set_pivot_from_preferences(Transformation& t)
{
Transformation::Corners corners;
t.transformBox(corners);
auto corners = t.transformedCorners();
gfx::PointT<double> nw(corners[Transformation::Corners::LEFT_TOP]);
gfx::PointT<double> ne(corners[Transformation::Corners::RIGHT_TOP]);
gfx::PointT<double> sw(corners[Transformation::Corners::LEFT_BOTTOM]);

View File

@ -53,11 +53,17 @@
#define DUMP_INNER_CMDS()
#endif
using vec2 = base::Vector2d<double>;
namespace app {
template<typename T>
static inline const base::Vector2d<double> point2Vector(const gfx::PointT<T>& pt) {
return base::Vector2d<double>(pt.x, pt.y);
static inline const vec2 to_vec2(const gfx::PointT<T>& pt) {
return vec2(pt.x, pt.y);
}
static inline const gfx::PointF to_point(const vec2& v) {
return gfx::PointF(v.x, v.y);
}
PixelsMovement::InnerCmd::InnerCmd(InnerCmd&& c)
@ -180,6 +186,11 @@ bool PixelsMovement::editMultipleCels() const
m_site.range().type() == DocRange::kCels));
}
void PixelsMovement::setDelegate(PixelsMovementDelegate* delegate)
{
m_delegate = delegate;
}
void PixelsMovement::setFastMode(const bool fastMode)
{
bool redraw = (m_fastMode && !fastMode);
@ -243,6 +254,49 @@ void PixelsMovement::shift(int dx, int dy)
}
}
void PixelsMovement::setTransformation(const Transformation& t)
{
m_initialData = m_currentData;
setTransformationBase(t);
redrawCurrentMask();
updateDocumentMask();
update_screen_for_document(m_document);
}
void PixelsMovement::setTransformationBase(const Transformation& t)
{
// Get old transformed corners, update transformation, and get new
// transformed corners. These corners will be used to know what to
// update in the editor's canvas.
auto oldCorners = m_currentData.transformedCorners();
m_currentData = t;
auto newCorners = m_currentData.transformedCorners();
redrawExtraImage();
m_document->setTransformation(m_currentData);
// Create a union of all corners, and that will be the bounds to
// redraw of the sprite.
gfx::Rect fullBounds;
for (int i=0; i<Transformation::Corners::NUM_OF_CORNERS; ++i) {
fullBounds |= gfx::Rect((int)oldCorners[i].x, (int)oldCorners[i].y, 1, 1);
fullBounds |= gfx::Rect((int)newCorners[i].x, (int)newCorners[i].y, 1, 1);
}
// If "fullBounds" is empty is because the cel was not moved
if (!fullBounds.isEmpty()) {
// Notify the modified region.
m_document->notifySpritePixelsModified(
m_site.sprite(),
gfx::Region(fullBounds),
m_site.frame());
}
}
void PixelsMovement::trim()
{
ContextWriter writer(m_reader, 1000);
@ -296,7 +350,7 @@ void PixelsMovement::copyMask()
hideDocumentMask();
}
void PixelsMovement::catchImage(const gfx::Point& pos, HandleType handle)
void PixelsMovement::catchImage(const gfx::PointF& pos, HandleType handle)
{
ASSERT(handle != NoHandle);
@ -305,7 +359,7 @@ void PixelsMovement::catchImage(const gfx::Point& pos, HandleType handle)
m_handle = handle;
}
void PixelsMovement::catchImageAgain(const gfx::Point& pos, HandleType handle)
void PixelsMovement::catchImageAgain(const gfx::PointF& pos, HandleType handle)
{
// Create a new Transaction to move the pixels to other position
m_initialData = m_currentData;
@ -316,33 +370,33 @@ void PixelsMovement::catchImageAgain(const gfx::Point& pos, HandleType handle)
hideDocumentMask();
}
void PixelsMovement::moveImage(const gfx::Point& pos, MoveModifier moveModifier)
void PixelsMovement::moveImage(const gfx::PointF& pos, MoveModifier moveModifier)
{
Transformation::Corners oldCorners;
m_currentData.transformBox(oldCorners);
ContextWriter writer(m_reader, 1000);
gfx::RectF bounds = m_initialData.bounds();
bool updateBounds = false;
double dx, dy;
gfx::PointF abs_initial_pivot = m_initialData.pivot();
gfx::PointF abs_pivot = m_currentData.pivot();
dx = ((pos.x - m_catchPos.x) * cos(m_currentData.angle()) +
(pos.y - m_catchPos.y) * -sin(m_currentData.angle()));
dy = ((pos.x - m_catchPos.x) * sin(m_currentData.angle()) +
(pos.y - m_catchPos.y) * cos(m_currentData.angle()));
auto newTransformation = m_currentData;
switch (m_handle) {
case MovePixelsHandle:
case MovePixelsHandle: {
double dx = (pos.x - m_catchPos.x);
double dy = (pos.y - m_catchPos.y);
if ((moveModifier & FineControl) == 0) {
if (dx >= 0.0) { dx = std::floor(dx); } else { dx = std::ceil(dx); }
if (dy >= 0.0) { dy = std::floor(dy); } else { dy = std::ceil(dy); }
}
if ((moveModifier & LockAxisMovement) == LockAxisMovement) {
if (ABS(dx) < ABS(dy))
if (std::abs(dx) < std::abs(dy))
dx = 0.0;
else
dy = 0.0;
}
bounds.offset(dx, dy);
updateBounds = true;
if ((moveModifier & SnapToGridMovement) == SnapToGridMovement) {
// Snap the x1,y1 point to the grid.
@ -355,10 +409,15 @@ void PixelsMovement::moveImage(const gfx::Point& pos, MoveModifier moveModifier)
// Now we calculate the difference from x1,y1 point and we can
// use it to adjust all coordinates (x1, y1, x2, y2).
gridOffset -= bounds.origin();
bounds.offset(gridOffset);
bounds.setOrigin(gridOffset);
}
newTransformation.bounds(bounds);
newTransformation.pivot(abs_initial_pivot +
bounds.origin() -
m_initialData.bounds().origin());
break;
}
case ScaleNWHandle:
case ScaleNHandle:
@ -379,8 +438,7 @@ void PixelsMovement::moveImage(const gfx::Point& pos, MoveModifier moveModifier)
handles[m_handle-ScaleNWHandle][1]);
if ((moveModifier & ScaleFromPivot) == ScaleFromPivot) {
pivot.x = m_currentData.pivot().x;
pivot.y = m_currentData.pivot().y;
pivot = m_currentData.pivot();
}
else {
pivot.x = 1.0 - handle.x;
@ -393,26 +451,36 @@ void PixelsMovement::moveImage(const gfx::Point& pos, MoveModifier moveModifier)
gfx::PointF b = bounds.point2();
if ((moveModifier & MaintainAspectRatioMovement) == MaintainAspectRatioMovement) {
auto u = point2Vector(gfx::PointF(m_catchPos) - pivot);
auto v = point2Vector(gfx::PointF(pos) - pivot);
auto w = v.projectOn(u);
vec2 u = to_vec2(m_catchPos - pivot);
vec2 v = to_vec2(pos - pivot);
vec2 w = v.projectOn(u);
double scale = u.magnitude();
if (scale != 0.0)
if (scale != 0.0) {
scale = (std::fabs(w.angle()-u.angle()) < PI/2.0 ? 1.0: -1.0) * w.magnitude() / scale;
}
else
scale = 1.0;
a.x = int((a.x-pivot.x)*scale + pivot.x);
a.y = int((a.y-pivot.y)*scale + pivot.y);
b.x = int((b.x-pivot.x)*scale + pivot.x);
b.y = int((b.y-pivot.y)*scale + pivot.y);
a.x = ((a.x-pivot.x)*scale + pivot.x);
a.y = ((a.y-pivot.y)*scale + pivot.y);
b.x = ((b.x-pivot.x)*scale + pivot.x);
b.y = ((b.y-pivot.y)*scale + pivot.y);
}
else {
handle.x = bounds.x + bounds.w*handle.x;
handle.y = bounds.y + bounds.h*handle.y;
double z = m_currentData.angle();
double w = (handle.x-pivot.x);
double h = (handle.y-pivot.y);
double dx = ((pos.x - m_catchPos.x) * std::cos(z) +
(pos.y - m_catchPos.y) * -std::sin(z));
double dy = ((pos.x - m_catchPos.x) * std::sin(z) +
(pos.y - m_catchPos.y) * std::cos(z));
if ((moveModifier & FineControl) == 0) {
if (dx >= 0.0) { dx = std::floor(dx); } else { dx = std::ceil(dx); }
if (dy >= 0.0) { dy = std::floor(dy); } else { dy = std::ceil(dy); }
}
if (m_handle == ScaleNHandle || m_handle == ScaleSHandle) {
dx = 0.0;
@ -423,10 +491,10 @@ void PixelsMovement::moveImage(const gfx::Point& pos, MoveModifier moveModifier)
h = 1.0;
}
a.x = int((a.x-pivot.x)*(1.0+dx/w) + pivot.x);
a.y = int((a.y-pivot.y)*(1.0+dy/h) + pivot.y);
b.x = int((b.x-pivot.x)*(1.0+dx/w) + pivot.x);
b.y = int((b.y-pivot.y)*(1.0+dy/h) + pivot.y);
a.x = ((a.x-pivot.x)*(1.0+dx/w) + pivot.x);
a.y = ((a.y-pivot.y)*(1.0+dy/h) + pivot.y);
b.x = ((b.x-pivot.x)*(1.0+dx/w) + pivot.x);
b.y = ((b.y-pivot.y)*(1.0+dy/h) + pivot.y);
}
// Do not use "gfx::Rect(a, b)" here because if a > b we want to
@ -437,109 +505,210 @@ void PixelsMovement::moveImage(const gfx::Point& pos, MoveModifier moveModifier)
bounds.w = b.x - a.x;
bounds.h = b.y - a.y;
updateBounds = true;
newTransformation.bounds(bounds);
m_adjustPivot = true;
break;
}
case RotateNWHandle:
case RotateNHandle:
case RotateNEHandle:
case RotateWHandle:
case RotateEHandle:
case RotateSWHandle:
case RotateSHandle:
case RotateSEHandle:
{
gfx::PointF abs_initial_pivot = m_initialData.pivot();
gfx::PointF abs_pivot = m_currentData.pivot();
case RotateSEHandle: {
double da = (std::atan2((double)(-pos.y + abs_pivot.y),
(double)(+pos.x - abs_pivot.x)) -
std::atan2((double)(-m_catchPos.y + abs_initial_pivot.y),
(double)(+m_catchPos.x - abs_initial_pivot.x)));
double newAngle = m_initialData.angle() + da;
newAngle = base::fmod_radians(newAngle);
double newAngle =
m_initialData.angle()
+ atan2((double)(-pos.y + abs_pivot.y),
(double)(+pos.x - abs_pivot.x))
- atan2((double)(-m_catchPos.y + abs_initial_pivot.y),
(double)(+m_catchPos.x - abs_initial_pivot.x));
// Is the "angle snap" is activated, we've to snap the angle
// to common (pixel art) angles.
if ((moveModifier & AngleSnapMovement) == AngleSnapMovement) {
// TODO make this configurable
static const double keyAngles[] = {
0.0, 26.565, 45.0, 63.435, 90.0, 116.565, 135.0, 153.435, 180.0,
180.0, -153.435, -135.0, -116, -90.0, -63.435, -45.0, -26.565
};
newAngle = base::fmod_radians(newAngle);
double newAngleDegrees = 180.0 * newAngle / PI;
// Is the "angle snap" is activated, we've to snap the angle
// to common (pixel art) angles.
if ((moveModifier & AngleSnapMovement) == AngleSnapMovement) {
// TODO make this configurable
static const double keyAngles[] = {
0.0, 26.565, 45.0, 63.435, 90.0, 116.565, 135.0, 153.435, 180.0,
180.0, -153.435, -135.0, -116, -90.0, -63.435, -45.0, -26.565
};
double newAngleDegrees = 180.0 * newAngle / PI;
int closest = 0;
int last = sizeof(keyAngles) / sizeof(keyAngles[0]) - 1;
for (int i=0; i<=last; ++i) {
if (std::fabs(newAngleDegrees-keyAngles[closest]) >
std::fabs(newAngleDegrees-keyAngles[i]))
closest = i;
}
newAngle = PI * keyAngles[closest] / 180.0;
int closest = 0;
int last = sizeof(keyAngles) / sizeof(keyAngles[0]) - 1;
for (int i=0; i<=last; ++i) {
if (std::fabs(newAngleDegrees-keyAngles[closest]) >
std::fabs(newAngleDegrees-keyAngles[i]))
closest = i;
}
m_currentData.angle(newAngle);
newAngle = PI * keyAngles[closest] / 180.0;
}
newTransformation.angle(newAngle);
break;
}
case PivotHandle:
{
// Calculate the new position of the pivot
gfx::PointF newPivot(m_initialData.pivot().x + (pos.x - m_catchPos.x),
m_initialData.pivot().y + (pos.y - m_catchPos.y));
case SkewNHandle:
case SkewSHandle:
case SkewWHandle:
case SkewEHandle: {
// u
// ------>
//
// A --- B |
// | | | v
// | | |
// C --- D v
auto corners = m_initialData.transformedCorners();
auto A = corners[Transformation::Corners::LEFT_TOP];
auto B = corners[Transformation::Corners::RIGHT_TOP];
auto C = corners[Transformation::Corners::LEFT_BOTTOM];
auto D = corners[Transformation::Corners::RIGHT_BOTTOM];
m_currentData = m_initialData;
m_currentData.displacePivotTo(newPivot);
// Pivot in pixels
gfx::PointF pivotPoint = m_currentData.pivot();
// Pivot in [0.0, 1.0] range
gfx::PointF pivot((pivotPoint.x - bounds.x) / bounds.w,
(pivotPoint.y - bounds.y) / bounds.h);
// Vector from AB (or CD), and AC (or BD)
vec2 u = to_vec2(B - A);
vec2 v = to_vec2(C - A);
// Move PQ and RS side by a delta value projected on u vector
vec2 delta = to_vec2(pos - m_catchPos);
switch (m_handle) {
case SkewNHandle:
delta = delta.projectOn(u);
A.x += delta.x;
A.y += delta.y;
B.x += delta.x;
B.y += delta.y;
break;
case SkewSHandle:
delta = delta.projectOn(u);
C.x += delta.x;
C.y += delta.y;
D.x += delta.x;
D.y += delta.y;
break;
case SkewWHandle: {
delta = delta.projectOn(v);
A.x += delta.x;
A.y += delta.y;
C.x += delta.x;
C.y += delta.y;
vec2 toPivot = to_vec2(pivotPoint - (A*(1.0-pivot.y) + C*pivot.y));
// TODO avoid division by zero (which happens when the pivot
// is in the exact same X pos as the west handle edge)
vec2 toOtherSide = toPivot / pivot.x;
B = A + to_point(toOtherSide);
D = C + to_point(toOtherSide);
break;
}
case SkewEHandle: {
delta = delta.projectOn(v);
B.x += delta.x;
B.y += delta.y;
D.x += delta.x;
D.y += delta.y;
vec2 toPivot = to_vec2(pivotPoint - (B*(1.0-pivot.y) + D*pivot.y));
// TODO avoid division by zero (which happens when the pivot
// is in the exact same X pos as the east handle edge)
vec2 toOtherSide = toPivot / (1.0-pivot.x);
A = B + to_point(toOtherSide);
C = D + to_point(toOtherSide);
break;
}
}
// t0 will be a transformation without skew, so we can compare
// the angle between vector PR with skew and without skew.
auto t0 = m_initialData;
t0.skew(0.0);
auto corners0 = t0.transformedCorners();
auto A0 = corners0[Transformation::Corners::LEFT_TOP];
auto B0 = corners0[Transformation::Corners::RIGHT_TOP];
auto C0 = corners0[Transformation::Corners::LEFT_BOTTOM];
// A0 ------- B
// /| /
// / ACp / <- pivot position
// / | /
// C -C0----- D
vec2 AC0 = to_vec2(C0 - A0);
auto ACp = A0*(1.0-pivot.y) + C0*pivot.y;
vec2 AC;
switch (m_handle) {
case SkewNHandle: AC = to_vec2(ACp - A); break;
case SkewSHandle: AC = to_vec2(C - ACp); break;
case SkewWHandle:
case SkewEHandle: {
vec2 AB = to_vec2(B - A);
bounds.w = AB.magnitude();
bounds.x = pivotPoint.x - bounds.w*pivot.x;
// New rotation angle is the angle between AB points
newTransformation.angle(-AB.angle());
// New skew angle is the angle between AC0 (vector from A to
// B rotated 45 degrees, like an AC vector without skew) and
// the current to AC vector.
//
// B
// / |
// / |
// / |
// A |
// | \ D
// | \ /
// | / <- AC0=AB rotated 45 degrees, if pivot is here
// | /
// C
auto ABp = A*(1.0-pivot.x) + B*pivot.x;
AC0 = vec2(ABp.y - B.y, B.x - ABp.x);
AC = to_vec2(C - A);
bounds.h = AC.projectOn(AC0).magnitude();
bounds.y = pivotPoint.y - bounds.h*pivot.y;
newTransformation.bounds(bounds);
break;
}
}
// Calculate angle between AC and AC0
double newSkew = std::atan2(AC.x*AC0.y - AC.y*AC0.x, AC * AC0);
newTransformation.skew(newSkew);
break;
}
case PivotHandle: {
// Calculate the new position of the pivot
gfx::PointF newPivot = m_initialData.pivot() + pos - m_catchPos;
newTransformation = m_initialData;
newTransformation.displacePivotTo(newPivot);
break;
}
}
if (updateBounds) {
m_currentData.bounds(bounds);
m_adjustPivot = true;
}
redrawExtraImage();
m_document->setTransformation(m_currentData);
// Get the new transformed corners
Transformation::Corners newCorners;
m_currentData.transformBox(newCorners);
// Create a union of all corners, and that will be the bounds to
// redraw of the sprite.
gfx::Rect fullBounds;
for (int i=0; i<Transformation::Corners::NUM_OF_CORNERS; ++i) {
fullBounds = fullBounds.createUnion(gfx::Rect((int)oldCorners[i].x, (int)oldCorners[i].y, 1, 1));
fullBounds = fullBounds.createUnion(gfx::Rect((int)newCorners[i].x, (int)newCorners[i].y, 1, 1));
}
// If "fullBounds" is empty is because the cel was not moved
if (!fullBounds.isEmpty()) {
// Notify the modified region.
m_document->notifySpritePixelsModified(
m_site.sprite(),
gfx::Region(fullBounds),
m_site.frame());
}
setTransformationBase(newTransformation);
}
void PixelsMovement::getDraggedImageCopy(std::unique_ptr<Image>& outputImage,
std::unique_ptr<Mask>& outputMask)
{
gfx::Rect bounds = m_currentData.transformedBounds();
if (bounds.isEmpty())
return;
std::unique_ptr<Image> image(
Image::create(
m_site.sprite()->pixelFormat(), bounds.w, bounds.h));
drawImage(m_currentData, image.get(), bounds.origin(), false);
drawImage(m_currentData, image.get(),
gfx::PointF(bounds.origin()), false);
// Draw mask without shrinking it, so the mask size is equal to the
// "image" render.
@ -623,8 +792,11 @@ void PixelsMovement::stampImage(bool finalStamp)
void PixelsMovement::stampExtraCelImage()
{
const Cel* cel = m_extraCel->cel();
const Image* image = m_extraCel->image();
if (!image)
return;
const Cel* cel = m_extraCel->cel();
// Expand the canvas to paste the image in the fully visible
// portion of sprite.
@ -664,27 +836,10 @@ void PixelsMovement::dropImageTemporarily()
// Displace the pivot to the new site:
if (m_adjustPivot) {
m_adjustPivot = false;
adjustPivot();
// Get the a factor for the X/Y position of the initial pivot
// position inside the initial non-rotated bounds.
gfx::PointF pivotPosFactor(m_initialData.pivot() - m_initialData.bounds().origin());
pivotPosFactor.x /= m_initialData.bounds().w;
pivotPosFactor.y /= m_initialData.bounds().h;
// Get the current transformed bounds.
Transformation::Corners corners;
m_currentData.transformBox(corners);
// The new pivot will be located from the rotated left-top
// corner a distance equal to the transformed bounds's
// width/height multiplied with the previously calculated X/Y
// factor.
base::Vector2d<double> newPivot(corners.leftTop().x,
corners.leftTop().y);
newPivot += pivotPosFactor.x * point2Vector(corners.rightTop() - corners.leftTop());
newPivot += pivotPosFactor.y * point2Vector(corners.leftBottom() - corners.leftTop());
m_currentData.displacePivotTo(gfx::PointF(newPivot.x, newPivot.y));
if (m_delegate)
m_delegate->onPivotChange();
}
redrawCurrentMask();
@ -694,6 +849,29 @@ void PixelsMovement::dropImageTemporarily()
}
}
void PixelsMovement::adjustPivot()
{
// Get the a factor for the X/Y position of the initial pivot
// position inside the initial non-rotated bounds.
gfx::PointF pivotPosFactor(m_initialData.pivot() - m_initialData.bounds().origin());
pivotPosFactor.x /= m_initialData.bounds().w;
pivotPosFactor.y /= m_initialData.bounds().h;
// Get the current transformed bounds.
auto corners = m_currentData.transformedCorners();
// The new pivot will be located from the rotated left-top
// corner a distance equal to the transformed bounds's
// width/height multiplied with the previously calculated X/Y
// factor.
vec2 newPivot(corners.leftTop().x,
corners.leftTop().y);
newPivot += pivotPosFactor.x * to_vec2(corners.rightTop() - corners.leftTop());
newPivot += pivotPosFactor.y * to_vec2(corners.leftBottom() - corners.leftTop());
m_currentData.displacePivotTo(gfx::PointF(newPivot.x, newPivot.y));
}
void PixelsMovement::dropImage()
{
m_isDragging = false;
@ -780,28 +958,35 @@ void PixelsMovement::redrawExtraImage(Transformation* transformation)
Cel* cel = m_site.cel();
if (cel) opacity = MUL_UN8(opacity, cel->opacity(), t);
gfx::Rect bounds = transformation->transformedBounds();
if (!m_extraCel)
m_extraCel.reset(new ExtraCel);
m_extraCel->create(
m_site.tilemapMode(),
m_document->sprite(),
bounds,
(m_site.tilemapMode() == TilemapMode::Tiles ? m_site.grid().tileToCanvas(bounds).size():
bounds.size()),
m_site.frame(),
opacity);
m_extraCel->setType(render::ExtraType::PATCH);
m_extraCel->setBlendMode(m_site.layer()->isImage() ?
static_cast<LayerImage*>(m_site.layer())->blendMode():
BlendMode::NORMAL);
gfx::Rect bounds = transformation->transformedBounds();
if (!bounds.isEmpty()) {
m_extraCel->create(
m_site.tilemapMode(),
m_document->sprite(),
bounds,
(m_site.tilemapMode() == TilemapMode::Tiles ? m_site.grid().tileToCanvas(bounds).size():
bounds.size()),
m_site.frame(),
opacity);
m_extraCel->setType(render::ExtraType::PATCH);
m_extraCel->setBlendMode(m_site.layer()->isImage() ?
static_cast<LayerImage*>(m_site.layer())->blendMode():
BlendMode::NORMAL);
}
else
m_extraCel->reset();
m_document->setExtraCel(m_extraCel);
// Draw the transformed pixels in the extra-cel which is the chunk
// of pixels that the user is moving.
drawImage(*transformation, m_extraCel->image(), bounds.origin(), true);
if (m_extraCel->image()) {
// Draw the transformed pixels in the extra-cel which is the chunk
// of pixels that the user is moving.
drawImage(*transformation, m_extraCel->image(),
gfx::PointF(bounds.origin()), true);
}
}
void PixelsMovement::redrawCurrentMask()
@ -811,13 +996,12 @@ void PixelsMovement::redrawCurrentMask()
void PixelsMovement::drawImage(
const Transformation& transformation,
doc::Image* dst, const gfx::Point& pt,
doc::Image* dst, const gfx::PointF& pt,
const bool renderOriginalLayer)
{
ASSERT(dst);
Transformation::Corners corners;
transformation.transformBox(corners);
auto corners = transformation.transformedCorners();
gfx::Rect bounds = corners.bounds();
dst->setMaskColor(m_site.sprite()->transparentColor());
@ -854,10 +1038,14 @@ void PixelsMovement::drawImage(
void PixelsMovement::drawMask(doc::Mask* mask, bool shrink)
{
Transformation::Corners corners;
m_currentData.transformBox(corners);
auto corners = m_currentData.transformedCorners();
gfx::Rect bounds = corners.bounds();
if (bounds.isEmpty()) {
mask->clear();
return;
}
mask->replace(bounds);
if (shrink)
mask->freeze();
@ -866,7 +1054,8 @@ void PixelsMovement::drawMask(doc::Mask* mask, bool shrink)
mask->bitmap(),
m_initialMask->bitmap(),
nullptr,
corners, bounds.origin());
corners,
gfx::PointF(bounds.origin()));
if (shrink)
mask->unfreeze();
}
@ -875,7 +1064,7 @@ void PixelsMovement::drawParallelogram(
const Transformation& transformation,
doc::Image* dst, const doc::Image* src, const doc::Mask* mask,
const Transformation::Corners& corners,
const gfx::Point& leftTop)
const gfx::PointF& leftTop)
{
tools::RotationAlgorithm rotAlgo = Preferences::instance().selection.rotationAlgorithm();
@ -943,6 +1132,9 @@ void PixelsMovement::onPivotChange()
{
set_pivot_from_preferences(m_currentData);
onRotationAlgorithmChange();
if (m_delegate)
m_delegate->onPivotChange();
}
void PixelsMovement::onRotationAlgorithmChange()

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019 Igara Studio S.A.
// Copyright (C) 2019-2020 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -36,6 +36,12 @@ namespace app {
class SetMask;
}
class PixelsMovementDelegate {
public:
virtual ~PixelsMovementDelegate() { }
virtual void onPivotChange() = 0;
};
// Helper class to move pixels interactively and control undo history
// correctly. The extra cel of the sprite is temporally used to show
// feedback, drag, and drop the specified image in the constructor
@ -50,6 +56,7 @@ namespace app {
MaintainAspectRatioMovement = 8,
LockAxisMovement = 16,
ScaleFromPivot = 32,
FineControl = 64,
};
enum CommitChangesOption {
@ -71,17 +78,18 @@ namespace app {
HandleType handle() const { return m_handle; }
bool canHandleFrameChange() const { return m_canHandleFrameChange; }
void setDelegate(PixelsMovementDelegate* delegate);
void setFastMode(const bool fastMode);
void trim();
void cutMask();
void copyMask();
void catchImage(const gfx::Point& pos, HandleType handle);
void catchImageAgain(const gfx::Point& pos, HandleType handle);
void catchImage(const gfx::PointF& pos, HandleType handle);
void catchImageAgain(const gfx::PointF& pos, HandleType handle);
// Moves the image to the new position (relative to the start
// position given in the ctor).
void moveImage(const gfx::Point& pos, MoveModifier moveModifier);
void moveImage(const gfx::PointF& pos, MoveModifier moveModifier);
// Returns a copy of the current image being dragged with the
// current transformation.
@ -118,8 +126,11 @@ namespace app {
bool gotoFrame(const doc::frame_t deltaFrame);
const Transformation& getTransformation() const { return m_currentData; }
void setTransformation(const Transformation& t);
private:
void setTransformationBase(const Transformation& t);
void adjustPivot();
bool editMultipleCels() const;
void stampImage(bool finalStamp);
void stampExtraCelImage();
@ -129,14 +140,14 @@ namespace app {
void redrawCurrentMask();
void drawImage(
const Transformation& transformation,
doc::Image* dst, const gfx::Point& pos,
doc::Image* dst, const gfx::PointF& pos,
const bool renderOriginalLayer);
void drawMask(doc::Mask* dst, bool shrink);
void drawParallelogram(
const Transformation& transformation,
doc::Image* dst, const doc::Image* src, const doc::Mask* mask,
const Transformation::Corners& corners,
const gfx::Point& leftTop);
const gfx::PointF& leftTop);
void updateDocumentMask();
void hideDocumentMask();
@ -149,6 +160,7 @@ namespace app {
void dumpInnerCmds();
#endif
PixelsMovementDelegate* m_delegate = nullptr;
const ContextReader m_reader;
Site m_site;
Doc* m_document;
@ -157,7 +169,7 @@ namespace app {
bool m_adjustPivot;
HandleType m_handle;
doc::ImageRef m_originalImage;
gfx::Point m_catchPos;
gfx::PointF m_catchPos;
Transformation m_initialData;
Transformation m_currentData;
std::unique_ptr<Mask> m_initialMask, m_initialMask0;

View File

@ -51,13 +51,13 @@
#include "app/util/layer_utils.h"
#include "app/util/new_image_from_mask.h"
#include "app/util/readable_time.h"
#include "base/clamp.h"
#include "base/pi.h"
#include "doc/grid.h"
#include "doc/layer.h"
#include "doc/mask.h"
#include "doc/slice.h"
#include "doc/sprite.h"
#include "fixmath/fixmath.h"
#include "fmt/format.h"
#include "gfx/rect.h"
#include "os/surface.h"
@ -74,17 +74,6 @@ namespace app {
using namespace ui;
static CursorType rotated_size_cursors[8] = {
kSizeECursor,
kSizeNECursor,
kSizeNCursor,
kSizeNWCursor,
kSizeWCursor,
kSizeSWCursor,
kSizeSCursor,
kSizeSECursor
};
#ifdef _MSC_VER
#pragma warning(disable:4355) // warning C4355: 'this' : used in base member initializer list
#endif
@ -714,7 +703,7 @@ void StandbyState::startSelectionTransformation(Editor* editor,
transformSelection(editor, NULL, NoHandle);
if (MovingPixelsState* movingPixels = dynamic_cast<MovingPixelsState*>(editor->getState().get())) {
movingPixels->translate(move);
movingPixels->translate(gfx::PointF(move));
if (std::fabs(angle) > 1e-5)
movingPixels->rotate(angle);
}
@ -905,65 +894,77 @@ bool StandbyState::Decorator::onSetCursor(tools::Ink* ink, Editor* editor, const
CursorType newCursorType = kArrowCursor;
const Cursor* newCursor = nullptr;
int angle = 0;
// TODO improve this for depending if we are handling corners or
// sides (e.g. from sides we can use the normal to know the
// angle/mouse cursor to show)
{
gfx::Point center = gfx::Point(
editor->editorToScreenF(transformation.transformedBounds()).center());
// Adjust the cursor depending the current transformation angle.
int dy = mouseScreenPos.y - center.y;
int dx = mouseScreenPos.x - center.x;
double a = std::atan2(-dy, dx);
a += PI * (45.0/2.0) / 180.0;
a = base::fmod_radians(a) + PI;
ASSERT(a >= 0.0 && a <= 2*PI);
angle = base::clamp<int>(std::floor(7.9 * a / (2.0*PI)), 0, 7);
}
switch (handle) {
case ScaleNWHandle: newCursorType = kSizeNWCursor; break;
case ScaleNHandle: newCursorType = kSizeNCursor; break;
case ScaleNEHandle: newCursorType = kSizeNECursor; break;
case ScaleWHandle: newCursorType = kSizeWCursor; break;
case ScaleEHandle: newCursorType = kSizeECursor; break;
case ScaleSWHandle: newCursorType = kSizeSWCursor; break;
case ScaleSHandle: newCursorType = kSizeSCursor; break;
case ScaleSEHandle: newCursorType = kSizeSECursor; break;
case RotateNWHandle: newCursor = theme->cursors.rotateNw(); break;
case RotateNHandle: newCursor = theme->cursors.rotateN(); break;
case RotateNEHandle: newCursor = theme->cursors.rotateNe(); break;
case RotateWHandle: newCursor = theme->cursors.rotateW(); break;
case RotateEHandle: newCursor = theme->cursors.rotateE(); break;
case RotateSWHandle: newCursor = theme->cursors.rotateSw(); break;
case RotateSHandle: newCursor = theme->cursors.rotateS(); break;
case RotateSEHandle: newCursor = theme->cursors.rotateSe(); break;
case PivotHandle: newCursorType = kHandCursor; break;
case ScaleNWHandle: newCursorType = kSizeNWCursor; break;
case ScaleNHandle: newCursorType = kSizeNCursor; break;
case ScaleNEHandle: newCursorType = kSizeNECursor; break;
case ScaleWHandle: newCursorType = kSizeWCursor; break;
case ScaleEHandle: newCursorType = kSizeECursor; break;
case ScaleSWHandle: newCursorType = kSizeSWCursor; break;
case ScaleSHandle: newCursorType = kSizeSCursor; break;
case ScaleSEHandle: newCursorType = kSizeSECursor; break;
case PivotHandle: newCursorType = kHandCursor; break;
case RotateNWHandle:
case RotateNEHandle:
case RotateSWHandle:
case RotateSEHandle:
case SkewNHandle:
case SkewWHandle:
case SkewEHandle:
case SkewSHandle:
break;
default:
return false;
}
// Adjust the cursor depending the current transformation angle.
fixmath::fixed angle = fixmath::ftofix(128.0 * transformation.angle() / PI);
angle = fixmath::fixadd(angle, fixmath::itofix(16));
angle &= (255<<16);
angle >>= 16;
angle /= 32;
if (newCursorType >= kSizeNCursor &&
newCursorType <= kSizeNWCursor) {
size_t num = sizeof(rotated_size_cursors) / sizeof(rotated_size_cursors[0]);
size_t c;
for (c=num-1; c>0; --c)
if (rotated_size_cursors[c] == newCursorType)
break;
newCursorType = rotated_size_cursors[(c+angle) % num];
}
else if (newCursor) {
auto theme = skin::SkinTheme::instance();
const Cursor* rotated_rotate_cursors[8] = {
theme->cursors.rotateE(),
theme->cursors.rotateNe(),
theme->cursors.rotateN(),
theme->cursors.rotateNw(),
theme->cursors.rotateW(),
theme->cursors.rotateSw(),
theme->cursors.rotateS(),
theme->cursors.rotateSe()
const CursorType rotated_size_cursors[8] = {
kSizeWCursor,
kSizeSWCursor, kSizeSCursor, kSizeSECursor,
kSizeECursor,
kSizeNECursor, kSizeNCursor, kSizeNWCursor,
};
size_t num = sizeof(rotated_rotate_cursors) / sizeof(rotated_rotate_cursors[0]);
size_t c;
for (c=num-1; c>0; --c)
if (rotated_rotate_cursors[c] == newCursor)
break;
newCursor = rotated_rotate_cursors[(c+angle) % num];
newCursorType = rotated_size_cursors[angle];
}
else if (handle >= RotateNWHandle &&
handle <= RotateSEHandle) {
const Cursor* rotated_rotate_cursors[8] = {
theme->cursors.rotateW(),
theme->cursors.rotateSw(), theme->cursors.rotateS(), theme->cursors.rotateSe(),
theme->cursors.rotateE(),
theme->cursors.rotateNe(), theme->cursors.rotateN(), theme->cursors.rotateNw(),
};
newCursor = rotated_rotate_cursors[angle];
newCursorType = kCustomCursor;
}
else if (handle >= SkewNHandle &&
handle <= SkewSHandle) {
const Cursor* rotated_skew_cursors[8] = {
theme->cursors.skewW(),
theme->cursors.skewSw(), theme->cursors.skewS(), theme->cursors.skewSe(),
theme->cursors.skewE(),
theme->cursors.skewNe(), theme->cursors.skewN(), theme->cursors.skewNw(),
};
newCursor = rotated_skew_cursors[angle];
newCursorType = kCustomCursor;
}

View File

@ -42,13 +42,13 @@ static struct HandlesInfo {
// The exact handle type ([0] for scaling, [1] for rotating).
HandleType handle[2];
} handles_info[HANDLES] = {
{ 1, 2, 0 << 16, { ScaleEHandle, RotateEHandle } },
{ 1, 2, 0 << 16, { ScaleEHandle, SkewEHandle } },
{ 1, 1, 32 << 16, { ScaleNEHandle, RotateNEHandle } },
{ 0, 1, 64 << 16, { ScaleNHandle, RotateNHandle } },
{ 0, 1, 64 << 16, { ScaleNHandle, SkewNHandle } },
{ 0, 0, 96 << 16, { ScaleNWHandle, RotateNWHandle } },
{ 0, 3, 128 << 16, { ScaleWHandle, RotateWHandle } },
{ 0, 3, 128 << 16, { ScaleWHandle, SkewWHandle } },
{ 3, 3, 160 << 16, { ScaleSWHandle, RotateSWHandle } },
{ 3, 2, 192 << 16, { ScaleSHandle, RotateSHandle } },
{ 3, 2, 192 << 16, { ScaleSHandle, SkewSHandle } },
{ 2, 2, 224 << 16, { ScaleSEHandle, RotateSEHandle } },
};
@ -58,8 +58,7 @@ HandleType TransformHandles::getHandleAtPoint(Editor* editor, const gfx::Point&
os::Surface* gfx = theme->parts.transformationHandle()->bitmap(0);
fixmath::fixed angle = fixmath::ftofix(128.0 * transform.angle() / PI);
Transformation::Corners corners;
transform.transformBox(corners);
auto corners = transform.transformedCorners();
std::vector<gfx::Point> screenPoints;
getScreenPoints(editor, corners, screenPoints);
@ -90,8 +89,7 @@ void TransformHandles::drawHandles(Editor* editor, ui::Graphics* g,
{
fixmath::fixed angle = fixmath::ftofix(128.0 * transform.angle() / PI);
Transformation::Corners corners;
transform.transformBox(corners);
auto corners = transform.transformedCorners();
std::vector<gfx::Point> screenPoints;
getScreenPoints(editor, corners, screenPoints);
@ -99,8 +97,7 @@ void TransformHandles::drawHandles(Editor* editor, ui::Graphics* g,
const gfx::Point origin = editor->bounds().origin();
#if 0 // Uncomment this if you want to see the bounds in red (only for debugging purposes)
// -----------------------------------------------
{
{ // Bounds
gfx::Point
a(transform.bounds().origin()),
b(transform.bounds().point2());
@ -112,7 +109,26 @@ void TransformHandles::drawHandles(Editor* editor, ui::Graphics* g,
a = editor->editorToScreen(a) - origin;
g->drawRect(gfx::rgba(255, 0, 0), gfx::Rect(a.x-2, a.y-2, 5, 5));
}
// -----------------------------------------------
{ // Rotated bounds
const gfx::Point& a = screenPoints[0] - origin;
const gfx::Point& b = screenPoints[1] - origin;
const gfx::Point& c = screenPoints[2] - origin;
const gfx::Point& d = screenPoints[3] - origin;
ui::Paint paint;
paint.style(ui::Paint::Stroke);
paint.antialias(true);
paint.color(gfx::rgba(255, 0, 0));
gfx::Path p;
p.moveTo(a.x, a.y);
p.lineTo(b.x, b.y);
p.lineTo(c.x, c.y);
p.lineTo(d.x, d.y);
p.close();
g->drawPath(p, paint);
}
#endif
// Draw corner handle
@ -141,8 +157,7 @@ void TransformHandles::invalidateHandles(Editor* editor, const Transformation& t
SkinTheme* theme = SkinTheme::instance();
fixmath::fixed angle = fixmath::ftofix(128.0 * transform.angle() / PI);
Transformation::Corners corners;
transform.transformBox(corners);
auto corners = transform.transformedCorners();
std::vector<gfx::Point> screenPoints;
getScreenPoints(editor, corners, screenPoints);
@ -266,9 +281,9 @@ void TransformHandles::getScreenPoints(
screenPoints.resize(corners.size());
for (size_t c=0; c<corners.size(); ++c)
screenPoints[c] = editor->editorToScreen(
gfx::Point((int)corners[c].x+main.x,
(int)corners[c].y+main.y));
screenPoints[c] = editor->editorToScreenF(
gfx::PointF(corners[c].x+main.x,
corners[c].y+main.y));
}
} // namespace app

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2020 Igara Studio S.A.
// Copyright (C) 2018 David Capello
//
// This program is distributed under the terms of
@ -12,6 +13,7 @@
#include "ui/message.h"
#include "fmt/format.h"
#include "tinyexpr.h"
#include <cmath>
@ -29,14 +31,8 @@ bool ExprEntry::onProcessMessage(ui::Message* msg)
{
switch (msg->type()) {
case ui::kFocusLeaveMessage: {
char buf[256];
if (m_decimals == 0) {
std::snprintf(buf, sizeof(buf), "%d", onGetTextInt());
}
else {
std::snprintf(buf, sizeof(buf), "%.*g",
m_decimals, onGetTextDouble());
}
std::string buf;
onFormatExprFocusLeave(buf);
if (text() != buf)
setText(buf);
break;
@ -71,4 +67,12 @@ double ExprEntry::onGetTextDouble() const
return v;
}
void ExprEntry::onFormatExprFocusLeave(std::string& buf)
{
if (m_decimals == 0)
buf = fmt::format("{}", onGetTextInt());
else
buf = fmt::format("{:.{}f}", onGetTextDouble(), m_decimals);
}
} // namespace app

View File

@ -26,6 +26,8 @@ namespace app {
int onGetTextInt() const override;
double onGetTextDouble() const override;
virtual void onFormatExprFocusLeave(std::string& buf);
int m_decimals;
};

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018 Igara Studio S.A.
// Copyright (C) 2018-2020 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -63,6 +63,7 @@ namespace app {
ScaleFromCenter = 0x00008000,
AngleSnapFromLastPoint = 0x00010000,
RotateShape = 0x00020000,
FineControl = 0x00040000,
};
enum class WheelAction {
@ -100,10 +101,12 @@ namespace app {
class Key {
public:
Key(Command* command, const Params& params, KeyContext keyContext);
Key(KeyType type, tools::Tool* tool);
explicit Key(KeyAction action);
explicit Key(WheelAction action);
Key(Command* command, const Params& params,
const KeyContext keyContext);
Key(const KeyType type, tools::Tool* tool);
explicit Key(const KeyAction action,
const KeyContext keyContext);
explicit Key(const WheelAction action);
KeyType type() const { return m_type; }
const ui::Accelerators& accels() const {

View File

@ -40,26 +40,29 @@ namespace {
const char* name;
const char* userfriendly;
app::KeyAction action;
app::KeyContext context;
} actions[] = {
{ "CopySelection" , "Copy Selection" , app::KeyAction::CopySelection },
{ "SnapToGrid" , "Snap To Grid" , app::KeyAction::SnapToGrid },
{ "AngleSnap" , "Angle Snap" , app::KeyAction::AngleSnap },
{ "MaintainAspectRatio" , "Maintain Aspect Ratio", app::KeyAction::MaintainAspectRatio },
{ "ScaleFromCenter" , "Scale From Center" , app::KeyAction::ScaleFromCenter },
{ "LockAxis" , "Lock Axis" , app::KeyAction::LockAxis },
{ "AddSelection" , "Add Selection" , app::KeyAction::AddSelection },
{ "SubtractSelection" , "Subtract Selection" , app::KeyAction::SubtractSelection },
{ "IntersectSelection" , "Intersect Selection" , app::KeyAction::IntersectSelection },
{ "AutoSelectLayer" , "Auto Select Layer" , app::KeyAction::AutoSelectLayer },
{ "StraightLineFromLastPoint", "Straight Line from Last Point", app::KeyAction::StraightLineFromLastPoint },
{ "AngleSnapFromLastPoint", "Angle Snap from Last Point", app::KeyAction::AngleSnapFromLastPoint },
{ "MoveOrigin" , "Move Origin" , app::KeyAction::MoveOrigin },
{ "SquareAspect" , "Square Aspect" , app::KeyAction::SquareAspect },
{ "DrawFromCenter" , "Draw From Center" , app::KeyAction::DrawFromCenter },
{ "RotateShape" , "Rotate Shape" , app::KeyAction::RotateShape },
{ "LeftMouseButton" , "Trigger Left Mouse Button" , app::KeyAction::LeftMouseButton },
{ "RightMouseButton" , "Trigger Right Mouse Button" , app::KeyAction::RightMouseButton },
{ NULL , NULL , app::KeyAction::None }
{ "CopySelection" , "Copy Selection" , app::KeyAction::CopySelection, app::KeyContext::TranslatingSelection },
{ "SnapToGrid" , "Snap To Grid" , app::KeyAction::SnapToGrid, app::KeyContext::TranslatingSelection },
{ "LockAxis" , "Lock Axis" , app::KeyAction::LockAxis, app::KeyContext::TranslatingSelection },
{ "FineControl" , "Fine Translating" , app::KeyAction::FineControl , app::KeyContext::TranslatingSelection },
{ "MaintainAspectRatio" , "Maintain Aspect Ratio", app::KeyAction::MaintainAspectRatio, app::KeyContext::ScalingSelection },
{ "ScaleFromCenter" , "Scale From Center" , app::KeyAction::ScaleFromCenter, app::KeyContext::ScalingSelection },
{ "FineControl" , "Fine Scaling" , app::KeyAction::FineControl , app::KeyContext::ScalingSelection },
{ "AngleSnap" , "Angle Snap" , app::KeyAction::AngleSnap, app::KeyContext::RotatingSelection },
{ "AddSelection" , "Add Selection" , app::KeyAction::AddSelection, app::KeyContext::SelectionTool },
{ "SubtractSelection" , "Subtract Selection" , app::KeyAction::SubtractSelection, app::KeyContext::SelectionTool },
{ "IntersectSelection" , "Intersect Selection" , app::KeyAction::IntersectSelection, app::KeyContext::SelectionTool },
{ "AutoSelectLayer" , "Auto Select Layer" , app::KeyAction::AutoSelectLayer, app::KeyContext::MoveTool },
{ "StraightLineFromLastPoint", "Straight Line from Last Point", app::KeyAction::StraightLineFromLastPoint, app::KeyContext::FreehandTool },
{ "AngleSnapFromLastPoint", "Angle Snap from Last Point", app::KeyAction::AngleSnapFromLastPoint, app::KeyContext::FreehandTool },
{ "MoveOrigin" , "Move Origin" , app::KeyAction::MoveOrigin, app::KeyContext::ShapeTool },
{ "SquareAspect" , "Square Aspect" , app::KeyAction::SquareAspect, app::KeyContext::ShapeTool },
{ "DrawFromCenter" , "Draw From Center" , app::KeyAction::DrawFromCenter, app::KeyContext::ShapeTool },
{ "RotateShape" , "Rotate Shape" , app::KeyAction::RotateShape, app::KeyContext::ShapeTool },
{ "LeftMouseButton" , "Trigger Left Mouse Button" , app::KeyAction::LeftMouseButton, app::KeyContext::Any },
{ "RightMouseButton" , "Trigger Right Mouse Button" , app::KeyAction::RightMouseButton, app::KeyContext::Any },
{ NULL , NULL , app::KeyAction::None, app::KeyContext::Any }
};
static struct {
@ -124,9 +127,11 @@ namespace {
return shortcut;
}
std::string get_user_friendly_string_for_keyaction(app::KeyAction action) {
std::string get_user_friendly_string_for_keyaction(app::KeyAction action,
app::KeyContext context) {
for (int c=0; actions[c].name; ++c) {
if (action == actions[c].action)
if (action == actions[c].action &&
context == actions[c].context)
return actions[c].userfriendly;
}
return std::string();
@ -203,7 +208,7 @@ using namespace ui;
//////////////////////////////////////////////////////////////////////
// Key
Key::Key(Command* command, const Params& params, KeyContext keyContext)
Key::Key(Command* command, const Params& params, const KeyContext keyContext)
: m_type(KeyType::Command)
, m_useUsers(false)
, m_keycontext(keyContext)
@ -212,7 +217,7 @@ Key::Key(Command* command, const Params& params, KeyContext keyContext)
{
}
Key::Key(KeyType type, tools::Tool* tool)
Key::Key(const KeyType type, tools::Tool* tool)
: m_type(type)
, m_useUsers(false)
, m_keycontext(KeyContext::Any)
@ -220,12 +225,17 @@ Key::Key(KeyType type, tools::Tool* tool)
{
}
Key::Key(KeyAction action)
Key::Key(const KeyAction action,
const KeyContext keyContext)
: m_type(KeyType::Action)
, m_useUsers(false)
, m_keycontext(KeyContext::Any)
, m_keycontext(keyContext)
, m_action(action)
{
if (m_keycontext != KeyContext::Any)
return;
// Automatic key context
switch (action) {
case KeyAction::None:
m_keycontext = KeyContext::Any;
@ -233,6 +243,7 @@ Key::Key(KeyAction action)
case KeyAction::CopySelection:
case KeyAction::SnapToGrid:
case KeyAction::LockAxis:
case KeyAction::FineControl:
m_keycontext = KeyContext::TranslatingSelection;
break;
case KeyAction::AngleSnap:
@ -269,7 +280,7 @@ Key::Key(KeyAction action)
}
}
Key::Key(WheelAction wheelAction)
Key::Key(const WheelAction wheelAction)
: m_type(KeyType::WheelAction)
, m_useUsers(false)
, m_keycontext(KeyContext::MouseWheel)
@ -395,7 +406,8 @@ std::string Key::triggerString() const
return text;
}
case KeyType::Action:
return get_user_friendly_string_for_keyaction(m_action);
return get_user_friendly_string_for_keyaction(m_action,
m_keycontext);
case KeyType::WheelAction:
return get_user_friendly_string_for_wheelaction(m_wheelAction);
}
@ -570,9 +582,16 @@ void KeyboardShortcuts::importFile(TiXmlElement* rootElement, KeySource source)
if (action_id) {
KeyAction action = base::convert_to<KeyAction, std::string>(action_id);
if (action != KeyAction::None) {
KeyPtr key = this->action(action);
// Read context
KeyContext keycontext = KeyContext::Any;
const char* keycontextstr = xmlKey->Attribute("context");
if (keycontextstr)
keycontext = base::convert_to<KeyContext>(std::string(keycontextstr));
KeyPtr key = this->action(action, keycontext);
if (key && action_key) {
LOG(VERBOSE, "KEYS: Shortcut for action %s: %s\n", action_id, action_key);
LOG(VERBOSE, "KEYS: Shortcut for action %s/%s: %s\n", action_id,
(keycontextstr ? keycontextstr: "Any"), action_key);
Accelerator accel(action_key);
if (!removed)
@ -703,6 +722,9 @@ void KeyboardShortcuts::exportAccel(TiXmlElement& parent, const Key* key, const
case KeyType::Action:
elem.SetAttribute("action",
base::convert_to<std::string>(key->action()).c_str());
if (key->keycontext() != KeyContext::Any)
elem.SetAttribute("context",
base::convert_to<std::string>(key->keycontext()).c_str());
break;
case KeyType::WheelAction:
@ -773,16 +795,18 @@ KeyPtr KeyboardShortcuts::quicktool(tools::Tool* tool)
return key;
}
KeyPtr KeyboardShortcuts::action(KeyAction action)
KeyPtr KeyboardShortcuts::action(KeyAction action,
KeyContext keyContext)
{
for (KeyPtr& key : m_keys) {
if (key->type() == KeyType::Action &&
key->action() == action) {
key->action() == action &&
key->keycontext() == keyContext) {
return key;
}
}
KeyPtr key = std::make_shared<Key>(action);
KeyPtr key = std::make_shared<Key>(action, keyContext);
m_keys.push_back(key);
return key;
}

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2020 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -43,11 +44,13 @@ namespace app {
void reset();
KeyPtr command(const char* commandName,
const Params& params = Params(), KeyContext keyContext = KeyContext::Any);
const Params& params = Params(),
const KeyContext keyContext = KeyContext::Any);
KeyPtr tool(tools::Tool* tool);
KeyPtr quicktool(tools::Tool* tool);
KeyPtr action(KeyAction action);
KeyPtr wheelAction(WheelAction action);
KeyPtr action(const KeyAction action,
const KeyContext keyContext = KeyContext::Any);
KeyPtr wheelAction(const WheelAction action);
void disableAccel(const ui::Accelerator& accel,
const KeyContext keyContext,
@ -88,9 +91,11 @@ namespace app {
commandName, params, keyContext).get());
}
inline std::string key_tooltip(const char* str, KeyAction keyAction) {
inline std::string key_tooltip(const char* str,
KeyAction keyAction,
KeyContext keyContext = KeyContext::Any) {
return key_tooltip(
str, KeyboardShortcuts::instance()->action(keyAction).get());
str, KeyboardShortcuts::instance()->action(keyAction, keyContext).get());
}
} // namespace app

View File

@ -157,6 +157,11 @@ void Mask::invert()
void Mask::replace(const gfx::Rect& bounds)
{
if (bounds.isEmpty()) {
clear();
return;
}
m_bounds = bounds;
m_bitmap.reset(Image::create(IMAGE_BITMAP, bounds.w, bounds.h, m_buffer));

View File

@ -423,6 +423,9 @@ bool Entry::onProcessMessage(Message* msg)
return true;
case kDoubleClickMessage:
if (!hasFocus())
requestFocus();
forwardWord();
m_select = m_caret;
backwardWord();