Compare commits

...

17 Commits

Author SHA1 Message Date
Martín Capello 14ded417cd
Merge 5d8a50d009 into 1b16cfbe71 2025-10-07 15:07:05 -03:00
David Capello 1b16cfbe71 Merge branch 'main' into beta
build-auto / build-auto (Debug, macos-latest) (push) Waiting to run Details
build-auto / build-auto (Debug, ubuntu-latest) (push) Waiting to run Details
build-auto / build-auto (Debug, windows-latest) (push) Waiting to run Details
build-auto / build-auto (RelWithDebInfo, macos-latest) (push) Waiting to run Details
build-auto / build-auto (RelWithDebInfo, ubuntu-latest) (push) Waiting to run Details
build-auto / build-auto (RelWithDebInfo, windows-latest) (push) Waiting to run Details
build / build (Debug, macos-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, macos-latest, noscripts, cli) (push) Waiting to run Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Waiting to run Details
build / build (Debug, windows-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, windows-latest, noscripts, cli) (push) Waiting to run Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Waiting to run Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Waiting to run Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Waiting to run Details
2025-10-07 08:06:10 -03:00
David Capello d3ef5c61da Update laf module 2025-10-07 08:05:18 -03:00
David Capello 107e846911 Remove direct TRACEARGS() calls
build / build (Debug, macos-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, macos-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, noscripts, cli) (push) Has been cancelled Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Has been cancelled Details
2025-10-01 16:53:07 -03:00
Martín Capello 5d8a50d009 Add corner radius field in context bar 2025-09-26 09:01:31 -03:00
Martín Capello 26cf154198 Make showing a slider for an IntEntry optional 2025-09-26 08:53:37 -03:00
Martín Capello 8c1fa31abd Use C instead of J as the corner radius modifier 2025-09-25 16:43:59 -03:00
Martín Capello bf5eb506ac Store corner radius by tool
Without this change the marquee tool and rectangle tool shared the
latest corner radius set. Now each one can have its own setting
2025-09-25 15:12:55 -03:00
Martín Capello a20847bef6 Fix Intertwine interface 2025-09-25 15:12:55 -03:00
Martín Capello be4dcb9476 Not display corner radius for non-rectangle tools 2025-09-25 15:12:55 -03:00
Martín Capello 7e8ccb55b8 Fix rounded rect drawing with max corner radius 2025-09-25 15:12:55 -03:00
Martín Capello 8ff57810a9 Move corner radius handling to another class 2025-09-25 15:12:55 -03:00
Martín Capello aa447c9802 Keep latest corner radius displayed in status bar 2025-09-25 15:12:55 -03:00
Martín Capello 6e0358073f Display visual corner radius in status bar
Before this change the corner radius displayed in the status bar was
just the latest corner radius set
2025-09-25 15:12:55 -03:00
Martín Capello abf43645da Allow corner radiuses of length 1 2025-09-25 15:12:55 -03:00
Martín Capello e082cbb549 Allow changing corner radius along both axis 2025-09-25 15:12:55 -03:00
Martín Capello 16aa3b4aa1 Add rounded rectangle support (fix #2184) 2025-09-25 15:12:55 -03:00
26 changed files with 544 additions and 57 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -393,6 +393,7 @@
<part id="icon_slice" x="248" y="264" w="8" h="8"/>
<part id="icon_aspect_ratio" x="256" y="264" w="10" h="8"/>
<part id="icon_delta" x="266" y="264" w="6" h="8"/>
<part id="icon_corner_radius" x="272" y="264" w="8" h="8"/>
<part id="icon_add" x="184" y="200" w="5" h="5"/>
<part id="tool_rectangular_marquee" x="144" y="0" w="16" h="16"/>
<part id="tool_elliptical_marquee" x="160" y="0" w="16" h="16"/>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -389,6 +389,7 @@
<part id="icon_slice" x="248" y="264" w="8" h="8"/>
<part id="icon_aspect_ratio" x="256" y="264" w="10" h="8"/>
<part id="icon_delta" x="266" y="264" w="6" h="8"/>
<part id="icon_corner_radius" x="272" y="264" w="8" h="8"/>
<part id="icon_add" x="184" y="200" w="5" h="5"/>
<part id="tool_rectangular_marquee" x="144" y="0" w="16" h="16"/>
<part id="tool_elliptical_marquee" x="160" y="0" w="16" h="16"/>

View File

@ -678,6 +678,8 @@
<!-- Modifiers for two-points tool controller -->
<key action="SquareAspect" shortcut="Shift" />
<key action="DrawFromCenter" shortcut="Ctrl" />
<key action="CornerRadius" shortcut="C" />
<!-- Modifiers for two-or-more-points tools -->
<key action="MoveOrigin" shortcut="Space" />
<key action="RotateShape" shortcut="Alt" />

View File

@ -481,6 +481,7 @@
<option id="filled_preview" type="bool" default="false" />
<option id="ink" type="app::tools::InkType" default="app::tools::InkType::DEFAULT" />
<option id="freehand_algorithm" type="app::tools::FreehandAlgorithm" default="app::tools::FreehandAlgorithm::DEFAULT" />
<option id="corner_radius" type="int" default="0" />
<!-- Update app::Preferences::resetToolPreferences() function if you add new sections here -->
<section id="brush">
<option id="type" type="BrushType" default="BrushType::CIRCLE" />

View File

@ -601,6 +601,7 @@ discard_brush = Discard Brush (Esc)
brush_type = Brush Type
brush_size = Brush Size (in pixels)
brush_angle = Brush Angle (in degrees)
corner_radius = Corner Radius (in pixels)
ink = Ink
opacity = Opacity (paint intensity)
shades = Shades
@ -1006,6 +1007,7 @@ move_origin = Move Origin
square_aspect = Square Aspect
draw_from_center = Draw From Center
rotate_shape = Rotate Shape
corner_radius = Corner Radius
trigger_left_mouse_button = Trigger Left Mouse Button
trigger_right_mouse_button = Trigger Right Mouse Button
ok = &OK

2
laf

@ -1 +1 @@
Subproject commit 39706c11063fb53cf4c8e865102c6f71e2606906
Subproject commit 29aa044517059df87158c9e5f26c92572effb103

View File

@ -34,7 +34,6 @@ namespace app { namespace script {
template<>
void push_value_to_lua(lua_State* L, const std::nullptr_t&)
{
TRACEARGS("push_value_to_lua nullptr_t");
lua_pushnil(L);
}

View File

@ -62,6 +62,10 @@ public:
// Returns the angle for a shape-like intertwiner (rectangles,
// ellipses, etc.).
virtual double getShapeAngle() const { return 0.0; }
// Returns the radius for each corner for a rectangle intertwiner when drawing
// rounded rectangles.
virtual int getCornerRadius() const { return 0; }
};
}} // namespace app::tools

