Fix symmetry button is kept pressed when we didn't pressed (fix #4760)

This fix removes the 'non sense symmetry filter' to prevent
some buttons from being unintentionally held down.

Instead of enabling ALL symmetry buttons via automatic logic,
flags were added to differentiate actual symmetry mode from
actual symmetry buttons being pressed.

Moved the drawing process for symmetry axes from
'Editor::drawSpriteUnclippedRect' to
'Editor::drawOneSpriteUnclippedRect' to allow semi-transparent axes.
This also produces axes on every tile in tile mode.

Pixel ratios other than 1:1 are now considered in the drawing logic of
diagonal axes.
This commit is contained in:
Gaspar Capello 2025-02-21 10:43:50 -03:00
parent 6c69840184
commit 5570559c34
4 changed files with 117 additions and 153 deletions

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Aseprite -->
<!-- Copyright (C) 2018-2024 Igara Studio S.A. -->
<!-- Copyright (C) 2018-2025 Igara Studio S.A. -->
<!-- Copyright (C) 2014-2018 David Capello -->
<preferences>
@ -522,6 +522,7 @@
<option id="mode" type="SymmetryMode" default="SymmetryMode::NONE" />
<option id="x_axis" type="double" default="0" />
<option id="y_axis" type="double" default="0" />
<option id="button_flags" type="int" default="0" />
</section>
<section id="grid" canforce="true">
<option id="snap" type="bool" default="false" />

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2024 Igara Studio S.A.
// Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -1530,14 +1530,10 @@ public:
DocumentPreferences& docPref = Preferences::instance().document(doc);
at(0)->setSelected(
int(docPref.symmetry.mode()) & int(app::gen::SymmetryMode::HORIZONTAL) ? true : false);
at(1)->setSelected(
int(docPref.symmetry.mode()) & int(app::gen::SymmetryMode::VERTICAL) ? true : false);
at(2)->setSelected(
int(docPref.symmetry.mode()) & int(app::gen::SymmetryMode::RIGHT_DIAG) ? true : false);
at(3)->setSelected(
int(docPref.symmetry.mode()) & int(app::gen::SymmetryMode::LEFT_DIAG) ? true : false);
at(0)->setSelected(docPref.symmetry.buttonFlags() & int(app::gen::SymmetryMode::HORIZONTAL));
at(1)->setSelected(docPref.symmetry.buttonFlags() & int(app::gen::SymmetryMode::VERTICAL));
at(2)->setSelected(docPref.symmetry.buttonFlags() & int(app::gen::SymmetryMode::RIGHT_DIAG));
at(3)->setSelected(docPref.symmetry.buttonFlags() & int(app::gen::SymmetryMode::LEFT_DIAG));
}
private:
@ -1551,75 +1547,29 @@ private:
DocumentPreferences& docPref = Preferences::instance().document(doc);
auto oldMode = docPref.symmetry.mode();
int mode = 0;
int buttonFlags = 0;
if (at(0)->isSelected())
mode |= int(app::gen::SymmetryMode::HORIZONTAL);
buttonFlags |= int(app::gen::SymmetryMode::HORIZONTAL);
if (at(1)->isSelected())
mode |= int(app::gen::SymmetryMode::VERTICAL);
buttonFlags |= int(app::gen::SymmetryMode::VERTICAL);
if (at(2)->isSelected())
mode |= int(app::gen::SymmetryMode::RIGHT_DIAG);
buttonFlags |= int(app::gen::SymmetryMode::RIGHT_DIAG);
if (at(3)->isSelected())
mode |= int(app::gen::SymmetryMode::LEFT_DIAG);
buttonFlags |= int(app::gen::SymmetryMode::LEFT_DIAG);
// Non sense symmetries filter:
// - H + 1Diag
// - V + 1Diag
// - H + V + 1Diag
const bool HorV = (mode & int(app::gen::SymmetryMode::HORIZONTAL)) ||
(mode & int(app::gen::SymmetryMode::VERTICAL));
const bool HxorV = !(mode & int(app::gen::SymmetryMode::HORIZONTAL)) !=
!(mode & int(app::gen::SymmetryMode::VERTICAL));
const bool RDxorLD = !(mode & int(app::gen::SymmetryMode::RIGHT_DIAG)) !=
!(mode & int(app::gen::SymmetryMode::LEFT_DIAG));
if (oldMode == gen::SymmetryMode::HORIZONTAL || oldMode == gen::SymmetryMode::VERTICAL ||
oldMode == gen::SymmetryMode::BOTH) {
if (HorV && RDxorLD) {
mode = int(app::gen::SymmetryMode::ALL);
at(0)->setSelected(true);
at(1)->setSelected(true);
at(2)->setSelected(true);
at(3)->setSelected(true);
}
}
else if (oldMode == gen::SymmetryMode::ALL) {
if (HxorV) {
mode = int(app::gen::SymmetryMode::BOTH_DIAG);
at(0)->setSelected(false);
at(1)->setSelected(false);
}
else if (RDxorLD) {
mode = int(app::gen::SymmetryMode::BOTH);
at(2)->setSelected(false);
at(3)->setSelected(false);
}
}
else if ((oldMode == gen::SymmetryMode::RIGHT_DIAG || oldMode == gen::SymmetryMode::LEFT_DIAG ||
oldMode == gen::SymmetryMode::BOTH_DIAG) &&
HorV) {
mode = int(app::gen::SymmetryMode::ALL);
at(0)->setSelected(true);
at(1)->setSelected(true);
at(2)->setSelected(true);
at(3)->setSelected(true);
}
// Non sense symmetries filter end
const bool forceAllMode = ((buttonFlags & int(app::gen::SymmetryMode::HORIZONTAL)) ||
(buttonFlags & int(app::gen::SymmetryMode::VERTICAL))) &&
((buttonFlags & int(app::gen::SymmetryMode::RIGHT_DIAG)) ||
(buttonFlags & int(app::gen::SymmetryMode::LEFT_DIAG)));
if (at(0)->isSelected())
mode |= int(app::gen::SymmetryMode::HORIZONTAL);
if (at(1)->isSelected())
mode |= int(app::gen::SymmetryMode::VERTICAL);
if (at(2)->isSelected())
mode |= int(app::gen::SymmetryMode::RIGHT_DIAG);
if (at(3)->isSelected())
mode |= int(app::gen::SymmetryMode::LEFT_DIAG);
if (app::gen::SymmetryMode(mode) != docPref.symmetry.mode()) {
docPref.symmetry.mode(app::gen::SymmetryMode(mode));
// Redraw symmetry rules
if (buttonFlags != docPref.symmetry.buttonFlags()) {
docPref.symmetry.mode(forceAllMode ? app::gen::SymmetryMode::ALL :
app::gen::SymmetryMode(buttonFlags));
docPref.symmetry.buttonFlags(buttonFlags);
doc->notifyGeneralUpdate();
}
else if (at(4)->isSelected()) {
if (at(4)->isSelected()) {
auto* item = at(4);
gfx::Rect bounds = item->bounds();

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2024 Igara Studio S.A.
// Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -831,6 +831,85 @@ void Editor::drawOneSpriteUnclippedRect(ui::Graphics* g,
m_docPref.grid.forceSection();
}
m_docPref.show.grid.forceDirtyFlag();
// Symmetry mode
if (isActive() && (m_flags & Editor::kShowSymmetryLine) &&
Preferences::instance().symmetryMode.enabled()) {
const int mode = int(m_docPref.symmetry.mode());
const gfx::Color color = color_utils::color_for_ui(m_docPref.grid.color());
const gfx::Color semiTransparentColor =
gfx::rgba(rgba_getr(color), rgba_getg(color), rgba_getb(color), rgba_geta(color) / 4);
const int symmetryButtonFlags = m_docPref.symmetry.buttonFlags();
const double x = int(m_proj.applyX<double>(m_docPref.symmetry.xAxis()));
const double y = int(m_proj.applyY<double>(m_docPref.symmetry.yAxis()));
if (mode & int(app::gen::SymmetryMode::HORIZONTAL) && x > 0) {
g->drawVLine(symmetryButtonFlags & int(app::gen::SymmetryMode::HORIZONTAL) ?
color :
semiTransparentColor,
enclosingRect.x + x,
enclosingRect.y,
enclosingRect.h);
}
if (mode & int(app::gen::SymmetryMode::VERTICAL) && y > 0) {
g->drawHLine(symmetryButtonFlags & int(app::gen::SymmetryMode::VERTICAL) ?
color :
semiTransparentColor,
enclosingRect.x,
enclosingRect.y + y,
enclosingRect.w);
}
if (mode & int(app::gen::SymmetryMode::RIGHT_DIAG)) {
// Bottom point intersection:
gfx::Point bottomLeft(
enclosingRect.x + x + m_proj.turnYinTermsOfX<int>(y - enclosingRect.h),
enclosingRect.y2());
if (bottomLeft.x < enclosingRect.x) {
// Left intersection
bottomLeft.y = enclosingRect.y2() +
m_proj.turnXinTermsOfY<int>(bottomLeft.x - enclosingRect.x);
bottomLeft.x = enclosingRect.x;
}
// Top intersection
gfx::Point topRight(enclosingRect.x + x + m_proj.turnYinTermsOfX<int>(y),
enclosingRect.y);
if (enclosingRect.x2() < topRight.x) {
// Right intersection
topRight.y = enclosingRect.y +
m_proj.applyY<int>(m_proj.removeX<int>(topRight.x - enclosingRect.x2()));
topRight.x = enclosingRect.x2();
}
g->drawLine(symmetryButtonFlags & int(app::gen::SymmetryMode::RIGHT_DIAG) ?
color :
semiTransparentColor,
bottomLeft,
topRight);
}
if (mode & int(app::gen::SymmetryMode::LEFT_DIAG)) {
// Bottom point intersection:
gfx::Point bottomRight(
enclosingRect.x + x + m_proj.turnYinTermsOfX<int>(enclosingRect.h - y),
enclosingRect.y2());
if (enclosingRect.x2() < bottomRight.x) {
// Left intersection
bottomRight.y = enclosingRect.y2() +
m_proj.turnXinTermsOfY<int>(enclosingRect.x2() - bottomRight.x);
bottomRight.x = enclosingRect.x2();
}
// Top intersection
gfx::Point topLeft(enclosingRect.x + x - m_proj.turnYinTermsOfX<int>(y), enclosingRect.y);
if (topLeft.x < enclosingRect.x) {
// Right intersection
topLeft.y = enclosingRect.y + m_proj.turnXinTermsOfY<int>(enclosingRect.x - topLeft.x);
topLeft.x = enclosingRect.x;
}
g->drawLine(symmetryButtonFlags & int(app::gen::SymmetryMode::LEFT_DIAG) ?
color :
semiTransparentColor,
topLeft,
bottomRight);
}
}
}
}
}
@ -907,86 +986,6 @@ void Editor::drawSpriteUnclippedRect(ui::Graphics* g, const gfx::Rect& _rc)
if (m_docPref.show.slices())
drawSlices(g);
// Symmetry mode
if (isActive() && (m_flags & Editor::kShowSymmetryLine) &&
Preferences::instance().symmetryMode.enabled()) {
int mode = int(m_docPref.symmetry.mode());
if (mode & int(app::gen::SymmetryMode::HORIZONTAL)) {
double x = m_docPref.symmetry.xAxis();
if (x > 0) {
gfx::Color color = color_utils::color_for_ui(m_docPref.grid.color());
g->drawVLine(
color,
spriteRect.x + m_proj.applyX(mainTilePosition().x) + int(m_proj.applyX<double>(x)),
enclosingRect.y,
enclosingRect.h);
}
}
if (mode & int(app::gen::SymmetryMode::VERTICAL)) {
double y = m_docPref.symmetry.yAxis();
if (y > 0) {
gfx::Color color = color_utils::color_for_ui(m_docPref.grid.color());
g->drawHLine(
color,
enclosingRect.x,
spriteRect.y + m_proj.applyY(mainTilePosition().y) + int(m_proj.applyY<double>(y)),
enclosingRect.w);
}
}
if (mode & int(app::gen::SymmetryMode::RIGHT_DIAG)) {
double y = m_docPref.symmetry.yAxis();
double x = m_docPref.symmetry.xAxis();
gfx::Color color = color_utils::color_for_ui(m_docPref.grid.color());
// Bottom point intersection:
gfx::Point bottomLeft(
enclosingRect.x + m_proj.applyY(mainTilePosition().x) + int(m_proj.applyX<double>(x)) -
(enclosingRect.h - m_proj.applyY(mainTilePosition().y) - int(m_proj.applyY<double>(y))),
enclosingRect.y2());
if (bottomLeft.x < enclosingRect.x) {
// Left intersection
bottomLeft.y = enclosingRect.y2() - enclosingRect.x + bottomLeft.x;
bottomLeft.x = enclosingRect.x;
}
// Top intersection
gfx::Point topRight(enclosingRect.x + m_proj.applyY(mainTilePosition().x) +
int(m_proj.applyX<double>(x)) + m_proj.applyY(mainTilePosition().y) +
int(m_proj.applyY<double>(y)),
enclosingRect.y);
if (enclosingRect.x2() < topRight.x) {
// Right intersection
topRight.y = enclosingRect.y + topRight.x - enclosingRect.x2();
topRight.x = enclosingRect.x2();
}
g->drawLine(color, bottomLeft, topRight);
}
if (mode & int(app::gen::SymmetryMode::LEFT_DIAG)) {
double y = m_docPref.symmetry.yAxis();
double x = m_docPref.symmetry.xAxis();
gfx::Color color = color_utils::color_for_ui(m_docPref.grid.color());
// Bottom point intersection:
gfx::Point bottomRight(
enclosingRect.x + m_proj.applyY(mainTilePosition().x) + int(m_proj.applyX<double>(x)) +
(enclosingRect.h - m_proj.applyY(mainTilePosition().y) - int(m_proj.applyX<double>(y))),
enclosingRect.y2());
if (enclosingRect.x2() < bottomRight.x) {
// Left intersection
bottomRight.y = enclosingRect.y2() - bottomRight.x + enclosingRect.x2();
bottomRight.x = enclosingRect.x2();
}
// Top intersection
gfx::Point topLeft(enclosingRect.x + m_proj.applyY(mainTilePosition().x) +
int(m_proj.applyX<double>(x)) - m_proj.applyY(mainTilePosition().y) -
int(m_proj.applyY<double>(y)),
enclosingRect.y);
if (topLeft.x < enclosingRect.x) {
// Right intersection
topLeft.y = enclosingRect.y + enclosingRect.x - topLeft.x;
topLeft.x = enclosingRect.x;
}
g->drawLine(color, topLeft, bottomRight);
}
}
// Draw active layer/cel edges
if ((m_docPref.show.layerEdges() || m_showAutoCelGuides) &&
// Show layer edges and possibly cel guides only on states that

View File

@ -1,5 +1,5 @@
// Aseprite Render Library
// Copyright (c) 2020 Igara Studio S.A.
// Copyright (c) 2020-2025 Igara Studio S.A.
// Copyright (c) 2016 David Capello
//
// This file is released under the terms of the MIT license.
@ -70,6 +70,20 @@ public:
return T(m_zoom.removeCeiling(y)) / T(m_pixelRatio.h);
}
// Used in 'editor.cpp' to do some math between x,y values.
// Useful for calculating diagonal symmetry axis positions when pixel ratio is other than 1:1
template<typename T>
T turnXinTermsOfY(T x) const
{
return x * T(m_pixelRatio.h) / T(m_pixelRatio.w);
}
template<typename T>
T turnYinTermsOfX(T y) const
{
return y * T(m_pixelRatio.w) / T(m_pixelRatio.h);
}
gfx::Rect apply(const gfx::Rect& r) const
{
int u = applyX(r.x);