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_manual" x="150" y="208" w="6" h="6" />
<part id="tiles_auto" x="156" 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="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> </parts>
<styles> <styles>
<style id="box" /> <style id="box" />

View File

@ -590,6 +590,12 @@
locks the movement to one axis (X or Y) --> locks the movement to one axis (X or Y) -->
<key action="LockAxis" shortcut="Shift" /> <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 <!-- When you rotate the selection, pressing this
keyboard shortcut you activate angle snap --> keyboard shortcut you activate angle snap -->
<key action="AngleSnap" shortcut="Shift" /> <key action="AngleSnap" shortcut="Shift" />

View File

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

View File

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

View File

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

View File

@ -1,4 +1,5 @@
// Aseprite // Aseprite
// Copyright (C) 2020 Igara Studio S.A.
// Copyright (C) 2001-2016 David Capello // Copyright (C) 2001-2016 David Capello
// //
// This program is distributed under the terms of // This program is distributed under the terms of
@ -29,6 +30,16 @@ public:
}; };
Corners() : m_corners(NUM_OF_CORNERS) { } 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(); } 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 rightBottom(const gfx::PointF& pt) { m_corners[RIGHT_BOTTOM] = pt; }
void leftBottom(const gfx::PointF& pt) { m_corners[LEFT_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() const {
gfx::RectF bounds; gfx::RectF bounds;
for (int i=0; i<Corners::NUM_OF_CORNERS; ++i) 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::RectF& bounds() const { return m_bounds; }
const gfx::PointF& pivot() const { return m_pivot; } const gfx::PointF& pivot() const { return m_pivot; }
double angle() const { return m_angle; } double angle() const { return m_angle; }
double skew() const { return m_skew; }
void bounds(const gfx::RectF& bounds) { m_bounds = bounds; } void bounds(const gfx::RectF& bounds) { m_bounds = bounds; }
void pivot(const gfx::PointF& pivot) { m_pivot = pivot; } void pivot(const gfx::PointF& pivot) { m_pivot = pivot; }
void angle(double angle) { m_angle = angle; } void angle(double angle) { m_angle = angle; }
void skew(double angle) { m_skew = angle; }
// Applies the transformation (rotation with angle/pivot) to the // Applies the transformation (rotation with angle/pivot) to the
// current bounds (m_bounds). // current bounds (m_bounds).
void transformBox(Corners& corners) const; Corners transformedCorners() const;
// Changes the pivot to another location, adjusting the bounds to // Changes the pivot to another location, adjusting the bounds to
// keep the current rotated-corners in the same location. // keep the current rotated-corners in the same location.
@ -94,12 +95,14 @@ public:
// Static helper method to rotate points. // Static helper method to rotate points.
static gfx::PointF rotatePoint(const gfx::PointF& point, static gfx::PointF rotatePoint(const gfx::PointF& point,
const gfx::PointF& pivot, const gfx::PointF& pivot,
double angle); const double angle,
const double skew);
private: private:
gfx::RectF m_bounds; gfx::RectF m_bounds = gfx::RectF(0.0, 0.0, 0.0, 0.0);
gfx::PointF m_pivot; gfx::PointF m_pivot = gfx::PointF(0.0, 0.0);
double m_angle; double m_angle = 0.0;
double m_skew = 0.0;
}; };
} // namespace app } // namespace app

View File