View File

@ -1,11 +1,18 @@
// Aseprite
// Copyright (C) 2019-2023 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#include "app/app.h"
#include "app/pref/preferences.h"
#include "app/snap_to_grid.h"
#include "app/tools/controller.h"
#include "app/tools/intertwine.h"
#include "app/tools/tool.h"
#include "app/tools/tool_loop.h"
#include "app/tools/tool_loop_modifiers.h"
#include "base/gcd.h"
#include "base/pi.h"
#include "fmt/format.h"
@ -105,6 +112,72 @@ private:
Stroke::Pt m_last;
};
class CornerRadius {
public:
ToolLoop* loop() const { return m_loop; }
void loop(ToolLoop* loop) { m_loop = loop; }
void load()
{
auto* tool = App::instance()->activeTool();
m_pref = &Preferences::instance().tool(tool);
m_radius = m_pref->cornerRadius();
}
void save() { m_pref->cornerRadius.setValue(m_radius); }
bool isModifying() const { return m_modifying; }
void modifyRadius(Stroke& stroke, const Stroke::Pt& pt)
{
if (!m_modifying) {
m_lastRadius = std::min(m_radius, maxRadius(stroke));
m_modifying = true;
}
int dx = stroke[1].x - pt.x;
int dy = stroke[1].y - pt.y;
if (stroke[1].y < stroke[0].y)
dy = -dy;
if (stroke[1].x < stroke[0].x)
dx = -dx;
m_radius = std::max(0, m_lastRadius + dx + dy);
capRadius(stroke);
}
void stopModifying()
{
m_modifying = false;
m_lastRadius = m_radius;
}
bool hasRadius() const { return m_radius > 0; }
int radius() const { return m_radius; }
// Gets the corner radius limited by the maximum radius allowed by the stroke
// points.
int radius(const Stroke& stroke) const { return std::min(m_radius, maxRadius(stroke)); }
void radius(int r) { m_radius = r; }
void capRadius(Stroke& stroke) { m_radius = radius(stroke); }
private:
static int maxRadius(const Stroke& stroke)
{
return std::min(ABS(stroke[1].x - stroke[0].x + 1), ABS(stroke[1].y - stroke[0].y + 1)) / 2;
}
ToolLoop* m_loop;
ToolPreferences* m_pref;
bool m_modifying = false;
int m_lastRadius = 0;
int m_radius = 0;
};
// Controls clicks for tools like line
class TwoPointsController : public MoveOriginCapability {
public:
@ -117,6 +190,11 @@ public:
m_first = m_center = pt;
m_angle = 0.0;
m_cornerRadius.loop(loop);
if (loop->getIntertwine()->cornerRadiusSupport()) {
m_cornerRadius.load();
}
stroke.addPoint(pt);
stroke.addPoint(pt);
@ -124,7 +202,14 @@ public:
snapPointsToGridTiles(loop, stroke);
}
bool releaseButton(Stroke& stroke, const Stroke::Pt& pt) override { return false; }
bool releaseButton(Stroke& stroke, const Stroke::Pt& pt) override
{
if (m_cornerRadius.loop() && m_cornerRadius.loop()->getIntertwine()->cornerRadiusSupport()) {
m_cornerRadius.capRadius(stroke);
m_cornerRadius.save();
}
return false;
}
void movement(ToolLoop* loop, Stroke& stroke, const Stroke::Pt& pt) override
{
@ -135,6 +220,18 @@ public:
if (MoveOriginCapability::isMovingOrigin(loop, stroke, pt))
return;
if (loop->getIntertwine()->cornerRadiusSupport() &&
(int(loop->getModifiers()) & int(ToolLoopModifiers::kCornerRadius))) {
m_cornerRadius.modifyRadius(stroke, pt);
m_cornerRadius.save();
return;
}
if (m_cornerRadius.isModifying()) {
m_cornerRadius.stopModifying();
return;
}
if (!loop->getIntertwine()->snapByAngle() &&
int(loop->getModifiers()) & int(ToolLoopModifiers::kRotateShape)) {
if ((int(loop->getModifiers()) & int(ToolLoopModifiers::kFromCenter))) {
@ -277,11 +374,15 @@ public:
text += fmt::format(" :angle: {:.1f}", 180.0 * angle / PI);
}
if (m_cornerRadius.hasRadius() && loop->getIntertwine()->cornerRadiusSupport())
text += fmt::format(" :corner_radius: {}", m_cornerRadius.radius(stroke));
// Aspect ratio at the end
text += fmt::format(" :aspect_ratio: {}:{}", w / gcd, h / gcd);
}
double getShapeAngle() const override { return m_angle; }
int getCornerRadius() const override { return m_cornerRadius.radius(); }
private:
void snapPointsToGridTiles(ToolLoop* loop, Stroke& stroke)
@ -312,6 +413,7 @@ private:
Stroke::Pt m_first;
Stroke::Pt m_center;
double m_angle;
CornerRadius m_cornerRadius;
};
// Controls clicks for tools like polygon

