Pencil2D Animation
Download Community News Docs Contribute
  • Overview
  • Articles
  • Code
  •  
  • Class List
  • Class Index
  • Class Hierarchy
  • Class Members
  • File List
Loading...
Searching...
No Matches
  • core_lib
  • src
  • tool
stroketool.cpp
1/*
2
3Pencil2D - Traditional Animation Software
4Copyright (C) 2005-2007 Patrick Corrieri & Pascal Naidon
5Copyright (C) 2012-2020 Matthew Chiawen Chang
6
7This program is free software; you can redistribute it and/or
8modify it under the terms of the GNU General Public License
9as published by the Free Software Foundation; version 2 of the License.
10
11This program is distributed in the hope that it will be useful,
12but WITHOUT ANY WARRANTY; without even the implied warranty of
13MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14GNU General Public License for more details.
15
16*/
17
18#include "stroketool.h"
19
20#include <QKeyEvent>
21#include "scribblearea.h"
22#include "viewmanager.h"
23#include "preferencemanager.h"
24#include "editor.h"
25#include "toolmanager.h"
26#include "mathutils.h"
27
28#include "canvascursorpainter.h"
29
30#ifdef Q_OS_MAC
31extern "C" {
32 void detectWhichOSX();
33 void disableCoalescing();
34 void enableCoalescing();
35}
36#else
37extern "C" {
38 void detectWhichOSX() {}
39 void disableCoalescing() {}
40 void enableCoalescing() {}
41}
42#endif
43
44const qreal StrokeTool::FEATHER_MIN = 1.;
45const qreal StrokeTool::FEATHER_MAX = 99.;
46const qreal StrokeTool::WIDTH_MIN = 1.;
47const qreal StrokeTool::WIDTH_MAX = 200.;
48
49// ---- shared static variables ---- ( only one instance for all the tools )
50bool StrokeTool::mQuickSizingEnabled = false;
51
52StrokeTool::StrokeTool(QObject* parent) : BaseTool(parent)
53{
54 detectWhichOSX();
55}
56
57void StrokeTool::loadSettings()
58{
59 mQuickSizingEnabled = mEditor->preference()->isOn(SETTING::QUICK_SIZING);
60 mCanvasCursorEnabled = mEditor->preference()->isOn(SETTING::CANVAS_CURSOR);
61
64 connect(mEditor->preference(), &PreferenceManager::optionChanged, this, &StrokeTool::onPreferenceChanged);
65
66 connect(&mWidthSizingTool, &RadialOffsetTool::offsetChanged, this, [=](qreal offset) {
67 mEditor->tools()->setWidth(qBound(WIDTH_MIN, offset * 2.0, WIDTH_MAX));
68 });
69
70 connect(&mFeatherSizingTool, &RadialOffsetTool::offsetChanged, this, [=](qreal offset){
71 const qreal inputMin = FEATHER_MIN;
72 const qreal inputMax = properties.width * 0.5;
73 const qreal outputMax = FEATHER_MAX;
74 const qreal outputMin = inputMin;
75
76 // We map the feather value to a value between the min width and max width
77 const qreal mappedValue = MathUtils::map(offset, inputMin, inputMax, outputMax, outputMin);
78
79 mEditor->tools()->setFeather(qBound(FEATHER_MIN, mappedValue, FEATHER_MAX));
80 });
81}
82
83bool StrokeTool::enteringThisTool()
84{
85 mActiveConnections.append(connect(mEditor->view(), &ViewManager::viewChanged, this, &StrokeTool::onViewUpdated));
86 return true;
87}
88
89bool StrokeTool::leavingThisTool()
90{
91 return BaseTool::leavingThisTool();
92}
93
94void StrokeTool::onPreferenceChanged(SETTING setting)
95{
96 if (setting == SETTING::QUICK_SIZING) {
97 mQuickSizingEnabled = mEditor->preference()->isOn(setting);
98 } else if (setting == SETTING::CANVAS_CURSOR) {
99 mCanvasCursorEnabled = mEditor->preference()->isOn(setting);
100 }
101}
102
103void StrokeTool::onViewUpdated()
104{
105 updateCanvasCursor();
106}
107
108QPointF StrokeTool::getCurrentPressPixel() const
109{
110 return mInterpolator.getCurrentPressPixel();
111}
112
113QPointF StrokeTool::getCurrentPressPoint() const
114{
115 return mEditor->view()->mapScreenToCanvas(mInterpolator.getCurrentPressPixel());
116}
117
118QPointF StrokeTool::getCurrentPixel() const
119{
120 return mInterpolator.getCurrentPixel();
121}
122
123QPointF StrokeTool::getCurrentPoint() const
124{
125 return mEditor->view()->mapScreenToCanvas(getCurrentPixel());
126}
127
128QPointF StrokeTool::getLastPixel() const
129{
130 return mInterpolator.getLastPixel();
131}
132
133QPointF StrokeTool::getLastPoint() const
134{
135 return mEditor->view()->mapScreenToCanvas(getLastPixel());
136}
137
138void StrokeTool::startStroke(PointerEvent::InputType inputType)
139{
140 if (emptyFrameActionEnabled())
141 {
142 mScribbleArea->handleDrawingOnEmptyFrame();
143 }
144
145 mFirstDraw = true;
146 mLastPixel = getCurrentPixel();
147
148 mStrokePoints.clear();
149
150 //Experimental
151 QPointF startStrokes = mInterpolator.interpolateStart(mLastPixel);
152 mStrokePoints << mEditor->view()->mapScreenToCanvas(startStrokes);
153
154 mStrokePressures.clear();
155 mStrokePressures << mInterpolator.getPressure();
156
157 mCurrentInputType = inputType;
158 mUndoSaveState = mEditor->undoRedo()->state(UndoRedoRecordType::KEYFRAME_MODIFY);
159
160 disableCoalescing();
161}
162
163bool StrokeTool::keyPressEvent(QKeyEvent *event)
164{
165 switch (event->key()) {
166 case Qt::Key_Alt:
167 if (mEditor->tools()->setTemporaryTool(EYEDROPPER, {}, Qt::AltModifier))
168 {
169 return true;
170 }
171 break;
172 case Qt::Key_Space:
173 if (mEditor->tools()->setTemporaryTool(HAND, Qt::Key_Space, Qt::NoModifier))
174 {
175 return true;
176 }
177 break;
178 }
179 return BaseTool::keyPressEvent(event);
180}
181
182bool StrokeTool::emptyFrameActionEnabled()
183{
184 return true;
185}
186
187void StrokeTool::endStroke()
188{
189 mInterpolator.interpolateEnd();
190 mStrokePressures << mInterpolator.getPressure();
191 mStrokePoints.clear();
192 mStrokePressures.clear();
193
194 enableCoalescing();
195
196 mEditor->setModified(mEditor->currentLayerIndex(), mEditor->currentFrame());
197 mScribbleArea->endStroke();
198
199 mEditor->undoRedo()->record(mUndoSaveState, typeName());
200}
201
202void StrokeTool::drawStroke()
203{
204 QPointF pixel = getCurrentPixel();
205 if (pixel != mLastPixel || !mFirstDraw)
206 {
207 // get last pixel before interpolation initializes
208 QPointF startStrokes = mInterpolator.interpolateStart(getLastPixel());
209 mStrokePoints << mEditor->view()->mapScreenToCanvas(startStrokes);
210 mStrokePressures << mInterpolator.getPressure();
211 }
212 else
213 {
214 mFirstDraw = false;
215 }
216}
217
218bool StrokeTool::handleQuickSizing(PointerEvent* event)
219{
220 if (!mQuickSizingEnabled) { return false; }
221
222 if (!mQuickSizingProperties.contains(event->modifiers())) {
223 mWidthSizingTool.stopAdjusting();
224 mFeatherSizingTool.stopAdjusting();
225 return false;
226 }
227
228 ToolPropertyType setting = mQuickSizingProperties[event->modifiers()];
229 if (event->eventType() == PointerEvent::Press) {
230 switch (setting) {
231 case WIDTH: {
232 mWidthSizingTool.setOffset(properties.width * 0.5);
233 break;
234 }
235 case FEATHER: {
236 const qreal factor = 0.5;
237 const qreal cursorRad = properties.width * factor;
238
239 // Pull feather handle closer to center as feather increases
240 const qreal featherWidthFactor = MathUtils::normalize(properties.feather, FEATHER_MIN, FEATHER_MAX);
241 const qreal offset = (cursorRad * featherWidthFactor);
242 mFeatherSizingTool.setOffset(offset);
243 break;
244 }
245 default: break;
246 }
247 }
248
249 switch (setting) {
250 case WIDTH: {
251 mWidthSizingTool.pointerEvent(event);
252 break;
253 }
254 case FEATHER: {
255 mFeatherSizingTool.pointerEvent(event);
256 break;
257 }
258 default: break;
259 }
260
261 updateCanvasCursor();
262 return true;
263}
264
265void StrokeTool::pointerPressEvent(PointerEvent*)
266{
267 updateCanvasCursor();
268}
269
270void StrokeTool::pointerMoveEvent(PointerEvent*)
271{
272 updateCanvasCursor();
273}
274
275void StrokeTool::pointerReleaseEvent(PointerEvent*)
276{
277 updateCanvasCursor();
278}
279
280bool StrokeTool::enterEvent(QEnterEvent*)
281{
282 mCanvasCursorEnabled = mEditor->preference()->isOn(SETTING::CANVAS_CURSOR);
283 return true;
284}
285
286bool StrokeTool::leaveEvent(QEvent*)
287{
288 if (isActive())
289 {
290 return false;
291 }
292
293 mCanvasCursorEnabled = false;
294 updateCanvasCursor();
295 return true;
296}
297
298
299QRectF StrokeTool::cursorRect(ToolPropertyType settingType, const QPointF& point)
300{
301 const qreal brushWidth = properties.width;
302 const qreal brushFeather = properties.feather;
303
304 const QPointF& cursorPos = point;
305 const qreal cursorRad = brushWidth * 0.5;
306 const QPointF& widthCursorTopLeft = QPointF(cursorPos.x() - cursorRad, cursorPos.y() - cursorRad);
307
308 const QRectF widthCircleRect = QRectF(widthCursorTopLeft, QSizeF(brushWidth, brushWidth));
309 if (settingType == WIDTH) {
310 return widthCircleRect;
311 } else if (settingType == FEATHER) {
312 const qreal featherWidthFactor = MathUtils::normalize(brushFeather, FEATHER_MIN, FEATHER_MAX);
313 QRectF featherRect = QRectF(widthCircleRect.center().x() - (cursorRad * featherWidthFactor),
314 widthCircleRect.center().y() - (cursorRad * featherWidthFactor),
315 brushWidth * featherWidthFactor,
316 brushWidth * featherWidthFactor);
317
318 // Adjust the feather rect so it doesn't colide with the width rect;
319 // as this cancels out both circles when painted
320 return featherRect.adjusted(2, 2, -2, -2);
321 }
322
323 return QRectF();
324}
325
326
327void StrokeTool::updateCanvasCursor()
328{
329 CanvasCursorPainterOptions widthOptions;
330 widthOptions.circleRect = cursorRect(WIDTH, mWidthSizingTool.isAdjusting() ? mWidthSizingTool.offsetPoint() : getCurrentPoint());
331 widthOptions.showCursor = mCanvasCursorEnabled;
332 widthOptions.showCross = true;
333
334 CanvasCursorPainterOptions featherOptions;
335 featherOptions.circleRect = cursorRect(FEATHER, mFeatherSizingTool.isAdjusting() ? mFeatherSizingTool.offsetPoint() : getCurrentPoint());
336 featherOptions.showCursor = mCanvasCursorEnabled;
337 featherOptions.showCross = false;
338
339 if (mFeatherSizingTool.isAdjusting()) {
340 widthOptions.circleRect = cursorRect(WIDTH, mFeatherSizingTool.offsetPoint());
341 } else if (mWidthSizingTool.isAdjusting()) {
342 featherOptions.circleRect = cursorRect(FEATHER, mWidthSizingTool.offsetPoint());
343 }
344
345 mWidthCursorPainter.preparePainter(widthOptions);
346 mFeatherCursorPainter.preparePainter(featherOptions);
347
348 const QRect& dirtyRect = mWidthCursorPainter.dirtyRect();
349
350 // We know that the width rect is bigger than the feather rect
351 // so we don't need to change this
352 const QRect& updateRect = widthOptions.circleRect.toAlignedRect();
353
354 // Adjusted to account for some pixel bleeding outside the update rect
355 mScribbleArea->update(mEditor->view()->getView().mapRect(updateRect.united(dirtyRect).adjusted(-2, -2, 2, 2)));
356 mWidthCursorPainter.clearDirty();
357}
358
359void StrokeTool::paint(QPainter& painter, const QRect& blitRect)
360{
361 painter.save();
362 painter.setTransform(mEditor->view()->getView());
363
364 if (properties.useFeather) {
365 mFeatherCursorPainter.paint(painter, blitRect);
366 }
367
368 mWidthCursorPainter.paint(painter, blitRect);
369
370 painter.restore();
371}
BaseTool
Definition: basetool.h:70
BaseTool::leavingThisTool
virtual bool leavingThisTool()
Will clean up active connections.
Definition: basetool.cpp:69
PointerEvent
Definition: pointerevent.h:8
ScribbleArea::handleDrawingOnEmptyFrame
void handleDrawingOnEmptyFrame()
Call this when starting to use a paint tool.
Definition: scribblearea.cpp:827
StrokeTool::leavingThisTool
bool leavingThisTool() override
Will clean up active connections.
Definition: stroketool.cpp:89
StrokeTool::enteringThisTool
bool enteringThisTool() override
Setup active connections here that should only emit while tool is active leavingThisTool will handle ...
Definition: stroketool.cpp:83
StrokeTool::isActive
bool isActive() const override
Check if the tool is active.
Definition: stroketool.h:56
StrokeTool::loadSettings
void loadSettings() override
Definition: stroketool.cpp:57
StrokeTool::emptyFrameActionEnabled
virtual bool emptyFrameActionEnabled()
Whether to enable the "drawing on empty frame" preference.
Definition: stroketool.cpp:182
UndoRedoManager::record
void record(const UndoSaveState *&undoState, const QString &description)
Records the given save state.
Definition: undoredomanager.cpp:95
UndoRedoManager::state
const UndoSaveState * state(UndoRedoRecordType recordType) const
Prepares and returns a save state with the given scope.
Definition: undoredomanager.cpp:199
QEnterEvent
QEvent
QHash::contains
bool contains(const Key &key) const const
QKeyEvent
QList::append
void append(const T &value)
QList::clear
void clear()
QObject
QObject::connect
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
QObject::event
virtual bool event(QEvent *e)
QPainter
QPainter::restore
void restore()
QPainter::save
void save()
QPainter::setTransform
void setTransform(const QTransform &transform, bool combine)
QPointF
QPointF::x
qreal x() const const
QPointF::y
qreal y() const const
QRect
QRect::adjusted
QRect adjusted(int dx1, int dy1, int dx2, int dy2) const const
QRect::united
QRect united(const QRect &rectangle) const const
QRectF
QRectF::adjusted
QRectF adjusted(qreal dx1, qreal dy1, qreal dx2, qreal dy2) const const
QRectF::center
QPointF center() const const
QRectF::toAlignedRect
QRect toAlignedRect() const const
QSizeF
Qt::Key_Alt
Key_Alt
Qt::AltModifier
AltModifier
QTransform::mapRect
QRect mapRect(const QRect &rectangle) const const
QWidget::update
void update()
CanvasCursorPainterOptions
Definition: canvascursorpainter.h:25
Generated on Mon Dec 15 2025 03:41:35 for Pencil2D by doxygen 1.9.6 based on revision 9bfef078cfa681fa5250352bbcb5a69281765ae9