@ -41,6 +41,7 @@
#include "app/ui/dithering_selector.h" #include "app/ui/dithering_selector.h"
#include "app/ui/dynamics_popup.h" #include "app/ui/dynamics_popup.h"
#include "app/ui/editor/editor.h" #include "app/ui/editor/editor.h"
#include "app/ui/expr_entry.h"
#include "app/ui/icon_button.h" #include "app/ui/icon_button.h"
#include "app/ui/keyboard_shortcuts.h" #include "app/ui/keyboard_shortcuts.h"
#include "app/ui/selection_mode_field.h" #include "app/ui/selection_mode_field.h"
@ -48,6 +49,7 @@
#include "app/ui_context.h" #include "app/ui_context.h"
#include "base/clamp.h" #include "base/clamp.h"
#include "base/fs.h" #include "base/fs.h"
#include "base/pi.h"
#include "base/scoped_value.h" #include "base/scoped_value.h"
#include "doc/brush.h" #include "doc/brush.h"
#include "doc/image.h" #include "doc/image.h"
@ -55,6 +57,7 @@
#include "doc/remap.h" #include "doc/remap.h"
#include "doc/selected_objects.h" #include "doc/selected_objects.h"
#include "doc/slice.h" #include "doc/slice.h"
#include "fmt/format.h"
#include "obs/connection.h" #include "obs/connection.h"
#include "os/surface.h" #include "os/surface.h"
#include "os/system.h" #include "os/system.h"
@ -969,6 +972,156 @@ private:
bool m_lockChange; 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 class ContextBar::DynamicsField : public ButtonSet
, public DynamicsPopup::Delegate { , public DynamicsPopup::Delegate {
public: public:
@ -1529,10 +1682,13 @@ private:
ContextBar::ContextBar(TooltipManager* tooltipManager, ContextBar::ContextBar(TooltipManager* tooltipManager,
ColorBar* colorBar) ColorBar* colorBar)
{ {
auto& pref = Preferences::instance();
addChild(m_selectionOptionsBox = new HBox()); addChild(m_selectionOptionsBox = new HBox());
m_selectionOptionsBox->addChild(m_dropPixels = new DropPixelsField()); m_selectionOptionsBox->addChild(m_dropPixels = new DropPixelsField());
m_selectionOptionsBox->addChild(m_selectionMode = new SelectionModeField); m_selectionOptionsBox->addChild(m_selectionMode = new SelectionModeField);
m_selectionOptionsBox->addChild(m_transparentColor = new TransparentColorField(this, tooltipManager)); 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_pivot = new PivotField);
m_selectionOptionsBox->addChild(m_rotAlgo = new RotAlgorithmField()); m_selectionOptionsBox->addChild(m_rotAlgo = new RotAlgorithmField());
@ -1573,7 +1729,7 @@ ContextBar::ContextBar(TooltipManager* tooltipManager,
m_freehandBox->addChild(m_freehandAlgo = new FreehandAlgorithmField()); m_freehandBox->addChild(m_freehandAlgo = new FreehandAlgorithmField());
addChild(m_symmetry = new SymmetryField()); addChild(m_symmetry = new SymmetryField());
m_symmetry->setVisible(Preferences::instance().symmetryMode.enabled()); m_symmetry->setVisible(pref.symmetryMode.enabled());
addChild(m_sliceFields = new SliceFields); addChild(m_sliceFields = new SliceFields);
@ -1582,7 +1738,6 @@ ContextBar::ContextBar(TooltipManager* tooltipManager,
App::instance()->activeToolManager()->add_observer(this); App::instance()->activeToolManager()->add_observer(this);
UIContext::instance()->add_observer(this); UIContext::instance()->add_observer(this);
auto& pref = Preferences::instance();
pref.symmetryMode.enabled.AfterChange.connect( pref.symmetryMode.enabled.AfterChange.connect(
[this]{ onSymmetryModeChange(); }); [this]{ onSymmetryModeChange(); });
pref.colorBar.fgColor.AfterChange.connect( pref.colorBar.fgColor.AfterChange.connect(
@ -1927,6 +2082,7 @@ void ContextBar::updateForTool(tools::Tool* tool)
m_selectionMode->setVisible(true); m_selectionMode->setVisible(true);
m_pivot->setVisible(true); m_pivot->setVisible(true);
m_dropPixels->setVisible(false); m_dropPixels->setVisible(false);
m_transformation->setVisible(false);
m_selectBoxHelp->setVisible(false); m_selectBoxHelp->setVisible(false);
m_symmetry->setVisible( m_symmetry->setVisible(
@ -1943,7 +2099,7 @@ void ContextBar::updateForTool(tools::Tool* tool)
layout(); layout();
} }
void ContextBar::updateForMovingPixels() void ContextBar::updateForMovingPixels(const Transformation& t)
{ {
tools::Tool* tool = App::instance()->toolBox()->getToolById( tools::Tool* tool = App::instance()->toolBox()->getToolById(
tools::WellKnownTools::RectangularMarquee); tools::WellKnownTools::RectangularMarquee);
@ -1953,6 +2109,8 @@ void ContextBar::updateForMovingPixels()
m_dropPixels->deselectItems(); m_dropPixels->deselectItems();
m_dropPixels->setVisible(true); m_dropPixels->setVisible(true);
m_selectionMode->setVisible(false); m_selectionMode->setVisible(false);
m_transformation->setVisible(true);
m_transformation->update(t);
layout(); layout();
} }

View File

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

View File

@ -116,8 +116,8 @@ protected:
case kKeyUpMessage: case kKeyUpMessage:
if (static_cast<KeyMessage*>(msg)->repeat() == 0) { if (static_cast<KeyMessage*>(msg)->repeat() == 0) {
KeyboardShortcuts* keys = KeyboardShortcuts::instance(); KeyboardShortcuts* keys = KeyboardShortcuts::instance();
KeyPtr lmb = keys->action(KeyAction::LeftMouseButton); KeyPtr lmb = keys->action(KeyAction::LeftMouseButton, KeyContext::Any);
KeyPtr rmb = keys->action(KeyAction::RightMouseButton); KeyPtr rmb = keys->action(KeyAction::RightMouseButton, KeyContext::Any);
// Convert action keys into mouse messages. // Convert action keys into mouse messages.
if (lmb->isPressed(msg, *keys) || 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(); ExtraCelRef extraCel = m_document->extraCel();
if (extraCel && extraCel->type() != render::ExtraType::NONE) { if (extraCel &&
extraCel->type() != render::ExtraType::NONE) {
m_renderEngine->setExtraImage( m_renderEngine->setExtraImage(
extraCel->type(), extraCel->type(),
extraCel->cel(), extraCel->cel(),
@ -1307,7 +1308,7 @@ void Editor::flashCurrentLayer()
return; return;
Site site = getSite(); Site site = getSite();
if (const Cel* src_cel = site.cel()) { if (site.cel()) {
// Hide and destroy the extra cel used by the brush preview // Hide and destroy the extra cel used by the brush preview
// because we'll need to use the extra cel now for the flashing // because we'll need to use the extra cel now for the flashing
// layer. // layer.
@ -2514,7 +2515,7 @@ void Editor::pasteImage(const Image* image, const Mask* mask)
void Editor::startSelectionTransformation(const gfx::Point& move, double angle) void Editor::startSelectionTransformation(const gfx::Point& move, double angle)
{ {
if (auto movingPixels = dynamic_cast<MovingPixelsState*>(m_state.get())) { if (auto movingPixels = dynamic_cast<MovingPixelsState*>(m_state.get())) {
movingPixels->translate(move); movingPixels->translate(gfx::PointF(move));
if (std::fabs(angle) > 1e-5) if (std::fabs(angle) > 1e-5)
movingPixels->rotate(angle); movingPixels->rotate(angle);
} }
@ -2531,6 +2532,12 @@ void Editor::startFlipTransformation(doc::algorithm::FlipType flipType)
standby->startFlipTransformation(this, 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() void Editor::notifyScrollChanged()
{ {
m_observers.notifyScrollChanged(this); m_observers.notifyScrollChanged(this);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -53,11 +53,17 @@
#define DUMP_INNER_CMDS() #define DUMP_INNER_CMDS()
#endif #endif
using vec2 = base::Vector2d<double>;
namespace app { namespace app {
template<typename T> template<typename T>
static inline const base::Vector2d<double> point2Vector(const gfx::PointT<T>& pt) { static inline const vec2 to_vec2(const gfx::PointT<T>& pt) {
return base::Vector2d<double>(pt.x, pt.y); 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) PixelsMovement::InnerCmd::InnerCmd(InnerCmd&& c)
@ -180,6 +186,11 @@ bool PixelsMovement::editMultipleCels() const
m_site.range().type() == DocRange::kCels)); m_site.range().type() == DocRange::kCels));
} }
void PixelsMovement::setDelegate(PixelsMovementDelegate* delegate)
{
m_delegate = delegate;
}
void PixelsMovement::setFastMode(const bool fastMode) void PixelsMovement::setFastMode(const bool fastMode)
{ {
bool redraw = (m_fastMode && !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() void PixelsMovement::trim()
{ {
ContextWriter writer(m_reader, 1000); ContextWriter writer(m_reader, 1000);
@ -296,7 +350,7 @@ void PixelsMovement::copyMask()
hideDocumentMask(); hideDocumentMask();
} }
void PixelsMovement::catchImage(const gfx::Point& pos, HandleType handle) void PixelsMovement::catchImage(const gfx::PointF& pos, HandleType handle)
{ {
ASSERT(handle != NoHandle); ASSERT(handle != NoHandle);
@ -305,7 +359,7 @@ void PixelsMovement::catchImage(const gfx::Point& pos, HandleType handle)
m_handle = 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 // Create a new Transaction to move the pixels to other position
m_initialData = m_currentData; m_initialData = m_currentData;
@ -316,33 +370,33 @@ void PixelsMovement::catchImageAgain(const gfx::Point& pos, HandleType handle)
hideDocumentMask(); 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); ContextWriter writer(m_reader, 1000);
gfx::RectF bounds = m_initialData.bounds(); gfx::RectF bounds = m_initialData.bounds();
bool updateBounds = false; gfx::PointF abs_initial_pivot = m_initialData.pivot();
double dx, dy; gfx::PointF abs_pivot = m_currentData.pivot();
dx = ((pos.x - m_catchPos.x) * cos(m_currentData.angle()) + auto newTransformation = m_currentData;
(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()));
switch (m_handle) { 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 ((moveModifier & LockAxisMovement) == LockAxisMovement) {
if (ABS(dx) < ABS(dy)) if (std::abs(dx) < std::abs(dy))
dx = 0.0; dx = 0.0;
else else
dy = 0.0; dy = 0.0;
} }
bounds.offset(dx, dy); bounds.offset(dx, dy);
updateBounds = true;
if ((moveModifier & SnapToGridMovement) == SnapToGridMovement) { if ((moveModifier & SnapToGridMovement) == SnapToGridMovement) {
// Snap the x1,y1 point to the grid. // 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 // Now we calculate the difference from x1,y1 point and we can
// use it to adjust all coordinates (x1, y1, x2, y2). // use it to adjust all coordinates (x1, y1, x2, y2).
gridOffset -= bounds.origin(); bounds.setOrigin(gridOffset);
bounds.offset(gridOffset);
} }
newTransformation.bounds(bounds);
newTransformation.pivot(abs_initial_pivot +
bounds.origin() -
m_initialData.bounds().origin());
break; break;
}
case ScaleNWHandle: case ScaleNWHandle:
case ScaleNHandle: case ScaleNHandle:
@ -379,8 +438,7 @@ void PixelsMovement::moveImage(const gfx::Point& pos, MoveModifier moveModifier)
handles[m_handle-ScaleNWHandle][1]); handles[m_handle-ScaleNWHandle][1]);
if ((moveModifier & ScaleFromPivot) == ScaleFromPivot) { if ((moveModifier & ScaleFromPivot) == ScaleFromPivot) {
pivot.x = m_currentData.pivot().x; pivot = m_currentData.pivot();
pivot.y = m_currentData.pivot().y;
} }
else { else {
pivot.x = 1.0 - handle.x; pivot.x = 1.0 - handle.x;
@ -393,26 +451,36 @@ void PixelsMovement::moveImage(const gfx::Point& pos, MoveModifier moveModifier)
gfx::PointF b = bounds.point2(); gfx::PointF b = bounds.point2();
if ((moveModifier & MaintainAspectRatioMovement) == MaintainAspectRatioMovement) { if ((moveModifier & MaintainAspectRatioMovement) == MaintainAspectRatioMovement) {
auto u = point2Vector(gfx::PointF(m_catchPos) - pivot); vec2 u = to_vec2(m_catchPos - pivot);
auto v = point2Vector(gfx::PointF(pos) - pivot); vec2 v = to_vec2(pos - pivot);
auto w = v.projectOn(u); vec2 w = v.projectOn(u);
double scale = u.magnitude(); 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; scale = (std::fabs(w.angle()-u.angle()) < PI/2.0 ? 1.0: -1.0) * w.magnitude() / scale;
}
else else
scale = 1.0; scale = 1.0;
a.x = int((a.x-pivot.x)*scale + pivot.x); a.x = ((a.x-pivot.x)*scale + pivot.x);
a.y = int((a.y-pivot.y)*scale + pivot.y); a.y = ((a.y-pivot.y)*scale + pivot.y);
b.x = int((b.x-pivot.x)*scale + pivot.x); b.x = ((b.x-pivot.x)*scale + pivot.x);
b.y = int((b.y-pivot.y)*scale + pivot.y); b.y = ((b.y-pivot.y)*scale + pivot.y);
} }
else { else {
handle.x = bounds.x + bounds.w*handle.x; handle.x = bounds.x + bounds.w*handle.x;
handle.y = bounds.y + bounds.h*handle.y; handle.y = bounds.y + bounds.h*handle.y;
double z = m_currentData.angle();
double w = (handle.x-pivot.x); double w = (handle.x-pivot.x);
double h = (handle.y-pivot.y); 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) { if (m_handle == ScaleNHandle || m_handle == ScaleSHandle) {
dx = 0.0; dx = 0.0;
@ -423,10 +491,10 @@ void PixelsMovement::moveImage(const gfx::Point& pos, MoveModifier moveModifier)
h = 1.0; h = 1.0;
} }
a.x = int((a.x-pivot.x)*(1.0+dx/w) + pivot.x); a.x = ((a.x-pivot.x)*(1.0+dx/w) + pivot.x);
a.y = int((a.y-pivot.y)*(1.0+dy/h) + pivot.y); a.y = ((a.y-pivot.y)*(1.0+dy/h) + pivot.y);
b.x = int((b.x-pivot.x)*(1.0+dx/w) + pivot.x); b.x = ((b.x-pivot.x)*(1.0+dx/w) + pivot.x);
b.y = int((b.y-pivot.y)*(1.0+dy/h) + pivot.y); 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 // 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.w = b.x - a.x;
bounds.h = b.y - a.y; bounds.h = b.y - a.y;
updateBounds = true; newTransformation.bounds(bounds);
m_adjustPivot = true;
break; break;
} }
case RotateNWHandle: case RotateNWHandle:
case RotateNHandle:
case RotateNEHandle: case RotateNEHandle:
case RotateWHandle:
case RotateEHandle:
case RotateSWHandle: case RotateSWHandle:
case RotateSHandle: case RotateSEHandle: {
case RotateSEHandle: double da = (std::atan2((double)(-pos.y + abs_pivot.y),
{ (double)(+pos.x - abs_pivot.x)) -
gfx::PointF abs_initial_pivot = m_initialData.pivot(); std::atan2((double)(-m_catchPos.y + abs_initial_pivot.y),
gfx::PointF abs_pivot = m_currentData.pivot(); (double)(+m_catchPos.x - abs_initial_pivot.x)));
double newAngle = m_initialData.angle() + da;
newAngle = base::fmod_radians(newAngle);
double newAngle = // Is the "angle snap" is activated, we've to snap the angle
m_initialData.angle() // to common (pixel art) angles.
+ atan2((double)(-pos.y + abs_pivot.y), if ((moveModifier & AngleSnapMovement) == AngleSnapMovement) {
(double)(+pos.x - abs_pivot.x)) // TODO make this configurable
- atan2((double)(-m_catchPos.y + abs_initial_pivot.y), static const double keyAngles[] = {
(double)(+m_catchPos.x - abs_initial_pivot.x)); 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 int closest = 0;
// to common (pixel art) angles. int last = sizeof(keyAngles) / sizeof(keyAngles[0]) - 1;
if ((moveModifier & AngleSnapMovement) == AngleSnapMovement) { for (int i=0; i<=last; ++i) {
// TODO make this configurable if (std::fabs(newAngleDegrees-keyAngles[closest]) >
static const double keyAngles[] = { std::fabs(newAngleDegrees-keyAngles[i]))
0.0, 26.565, 45.0, 63.435, 90.0, 116.565, 135.0, 153.435, 180.0, closest = i;
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;
} }
m_currentData.angle(newAngle); newAngle = PI * keyAngles[closest] / 180.0;
} }
newTransformation.angle(newAngle);
break; break;
}
case PivotHandle: case SkewNHandle:
{ case SkewSHandle:
// Calculate the new position of the pivot case SkewWHandle:
gfx::PointF newPivot(m_initialData.pivot().x + (pos.x - m_catchPos.x), case SkewEHandle: {
m_initialData.pivot().y + (pos.y - m_catchPos.y)); // 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; // Pivot in pixels
m_currentData.displacePivotTo(newPivot); 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; 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) { setTransformationBase(newTransformation);
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());
}
} }
void PixelsMovement::getDraggedImageCopy(std::unique_ptr<Image>& outputImage, void PixelsMovement::getDraggedImageCopy(std::unique_ptr<Image>& outputImage,
std::unique_ptr<Mask>& outputMask) std::unique_ptr<Mask>& outputMask)
{ {
gfx::Rect bounds = m_currentData.transformedBounds(); gfx::Rect bounds = m_currentData.transformedBounds();
if (bounds.isEmpty())
return;
std::unique_ptr<Image> image( std::unique_ptr<Image> image(
Image::create( Image::create(
m_site.sprite()->pixelFormat(), bounds.w, bounds.h)); 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 // Draw mask without shrinking it, so the mask size is equal to the
// "image" render. // "image" render.
@ -623,8 +792,11 @@ void PixelsMovement::stampImage(bool finalStamp)
void PixelsMovement::stampExtraCelImage() void PixelsMovement::stampExtraCelImage()
{ {
const Cel* cel = m_extraCel->cel();
const Image* image = m_extraCel->image(); 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 // Expand the canvas to paste the image in the fully visible
// portion of sprite. // portion of sprite.
@ -664,27 +836,10 @@ void PixelsMovement::dropImageTemporarily()
// Displace the pivot to the new site: // Displace the pivot to the new site:
if (m_adjustPivot) { if (m_adjustPivot) {
m_adjustPivot = false; m_adjustPivot = false;
adjustPivot();
// Get the a factor for the X/Y position of the initial pivot if (m_delegate)
// position inside the initial non-rotated bounds. m_delegate->onPivotChange();
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));
} }
redrawCurrentMask(); 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() void PixelsMovement::dropImage()
{ {
m_isDragging = false; m_isDragging = false;
@ -780,28 +958,35 @@ void PixelsMovement::redrawExtraImage(Transformation* transformation)
Cel* cel = m_site.cel(); Cel* cel = m_site.cel();
if (cel) opacity = MUL_UN8(opacity, cel->opacity(), t); if (cel) opacity = MUL_UN8(opacity, cel->opacity(), t);
gfx::Rect bounds = transformation->transformedBounds();
if (!m_extraCel) if (!m_extraCel)
m_extraCel.reset(new ExtraCel); m_extraCel.reset(new ExtraCel);
m_extraCel->create( gfx::Rect bounds = transformation->transformedBounds();
m_site.tilemapMode(), if (!bounds.isEmpty()) {
m_document->sprite(), m_extraCel->create(
bounds, m_site.tilemapMode(),
(m_site.tilemapMode() == TilemapMode::Tiles ? m_site.grid().tileToCanvas(bounds).size(): m_document->sprite(),
bounds.size()), bounds,
m_site.frame(), (m_site.tilemapMode() == TilemapMode::Tiles ? m_site.grid().tileToCanvas(bounds).size():
opacity); bounds.size()),
m_extraCel->setType(render::ExtraType::PATCH); m_site.frame(),
m_extraCel->setBlendMode(m_site.layer()->isImage() ? opacity);
static_cast<LayerImage*>(m_site.layer())->blendMode(): m_extraCel->setType(render::ExtraType::PATCH);
BlendMode::NORMAL); 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); m_document->setExtraCel(m_extraCel);
// Draw the transformed pixels in the extra-cel which is the chunk if (m_extraCel->image()) {
// of pixels that the user is moving. // Draw the transformed pixels in the extra-cel which is the chunk
drawImage(*transformation, m_extraCel->image(), bounds.origin(), true); // of pixels that the user is moving.
drawImage(*transformation, m_extraCel->image(),
gfx::PointF(bounds.origin()), true);
}
} }
void PixelsMovement::redrawCurrentMask() void PixelsMovement::redrawCurrentMask()
@ -811,13 +996,12 @@ void PixelsMovement::redrawCurrentMask()
void PixelsMovement::drawImage( void PixelsMovement::drawImage(
const Transformation& transformation, const Transformation& transformation,
doc::Image* dst, const gfx::Point& pt, doc::Image* dst, const gfx::PointF& pt,
const bool renderOriginalLayer) const bool renderOriginalLayer)
{ {
ASSERT(dst); ASSERT(dst);
Transformation::Corners corners; auto corners = transformation.transformedCorners();
transformation.transformBox(corners);
gfx::Rect bounds = corners.bounds(); gfx::Rect bounds = corners.bounds();
dst->setMaskColor(m_site.sprite()->transparentColor()); dst->setMaskColor(m_site.sprite()->transparentColor());
@ -854,10 +1038,14 @@ void PixelsMovement::drawImage(
void PixelsMovement::drawMask(doc::Mask* mask, bool shrink) void PixelsMovement::drawMask(doc::Mask* mask, bool shrink)
{ {
Transformation::Corners corners; auto corners = m_currentData.transformedCorners();
m_currentData.transformBox(corners);
gfx::Rect bounds = corners.bounds(); gfx::Rect bounds = corners.bounds();
if (bounds.isEmpty()) {
mask->clear();
return;
}
mask->replace(bounds); mask->replace(bounds);
if (shrink) if (shrink)
mask->freeze(); mask->freeze();
@ -866,7 +1054,8 @@ void PixelsMovement::drawMask(doc::Mask* mask, bool shrink)
mask->bitmap(), mask->bitmap(),
m_initialMask->bitmap(), m_initialMask->bitmap(),
nullptr, nullptr,
corners, bounds.origin()); corners,
gfx::PointF(bounds.origin()));
if (shrink) if (shrink)
mask->unfreeze(); mask->unfreeze();
} }
@ -875,7 +1064,7 @@ void PixelsMovement::drawParallelogram(
const Transformation& transformation, const Transformation& transformation,
doc::Image* dst, const doc::Image* src, const doc::Mask* mask, doc::Image* dst, const doc::Image* src, const doc::Mask* mask,
const Transformation::Corners& corners, const Transformation::Corners& corners,
const gfx::Point& leftTop) const gfx::PointF& leftTop)
{ {
tools::RotationAlgorithm rotAlgo = Preferences::instance().selection.rotationAlgorithm(); tools::RotationAlgorithm rotAlgo = Preferences::instance().selection.rotationAlgorithm();
@ -943,6 +1132,9 @@ void PixelsMovement::onPivotChange()
{ {
set_pivot_from_preferences(m_currentData); set_pivot_from_preferences(m_currentData);
onRotationAlgorithmChange(); onRotationAlgorithmChange();
if (m_delegate)
m_delegate->onPivotChange();
} }
void PixelsMovement::onRotationAlgorithmChange() void PixelsMovement::onRotationAlgorithmChange()

View File

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

View File

@ -51,13 +51,13 @@
#include "app/util/layer_utils.h" #include "app/util/layer_utils.h"
#include "app/util/new_image_from_mask.h" #include "app/util/new_image_from_mask.h"
#include "app/util/readable_time.h" #include "app/util/readable_time.h"
#include "base/clamp.h"
#include "base/pi.h" #include "base/pi.h"
#include "doc/grid.h" #include "doc/grid.h"
#include "doc/layer.h" #include "doc/layer.h"
#include "doc/mask.h" #include "doc/mask.h"
#include "doc/slice.h" #include "doc/slice.h"
#include "doc/sprite.h" #include "doc/sprite.h"
#include "fixmath/fixmath.h"
#include "fmt/format.h" #include "fmt/format.h"
#include "gfx/rect.h" #include "gfx/rect.h"
#include "os/surface.h" #include "os/surface.h"
@ -74,17 +74,6 @@ namespace app {
using namespace ui; using namespace ui;
static CursorType rotated_size_cursors[8] = {
kSizeECursor,
kSizeNECursor,
kSizeNCursor,
kSizeNWCursor,
kSizeWCursor,
kSizeSWCursor,
kSizeSCursor,
kSizeSECursor
};
#ifdef _MSC_VER #ifdef _MSC_VER
#pragma warning(disable:4355) // warning C4355: 'this' : used in base member initializer list #pragma warning(disable:4355) // warning C4355: 'this' : used in base member initializer list
#endif #endif
@ -714,7 +703,7 @@ void StandbyState::startSelectionTransformation(Editor* editor,
transformSelection(editor, NULL, NoHandle); transformSelection(editor, NULL, NoHandle);
if (MovingPixelsState* movingPixels = dynamic_cast<MovingPixelsState*>(editor->getState().get())) { if (MovingPixelsState* movingPixels = dynamic_cast<MovingPixelsState*>(editor->getState().get())) {
movingPixels->translate(move); movingPixels->translate(gfx::PointF(move));
if (std::fabs(angle) > 1e-5) if (std::fabs(angle) > 1e-5)
movingPixels->rotate(angle); movingPixels->rotate(angle);
} }
@ -905,65 +894,77 @@ bool StandbyState::Decorator::onSetCursor(tools::Ink* ink, Editor* editor, const
CursorType newCursorType = kArrowCursor; CursorType newCursorType = kArrowCursor;
const Cursor* newCursor = nullptr; 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) { switch (handle) {
case ScaleNWHandle: newCursorType = kSizeNWCursor; break; case ScaleNWHandle: newCursorType = kSizeNWCursor; break;
case ScaleNHandle: newCursorType = kSizeNCursor; break; case ScaleNHandle: newCursorType = kSizeNCursor; break;
case ScaleNEHandle: newCursorType = kSizeNECursor; break; case ScaleNEHandle: newCursorType = kSizeNECursor; break;
case ScaleWHandle: newCursorType = kSizeWCursor; break; case ScaleWHandle: newCursorType = kSizeWCursor; break;
case ScaleEHandle: newCursorType = kSizeECursor; break; case ScaleEHandle: newCursorType = kSizeECursor; break;
case ScaleSWHandle: newCursorType = kSizeSWCursor; break; case ScaleSWHandle: newCursorType = kSizeSWCursor; break;
case ScaleSHandle: newCursorType = kSizeSCursor; break; case ScaleSHandle: newCursorType = kSizeSCursor; break;
case ScaleSEHandle: newCursorType = kSizeSECursor; break; case ScaleSEHandle: newCursorType = kSizeSECursor; break;
case RotateNWHandle: newCursor = theme->cursors.rotateNw(); break; case PivotHandle: newCursorType = kHandCursor; break;
case RotateNHandle: newCursor = theme->cursors.rotateN(); break; case RotateNWHandle:
case RotateNEHandle: newCursor = theme->cursors.rotateNe(); break; case RotateNEHandle:
case RotateWHandle: newCursor = theme->cursors.rotateW(); break; case RotateSWHandle:
case RotateEHandle: newCursor = theme->cursors.rotateE(); break; case RotateSEHandle:
case RotateSWHandle: newCursor = theme->cursors.rotateSw(); break; case SkewNHandle:
case RotateSHandle: newCursor = theme->cursors.rotateS(); break; case SkewWHandle:
case RotateSEHandle: newCursor = theme->cursors.rotateSe(); break; case SkewEHandle:
case PivotHandle: newCursorType = kHandCursor; break; case SkewSHandle:
break;
default: default:
return false; 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 && if (newCursorType >= kSizeNCursor &&
newCursorType <= kSizeNWCursor) { newCursorType <= kSizeNWCursor) {
size_t num = sizeof(rotated_size_cursors) / sizeof(rotated_size_cursors[0]); const CursorType rotated_size_cursors[8] = {
size_t c; kSizeWCursor,
for (c=num-1; c>0; --c) kSizeSWCursor, kSizeSCursor, kSizeSECursor,
if (rotated_size_cursors[c] == newCursorType) kSizeECursor,
break; kSizeNECursor, kSizeNCursor, kSizeNWCursor,
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()
}; };
size_t num = sizeof(rotated_rotate_cursors) / sizeof(rotated_rotate_cursors[0]); newCursorType = rotated_size_cursors[angle];
size_t c; }
for (c=num-1; c>0; --c) else if (handle >= RotateNWHandle &&
if (rotated_rotate_cursors[c] == newCursor) handle <= RotateSEHandle) {
break; const Cursor* rotated_rotate_cursors[8] = {
theme->cursors.rotateW(),
newCursor = rotated_rotate_cursors[(c+angle) % num]; 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; newCursorType = kCustomCursor;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -157,6 +157,11 @@ void Mask::invert()
void Mask::replace(const gfx::Rect& bounds) void Mask::replace(const gfx::Rect& bounds)
{ {
if (bounds.isEmpty()) {
clear();
return;
}
m_bounds = bounds; m_bounds = bounds;
m_bitmap.reset(Image::create(IMAGE_BITMAP, bounds.w, bounds.h, m_buffer)); 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; return true;
case kDoubleClickMessage: case kDoubleClickMessage:
if (!hasFocus())
requestFocus();
forwardWord(); forwardWord();
m_select = m_caret; m_select = m_caret;
backwardWord(); backwardWord();