View File

@ -18,6 +18,7 @@
#include "app/tools/tool_loop.h"
#include "base/pi.h"
#include "doc/algo.h"
#include "doc/algorithm/hline.h"
#include "doc/layer.h"
#include <cmath>

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2021 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -26,6 +26,8 @@ public:
virtual ~Intertwine() {}
virtual bool snapByAngle() { return false; }
virtual void prepareIntertwine(ToolLoop* loop) {}
// Returns true if the implementation supports corner radius modification.
virtual bool cornerRadiusSupport() { return false; }
// The given stroke must be relative to the cel origin.
virtual void joinStroke(ToolLoop* loop, const Stroke& stroke) = 0;

View File

@ -5,8 +5,19 @@
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#include "app/tools/controller.h"
#include "app/tools/intertwine.h"
#include "app/tools/point_shape.h"
#include "app/tools/tool_loop.h"
#include "app/tools/tool_loop_modifiers.h"
#include "base/pi.h"
#include "doc/algo.h"
#include "doc/algorithm/polygon.h"
#include "doc/layer_tilemap.h"
#include "gfx/point.h"
#include <algorithm>
using namespace gfx;
namespace app { namespace tools {
@ -160,6 +171,8 @@ public:
class IntertwineAsRectangles : public Intertwine {
public:
bool cornerRadiusSupport() override { return true; }
void joinStroke(ToolLoop* loop, const Stroke& stroke) override
{
if (stroke.size() == 0)
@ -183,22 +196,62 @@ public:
std::swap(y1, y2);
const double angle = loop->getController()->getShapeAngle();
const int cornerRadius = loop->getController()->getCornerRadius();
if (ABS(angle) < 0.001) {
doPointshapeLineWithoutDynamics(x1, y1, x2, y1, loop);
doPointshapeLineWithoutDynamics(x1, y2, x2, y2, loop);
int r = 0;
if (cornerRadius > 0) {
int w = x2 - x1 + 1;
int h = y2 - y1 + 1;
r = std::min(w, std::min(h, 2 * cornerRadius)) / 2;
algo_sliced_circle(x1, y1, x2, y2, r, loop, (AlgoPixel)doPointshapePoint);
}
for (y = y1; y <= y2; y++) {
doPointshapeLineWithoutDynamics(x1 + r, y1, x2 - r, y1, loop);
doPointshapeLineWithoutDynamics(x1 + r, y2, x2 - r, y2, loop);
for (y = y1 + r; y <= y2 - r; y++) {
doPointshapePoint(x1, y, loop);
doPointshapePoint(x2, y, loop);
}
}
else {
Stroke p = rotateRectangle(x1, y1, x2, y2, angle);
int n = p.size();
for (int i = 0; i + 1 < n; ++i) {
doPointshapeLine(p[i], p[i + 1], loop);
if (cornerRadius <= 0) {
Stroke p = rotateRectangle(x1, y1, x2, y2, angle);
int n = p.size();
for (int i = 0; i + 1 < n; ++i) {
doPointshapeLine(p[i], p[i + 1], loop);
}
doPointshapeLine(p[n - 1], p[0], loop);
}
else {
int w = x2 - x1 + 1;
int h = y2 - y1 + 1;
int r = std::min(w, std::min(h, 2 * cornerRadius)) / 2;
Stroke p = rotateRectangle(x1, y1, x2, y2, angle, r);
int n = p.size();
for (int i = 0; i + 1 < n; i += 3) {
doPointshapeLine(p[i], p[i + 1], loop);
}
const double ang_minus_PI_2 = base::fmod_radians(angle - PI / 2);
const double ang_plus_PI_2 = base::fmod_radians(angle + PI / 2);
const double ang_plus_PI = base::fmod_radians(angle + PI);
algo_arc(p[2].x, p[2].y, ang_minus_PI_2, angle, r, loop, (AlgoPixel)doPointshapePoint);
algo_arc(p[5].x, p[5].y, angle, ang_plus_PI_2, r, loop, (AlgoPixel)doPointshapePoint);
algo_arc(p[8].x,
p[8].y,
ang_plus_PI_2,
ang_plus_PI,
r,
loop,
(AlgoPixel)doPointshapePoint);
algo_arc(p[11].x,
p[11].y,
ang_plus_PI,
ang_minus_PI_2,
r,
loop,
(AlgoPixel)doPointshapePoint);
}
doPointshapeLine(p[n - 1], p[0], loop);
}
}
}
@ -224,14 +277,66 @@ public:
std::swap(y1, y2);
const double angle = loop->getController()->getShapeAngle();
const int cornerRadius = loop->getController()->getCornerRadius();
if (ABS(angle) < 0.001) {
for (y = y1; y <= y2; y++)
int r = 0;
if (cornerRadius > 0) {
int w = x2 - x1 + 1;
int h = y2 - y1 + 1;
r = std::min(w, std::min(h, 2 * cornerRadius)) / 2;
algo_sliced_circlefill(x1, y1, x2, y2, r, loop, (AlgoHLine)doPointshapeHline);
for (y = y1; y < y1 + r; y++)
doPointshapeLineWithoutDynamics(x1 + r, y, x2 - r, y, loop);
for (y = y2 - r + 1; y <= y2; y++)
doPointshapeLineWithoutDynamics(x1 + r, y, x2 - r, y, loop);
}
for (y = y1 + r; y <= y2 - r; y++)
doPointshapeLineWithoutDynamics(x1, y, x2, y, loop);
}
else {
Stroke p = rotateRectangle(x1, y1, x2, y2, angle);
auto v = p.toXYInts();
doc::algorithm::polygon(v.size() / 2, &v[0], loop, (AlgoHLine)doPointshapeHline);
if (cornerRadius <= 0) {
Stroke p = rotateRectangle(x1, y1, x2, y2, angle);
auto v = p.toXYInts();
doc::algorithm::polygon(v.size() / 2, &v[0], loop, (AlgoHLine)doPointshapeHline);
}
else {
int w = x2 - x1 + 1;
int h = y2 - y1 + 1;
int r = std::min(w, std::min(h, 2 * cornerRadius)) / 2;
Stroke p = rotateRectangle(x1, y1, x2, y2, angle, cornerRadius);
auto v = p.toXYInts();
doc::algorithm::polygon(v.size() / 2, &v[0], loop, (AlgoHLine)doPointshapeHline);
algo_sliced_circlefill(p[2].x - r,
p[2].y - r,
p[2].x + r,
p[2].y + r,
r,
loop,
(AlgoHLine)doPointshapeHline);
algo_sliced_circlefill(p[5].x - r,
p[5].y - r,
p[5].x + r,
p[5].y + r,
r,
loop,
(AlgoHLine)doPointshapeHline);
algo_sliced_circlefill(p[8].x - r,
p[8].y - r,
p[8].x + r,
p[8].y + r,
r,
loop,
(AlgoHLine)doPointshapeHline);
algo_sliced_circlefill(p[11].x - r,
p[11].y - r,
p[11].x + r,
p[11].y + r,
r,
loop,
(AlgoHLine)doPointshapeHline);
}
}
}
}
@ -274,6 +379,54 @@ private:
stroke.addPoint(Point(cx - a * c + b * s, cy + a * s + b * c));
return stroke;
}
// Returns a stroke with the rotated points of a rectangle making room for a
// rounded corner of the specified radius, and with points where the center of
// each corner must be.
static Stroke rotateRectangle(int x1, int y1, int x2, int y2, double angle, int cornerRadius)
{
cornerRadius = std::max(cornerRadius, 0);
int cx = (x1 + x2) / 2;
int cy = (y1 + y2) / 2;
int a = ((x2 - x1) / 2);
int b = ((y2 - y1) / 2);
int ai = a - cornerRadius;
int bi = b - cornerRadius;
double s = -std::sin(angle);
double c = std::cos(angle);
Stroke stroke;
// Top segment
stroke.addPoint(Point(cx - ai * c - b * s, cy + ai * s - b * c));
stroke.addPoint(Point(cx + ai * c - b * s, cy - ai * s - b * c));
// Center for top-right corner
stroke.addPoint(Point(cx + ai * c - bi * s, cy - ai * s - bi * c));
// Right segment
stroke.addPoint(Point(cx + a * c - bi * s, cy - a * s - bi * c));
stroke.addPoint(Point(cx + a * c + bi * s, cy - a * s + bi * c));
// Center for bottom-right corner
stroke.addPoint(Point(cx + ai * c + bi * s, cy - ai * s + bi * c));
// Bottom segment
stroke.addPoint(Point(cx + ai * c + b * s, cy - ai * s + b * c));
stroke.addPoint(Point(cx - ai * c + b * s, cy + ai * s + b * c));
// Center for bottom-left corner
stroke.addPoint(Point(cx - ai * c + bi * s, cy + ai * s + bi * c));
// Left segment
stroke.addPoint(Point(cx - a * c + bi * s, cy + a * s + bi * c));
stroke.addPoint(Point(cx - a * c - bi * s, cy + a * s - bi * c));
// Center for top-left corner
stroke.addPoint(Point(cx - ai * c - bi * s, cy + ai * s - bi * c));
return stroke;
}
};
class IntertwineAsEllipses : public Intertwine {

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018 Igara Studio S.A.
// Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2016-2018 David Capello
//
// This program is distributed under the terms of
@ -21,6 +21,7 @@ enum class ToolLoopModifiers {
kSquareAspect = 0x00000020,
kFromCenter = 0x00000040,
kRotateShape = 0x00000080,
kCornerRadius = 0x00000100,
};
}} // namespace app::tools

View File

@ -30,6 +30,7 @@
#include "app/tools/controller.h"
#include "app/tools/ink.h"
#include "app/tools/ink_type.h"
#include "app/tools/intertwine.h"
#include "app/tools/point_shape.h"
#include "app/tools/tool.h"
#include "app/tools/tool_box.h"
@ -349,6 +350,24 @@ protected:
bool m_lock;
};
class ContextBar::CornerRadiusField : public IntEntry {
public:
CornerRadiusField() : IntEntry(0, 999) { setSuffix("px"); }
private:
void onValueChange() override
{
if (g_updatingFromCode)
return;
IntEntry::onValueChange();
base::ScopedValue lockFlag(g_updatingFromCode, true);
Tool* tool = App::instance()->activeTool();
Preferences::instance().tool(tool).cornerRadius.setValue(getValue());
}
};
class ContextBar::ToleranceField : public IntEntry {
public:
ToleranceField() : IntEntry(0, 255) {}
@ -1919,6 +1938,8 @@ ContextBar::ContextBar(TooltipManager* tooltipManager, ColorBar* colorBar)
addChild(m_brushBack = new BrushBackField);
addChild(m_brushType = new BrushTypeField(this));
addChild(m_brushSize = new BrushSizeField());
addChild(m_cornerRadius = new CornerRadiusField());
m_cornerRadius->useSlider(false);
addChild(m_brushAngle = new BrushAngleField(m_brushType));
addChild(m_brushPatternField = new BrushPatternField());
@ -2074,6 +2095,11 @@ void ContextBar::onBrushSizeChange()
updateForActiveTool();
}
void ContextBar::onCornerRadiusChange(int value)
{
m_cornerRadius->setValue(value);
}
void ContextBar::onBrushAngleChange()
{
if (m_activeBrush->type() != kImageBrushType)
@ -2164,6 +2190,9 @@ void ContextBar::updateForTool(tools::Tool* tool)
m_freehandAlgoConn = toolPref->freehandAlgorithm.AfterChange.connect(
[this] { onToolSetFreehandAlgorithm(); });
m_contiguousConn = toolPref->contiguous.AfterChange.connect([this] { onToolSetContiguous(); });
m_cornerRadius->setValue(toolPref->cornerRadius());
m_cornerRadiusConn = toolPref->cornerRadius.AfterChange.connect(
[this](const int value) { onCornerRadiusChange(value); });
}
if (tool)
@ -2241,6 +2270,9 @@ void ContextBar::updateForTool(tools::Tool* tool)
const bool isFloodfill = tool && (tool->getPointShape(0)->isFloodFill() ||
tool->getPointShape(1)->isFloodFill());
const bool hasCornerRadius = tool && (tool->getIntertwine(0)->cornerRadiusSupport() ||
tool->getIntertwine(1)->cornerRadiusSupport());
// True if the current tool needs tolerance options
const bool hasTolerance = tool && (tool->getPointShape(0)->isFloodFill() ||
tool->getPointShape(1)->isFloodFill());
@ -2276,6 +2308,7 @@ void ContextBar::updateForTool(tools::Tool* tool)
m_brushSize->setVisible(supportOpacity && !isFloodfill && !hasImageBrush);
m_brushAngle->setVisible(supportOpacity && !isFloodfill && !hasImageBrush && hasBrushWithAngle);
m_brushPatternField->setVisible(supportOpacity && hasImageBrush && !withDithering);
m_cornerRadius->setVisible(hasCornerRadius);
m_inkType->setVisible(hasInk);
m_inkOpacityLabel->setVisible(showOpacity);
m_inkOpacity->setVisible(showOpacity);
@ -2637,6 +2670,7 @@ void ContextBar::setupTooltips(TooltipManager* tooltipManager)
tooltipManager->addTooltipFor(m_brushType->at(0), Strings::context_bar_brush_type(), BOTTOM);
tooltipManager->addTooltipFor(m_brushSize, Strings::context_bar_brush_size(), BOTTOM);
tooltipManager->addTooltipFor(m_brushAngle, Strings::context_bar_brush_angle(), BOTTOM);
tooltipManager->addTooltipFor(m_cornerRadius, Strings::context_bar_corner_radius(), BOTTOM);
tooltipManager->addTooltipFor(m_inkType->at(0), Strings::context_bar_ink(), BOTTOM);
tooltipManager->addTooltipFor(m_inkOpacity, Strings::context_bar_opacity(), BOTTOM);
tooltipManager->addTooltipFor(m_inkShades->at(0), Strings::context_bar_shades(), BOTTOM);

View File

@ -131,6 +131,7 @@ protected:
private:
void onBrushSizeChange();
void onBrushAngleChange();
void onCornerRadiusChange(int value);
void onSymmetryModeChange();
void onFgOrBgColorChange(doc::Brush::ImageColor imageColor);
void onOpacityRangeChange();
@ -168,6 +169,7 @@ private:
class DynamicsField;
class FreehandAlgorithmField;
class BrushPatternField;
class CornerRadiusField;
class EyedropperField;
class DropPixelsField;
class AutoSelectLayerField;
@ -181,6 +183,7 @@ private:
BrushTypeField* m_brushType;
BrushAngleField* m_brushAngle;
BrushSizeField* m_brushSize;
CornerRadiusField* m_cornerRadius;
ui::Label* m_toleranceLabel;
ToleranceField* m_tolerance;
ContiguousField* m_contiguous;
@ -224,6 +227,7 @@ private:
obs::scoped_connection m_opacityConn;
obs::scoped_connection m_freehandAlgoConn;
obs::scoped_connection m_contiguousConn;
obs::scoped_connection m_cornerRadiusConn;
};
} // namespace app

View File

@ -1834,7 +1834,7 @@ void Editor::updateToolLoopModifiersIndicators(const bool firstFromMouseDown)
// square-aspect/rotation/etc. only when the user presses the
// modifier key again in the ToolLoop (and not before starting
// the loop). So Alt+selection will add a selection, but
// willn't start the square-aspect until we press Alt key
// won't start the square-aspect until we press Alt key
// again, or Alt+Shift+selection tool will subtract the
// selection but will not start the rotation until we release
// and press the Alt key again.
@ -1847,6 +1847,8 @@ void Editor::updateToolLoopModifiersIndicators(const bool firstFromMouseDown)
modifiers |= int(tools::ToolLoopModifiers::kFromCenter);
if (int(action & KeyAction::RotateShape))
modifiers |= int(tools::ToolLoopModifiers::kRotateShape);
if (int(action & KeyAction::CornerRadius))
modifiers |= int(tools::ToolLoopModifiers::kCornerRadius);
}
}

View File

@ -110,6 +110,10 @@ const std::vector<KeyShortcutAction>& actions()
I18N_KEY(rotate_shape),
app::KeyAction::RotateShape,
app::KeyContext::ShapeTool },
{ "CornerRadius",
I18N_KEY(corner_radius),
app::KeyAction::CornerRadius,
app::KeyContext::ShapeTool },
{ "LeftMouseButton",
I18N_KEY(trigger_left_mouse_button),
app::KeyAction::LeftMouseButton,
@ -392,6 +396,7 @@ Key::Key(const KeyAction action, const KeyContext keyContext)
case KeyAction::RotateShape: m_keycontext = KeyContext::ShapeTool; break;
case KeyAction::LeftMouseButton:
case KeyAction::RightMouseButton: m_keycontext = KeyContext::Any; break;
case KeyAction::CornerRadius: m_keycontext = KeyContext::ShapeTool; break;
}
}

View File

@ -64,6 +64,7 @@ enum class KeyAction {
AngleSnapFromLastPoint = 0x00010000,
RotateShape = 0x00020000,
FineControl = 0x00040000,
CornerRadius = 0x00080000,
};
enum class WheelAction {

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2023 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
@ -50,10 +50,8 @@ TaskWidget::TaskWidget(const Type type, base::task::func_t&& func)
}
else if (m_progressBar.parent()) {
float v = m_task.progress();
if (v > 0.0f) {
TRACEARGS("progressBar setValue", int(std::clamp(v * 100.0f, 0.0f, 100.0f)));
if (v > 0.0f)
m_progressBar.setValue(int(std::clamp(v * 100.0f, 0.0f, 100.0f)));
}
}
});
m_monitorTimer.start();

View File

@ -1,5 +1,5 @@
// Aseprite Document Library
// Copyright (c) 2018-2022 Igara Studio S.A.
// Copyright (c) 2018-2025 Igara Studio S.A.
// Copyright (c) 2001-2018 David Capello
//
// This file is released under the terms of the MIT license.
@ -12,6 +12,7 @@
#include "doc/algo.h"
#include "base/debug.h"
#include "base/pi.h"
#include <algorithm>
#include <cmath>
@ -180,6 +181,167 @@ void algo_line_continuous_with_fix_for_line_brush(int x0,
}
}
// Circle code based on Alois Zingl work released under the MIT
// license http://members.chello.at/easyfilter/bresenham.html
//
// Adapted for Aseprite by Igara Studio S.A.
//
// Draws a circle of the specified radius divided in 4 slices, adjusting each
// slice inside the specified rectangle.
// |--r --|
//
// x1,y1 --> * OOO OOO
// O O
// O O
//
//
//
// T O O
// r | O O
// _ OOO OOO * <-- x2,y2
//
// If the rectangle is smaller than the circle, it doesn't make any clipping.
void algo_sliced_circle(int x1, int y1, int x2, int y2, int r, void* data, AlgoPixel proc)
{
int x = -r, y = 0, err = 2 - 2 * r; /* II. Quadrant */
const int r0 = r;
do {
proc(x2 - r0 - x, y2 - r0 + y, data); /* I. Quadrant */
proc(x1 + r0 - y, y2 - r0 - x, data); /* II. Quadrant */
proc(x1 + r0 + x, y1 + r0 - y, data); /* III. Quadrant */
proc(x2 - r0 + y, y1 + r0 + x, data); /* IV. Quadrant */
r = err;
if (r <= y)
err += ++y * 2 + 1; /* e_xy+e_y < 0 */
if (r > x || err > y)
err += ++x * 2 + 1; /* e_xy+e_x > 0 or no 2nd y-step */
} while (x < 0);
}
// Same as algo_sliced_circle but with the parts filled.
void algo_sliced_circlefill(int x1, int y1, int x2, int y2, int r, void* data, AlgoHLine proc)
{
int x = -r, y = 0, err = 2 - 2 * r; /* II. Quadrant */
const int r0 = r;
do {
proc(x2 - r0, y2 - r0 + y, x2 - r0 - x, data); /* I. Quadrant */
proc(x1 + r0 - y, y2 - r0 - x, x1 + r0, data); /* II. Quadrant */
proc(x1 + r0 + x, y1 + r0 - y, x1 + r0, data); /* III. Quadrant */
proc(x2 - r0, y1 + r0 + x, x2 - r0 + y, data); /* IV. Quadrant */
r = err;
if (r <= y)
err += ++y * 2 + 1; /* e_xy+e_y < 0 */
if (r > x || err > y)
err += ++x * 2 + 1; /* e_xy+e_x > 0 or no 2nd y-step */
} while (x < 0);
}
void algo_arc(int xm, int ym, double sa, double ea, int r, void* data, AlgoPixel proc)
{
int sx = std::cos(sa) * r;
int ex = std::cos(ea) * r;
int startQuadrant;
if (sa <= 0 && sa > -PI / 2) {
startQuadrant = 4;
}
else if (sa <= -PI / 2 && sa >= -PI) {
startQuadrant = 3;
}
else if (sa > 0 && sa < PI / 2) {
startQuadrant = 1;
}
else {
startQuadrant = 2;
}
int endQuadrant;
if (ea <= 0 && ea > -PI / 2) {
endQuadrant = 4;
}
else if (ea <= -PI / 2 && ea >= -PI) {
endQuadrant = 3;
}
else if (ea > 0 && ea < PI / 2) {
endQuadrant = 1;
}
else {
endQuadrant = 2;
}
// If start angle and end angle falls in the same quadrant we have to determine
// if we have to include the other quadrants or not since the arc is determined
// from start angle to end angle in clockwise direction.
bool includeQuadrant[4] = { false, false, false, false };
if (startQuadrant == endQuadrant) {
// If start angle is greater than end angle, include all quadrants for drawing
if (sa > ea) {
includeQuadrant[0] = true;
includeQuadrant[1] = true;
includeQuadrant[2] = true;
includeQuadrant[3] = true;
}
else {
// start angle is less to or equal to end angle then only include one quadrant
// for drawing.
includeQuadrant[startQuadrant - 1] = true;
}
}
else {
for (int i = startQuadrant - 1; i < startQuadrant - 1 + 4; ++i) {
int q = i % 4;
includeQuadrant[q] = true;
if (q == endQuadrant - 1)
break;
}
}
int x = -r, y = 0, err = 2 - 2 * r; /* II. Quadrant */
do {
if (includeQuadrant[0]) {
if ((startQuadrant != 1 && endQuadrant != 1) ||
(startQuadrant == 1 && endQuadrant != 1 && -x <= sx) ||
(startQuadrant != 1 && endQuadrant == 1 && -x >= ex) ||
(startQuadrant == 1 && endQuadrant == 1 &&
((sa <= ea && -x <= sx && -x >= ex) || (sa > ea && (-x <= sx || -x >= ex)))))
proc(xm - x, ym + y, data); /* I. Quadrant */
}
if (includeQuadrant[1]) {
if ((startQuadrant != 2 && endQuadrant != 2) ||
(startQuadrant == 2 && endQuadrant != 2 && -y <= sx) ||
(startQuadrant != 2 && endQuadrant == 2 && -y >= ex) ||
(startQuadrant == 2 && endQuadrant == 2 &&
((sa <= ea && -y <= sx && -y >= ex) || (sa > ea && (-y <= sx || -y >= ex)))))
proc(xm - y, ym - x, data); /* II. Quadrant */
}
if (includeQuadrant[2]) {
if ((startQuadrant != 3 && endQuadrant != 3) ||
(startQuadrant == 3 && endQuadrant != 3 && x >= sx) ||
(startQuadrant != 3 && endQuadrant == 3 && x <= ex) ||
(startQuadrant == 3 && endQuadrant == 3 &&
((sa <= ea && -x <= -sx && -x >= -ex) || (sa > ea && (-x <= -sx || -x >= -ex)))))
proc(xm + x, ym - y, data); /* III. Quadrant */
}
if (includeQuadrant[3]) {
if ((startQuadrant != 4 && endQuadrant != 4) ||
(startQuadrant == 4 && endQuadrant != 4 && y >= sx) ||
(startQuadrant != 4 && endQuadrant == 4 && y <= ex) ||
(startQuadrant == 4 && endQuadrant == 4 &&
((sa <= ea && y >= sx && y <= ex) || (sa > ea && (y >= sx || y <= ex)))))
proc(xm + y, ym + x, data); /* IV. Quadrant */
}
r = err;
if (r <= y)
err += ++y * 2 + 1; /* e_xy+e_y < 0 */
if (r > x || err > y)
err += ++x * 2 + 1; /* e_xy+e_x > 0 or no 2nd y-step */
} while (x < 0);
}
static int adjust_ellipse_args(int& x0, int& y0, int& x1, int& y1, int& hPixels, int& vPixels)
{
// hPixels : straight horizontal pixels added to mid region of the ellipse.

View File

@ -1,5 +1,5 @@
// Aseprite Document Library
// Copyright (C) 2018-2021 Igara Studio S.A.
// Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (c) 2001-2018 David Capello
//
// This file is released under the terms of the MIT license.
@ -48,6 +48,12 @@ void algo_line_continuous_with_fix_for_line_brush(int x1,
void* data,
AlgoPixel proc);
void algo_sliced_circle(int x1, int y1, int x2, int y2, int r, void* data, AlgoPixel proc);
void algo_sliced_circlefill(int x1, int y1, int x2, int y2, int r, void* data, AlgoHLine proc);
void algo_arc(int xm, int ym, double sa, double ea, int r, void* data, AlgoPixel proc);
void algo_ellipse(int x1,
int y1,
int x2,

View File

@ -1,5 +1,5 @@
// Aseprite Steam Wrapper
// Copyright (c) 2020-2024 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.
@ -153,8 +153,6 @@ public:
CallbackMsg_t msg;
if (SteamAPI_ManualDispatch_GetNextCallback(m_pipe, &msg)) {
// TRACEARGS("SteamAPI_ManualDispatch_GetNextCallback", msg.callback);
bool disconnected = false;
switch (msg.callback) {
case kSteamServersDisconnected:

View File

@ -175,43 +175,45 @@ void IntEntry::openPopup()
{
m_slider->setValue(getValue());
// We weren't able to reproduce it, but there are crash reports
// where this openPopup() function is called and the popup is still
// alive, with the slider inside (we have to remove it before
// resetting m_popupWindow pointer to avoid deleting the slider
// pointer).
removeSlider();
if (m_useSlider) {
// We weren't able to reproduce it, but there are crash reports
// where this openPopup() function is called and the popup is still
// alive, with the slider inside (we have to remove it before
// resetting m_popupWindow pointer to avoid deleting the slider
// pointer).
removeSlider();
m_popupWindow = std::make_unique<TransparentPopupWindow>(
PopupWindow::ClickBehavior::CloseOnClickInOtherWindow);
m_popupWindow->setAutoRemap(false);
m_popupWindow->addChild(m_slider.get());
m_popupWindow->Close.connect(&IntEntry::onPopupClose, this);
m_popupWindow = std::make_unique<TransparentPopupWindow>(
PopupWindow::ClickBehavior::CloseOnClickInOtherWindow);
m_popupWindow->setAutoRemap(false);
m_popupWindow->addChild(m_slider.get());
m_popupWindow->Close.connect(&IntEntry::onPopupClose, this);
fit_bounds(display(),
m_popupWindow.get(),
gfx::Rect(0, 0, 128 * guiscale(), m_popupWindow->sizeHint().h),
[this](const gfx::Rect& workarea,
gfx::Rect& rc,
std::function<gfx::Rect(Widget*)> getWidgetBounds) {
Rect entryBounds = getWidgetBounds(this);
fit_bounds(display(),
m_popupWindow.get(),
gfx::Rect(0, 0, 128 * guiscale(), m_popupWindow->sizeHint().h),
[this](const gfx::Rect& workarea,
gfx::Rect& rc,
std::function<gfx::Rect(Widget*)> getWidgetBounds) {
Rect entryBounds = getWidgetBounds(this);
rc.x = entryBounds.x;
rc.y = entryBounds.y2();
rc.x = entryBounds.x;
rc.y = entryBounds.y2();
if (rc.x2() > workarea.x2())
rc.x = rc.x - rc.w + entryBounds.w;
if (rc.x2() > workarea.x2())
rc.x = rc.x - rc.w + entryBounds.w;
if (rc.y2() > workarea.y2())
rc.y = entryBounds.y - entryBounds.h;
if (rc.y2() > workarea.y2())
rc.y = entryBounds.y - entryBounds.h;
m_popupWindow->setBounds(rc);
});
m_popupWindow->setBounds(rc);
});
Region rgn(m_popupWindow->boundsOnScreen().createUnion(boundsOnScreen()));
m_popupWindow->setHotRegion(rgn);
Region rgn(m_popupWindow->boundsOnScreen().createUnion(boundsOnScreen()));
m_popupWindow->setHotRegion(rgn);
m_popupWindow->openWindow();
m_popupWindow->openWindow();
}
}
void IntEntry::closePopup()

View File

@ -27,6 +27,10 @@ public:
virtual int getValue() const;
virtual void setValue(int value);
// If useSlider is false, then it won't show the slider popup to change its
// value.
void useSlider(bool useSlider) { m_useSlider = useSlider; }
protected:
bool onProcessMessage(Message* msg) override;
void onInitTheme(InitThemeEvent& ev) override;
@ -42,6 +46,8 @@ protected:
int m_max;
std::unique_ptr<PopupWindow> m_popupWindow;
bool m_changeFromSlider;
// If true a slider can be used to modify the value.
bool m_useSlider = true;
std::unique_ptr<Slider> m_slider;
private: