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::msIsAdjusting = false;
51bool StrokeTool::mQuickSizingEnabled = false;
52
53StrokeTool::StrokeTool(QObject* parent) : BaseTool(parent)
54{
55 detectWhichOSX();
56}
57
58void StrokeTool::loadSettings()
59{
60 mQuickSizingEnabled = mEditor->preference()->isOn(SETTING::QUICK_SIZING);
61 mCanvasCursorEnabled = mEditor->preference()->isOn(SETTING::CANVAS_CURSOR);
62
65 connect(mEditor->preference(), &PreferenceManager::optionChanged, this, &StrokeTool::onPreferenceChanged);
66}
67
68bool StrokeTool::enteringThisTool()
69{
70 mActiveConnections.append(connect(mEditor->view(), &ViewManager::viewChanged, this, &StrokeTool::onViewUpdated));
71 return true;
72}
73
74bool StrokeTool::leavingThisTool()
75{
76 return BaseTool::leavingThisTool();
77}
78
79void StrokeTool::onPreferenceChanged(SETTING setting)
80{
81 if (setting == SETTING::QUICK_SIZING) {
82 mQuickSizingEnabled = mEditor->preference()->isOn(setting);
83 } else if (setting == SETTING::CANVAS_CURSOR) {
84 mCanvasCursorEnabled = mEditor->preference()->isOn(setting);
85 }
86}
87
88void StrokeTool::onViewUpdated()
89{
90 updateCanvasCursor();
91}
92
93QPointF StrokeTool::getCurrentPressPixel() const
94{
95 return mInterpolator.getCurrentPressPixel();
96}
97
98QPointF StrokeTool::getCurrentPressPoint() const
99{
100 return mEditor->view()->mapScreenToCanvas(mInterpolator.getCurrentPressPixel());
101}
102
103QPointF StrokeTool::getCurrentPixel() const
104{
105 return mInterpolator.getCurrentPixel();
106}
107
108QPointF StrokeTool::getCurrentPoint() const
109{
110 return mEditor->view()->mapScreenToCanvas(getCurrentPixel());
111}
112
113QPointF StrokeTool::getLastPixel() const
114{
115 return mInterpolator.getLastPixel();
116}
117
118QPointF StrokeTool::getLastPoint() const
119{
120 return mEditor->view()->mapScreenToCanvas(getLastPixel());
121}
122
123void StrokeTool::startStroke(PointerEvent::InputType inputType)
124{
125 if (emptyFrameActionEnabled())
126 {
127 mScribbleArea->handleDrawingOnEmptyFrame();
128 }
129
130 mFirstDraw = true;
131 mLastPixel = getCurrentPixel();
132
133 mStrokePoints.clear();
134
135 //Experimental
136 QPointF startStrokes = mInterpolator.interpolateStart(mLastPixel);
137 mStrokePoints << mEditor->view()->mapScreenToCanvas(startStrokes);
138
139 mStrokePressures.clear();
140 mStrokePressures << mInterpolator.getPressure();
141
142 mCurrentInputType = inputType;
143 mUndoSaveState = mEditor->undoRedo()->state(UndoRedoRecordType::KEYFRAME_MODIFY);
144
145 disableCoalescing();
146}
147
148bool StrokeTool::keyPressEvent(QKeyEvent *event)
149{
150 switch (event->key()) {
151 case Qt::Key_Alt:
152 if (mEditor->tools()->setTemporaryTool(EYEDROPPER, {}, Qt::AltModifier))
153 {
154 return true;
155 }
156 break;
157 case Qt::Key_Space:
158 if (mEditor->tools()->setTemporaryTool(HAND, Qt::Key_Space, Qt::NoModifier))
159 {
160 return true;
161 }
162 break;
163 }
164 return BaseTool::keyPressEvent(event);
165}
166
167bool StrokeTool::emptyFrameActionEnabled()
168{
169 return true;
170}
171
172void StrokeTool::endStroke()
173{
174 mInterpolator.interpolateEnd();
175 mStrokePressures << mInterpolator.getPressure();
176 mStrokePoints.clear();
177 mStrokePressures.clear();
178
179 enableCoalescing();
180
181 mEditor->setModified(mEditor->currentLayerIndex(), mEditor->currentFrame());
182 mScribbleArea->endStroke();
183
184 mEditor->undoRedo()->record(mUndoSaveState, typeName());
185}
186
187void StrokeTool::drawStroke()
188{
189 QPointF pixel = getCurrentPixel();
190 if (pixel != mLastPixel || !mFirstDraw)
191 {
192 // get last pixel before interpolation initializes
193 QPointF startStrokes = mInterpolator.interpolateStart(getLastPixel());
194 mStrokePoints << mEditor->view()->mapScreenToCanvas(startStrokes);
195 mStrokePressures << mInterpolator.getPressure();
196 }
197 else
198 {
199 mFirstDraw = false;
200 }
201}
202
203bool StrokeTool::handleQuickSizing(PointerEvent* event)
204{
205 if (event->eventType() == PointerEvent::Press) {
206 if (mQuickSizingEnabled) {
207 return startAdjusting(event->modifiers());
208 }
209 } else if (event->eventType() == PointerEvent::Move) {
210 if (event->buttons() & Qt::LeftButton && msIsAdjusting) {
211 adjustCursor(event->modifiers());
212 return true;
213 }
214 } else if (event->eventType() == PointerEvent::Release) {
215 if (msIsAdjusting) {
216 stopAdjusting();
217 return true;
218 }
219 }
220 return false;
221}
222
223void StrokeTool::pointerPressEvent(PointerEvent*)
224{
225 updateCanvasCursor();
226}
227
228void StrokeTool::pointerMoveEvent(PointerEvent*)
229{
230 updateCanvasCursor();
231}
232
233void StrokeTool::pointerReleaseEvent(PointerEvent*)
234{
235 updateCanvasCursor();
236}
237
238bool StrokeTool::enterEvent(QEnterEvent*)
239{
240 mCanvasCursorEnabled = mEditor->preference()->isOn(SETTING::CANVAS_CURSOR);
241 return true;
242}
243
244bool StrokeTool::leaveEvent(QEvent*)
245{
246 if (isActive())
247 {
248 return false;
249 }
250
251 mCanvasCursorEnabled = false;
252 updateCanvasCursor();
253 return true;
254}
255
256void StrokeTool::updateCanvasCursor()
257{
258 const qreal brushWidth = properties.width;
259 const qreal brushFeather = properties.feather;
260
261 const QPointF& cursorPos = msIsAdjusting ? mAdjustPosition : getCurrentPoint();
262 const qreal cursorRad = brushWidth * 0.5;
263 const QPointF& cursorOffset = QPointF(cursorPos.x() - cursorRad, cursorPos.y() - cursorRad);
264
265 CanvasCursorPainterOptions options;
266 options.widthRect = QRectF(cursorOffset, QSizeF(brushWidth, brushWidth));
267
268 const qreal featherWidthFactor = MathUtils::normalize(brushFeather, 0.0, FEATHER_MAX);
269 options.featherRect = QRectF(options.widthRect.center().x() - (cursorRad * featherWidthFactor),
270 options.widthRect.center().y() - (cursorRad * featherWidthFactor),
271 brushWidth * featherWidthFactor,
272 brushWidth * featherWidthFactor);
273 options.showCursor = mCanvasCursorEnabled;
274 options.isAdjusting = msIsAdjusting && mQuickSizingEnabled;
275 options.useFeather = mPropertyEnabled[FEATHER];
276
277 mCanvasCursorPainter.preparePainter(options, mEditor->view()->getView());
278
279 const QRect& dirtyRect = mCanvasCursorPainter.dirtyRect();
280 const QRect& updateRect = mEditor->view()->getView().mapRect(QRectF(cursorOffset, QSizeF(brushWidth, brushWidth))).toAlignedRect();
281
282 if (!msIsAdjusting && !mCanvasCursorEnabled) {
283 if (mCanvasCursorPainter.isDirty()) {
284 // Adjusted to account for some pixel bleeding outside the update rect
285 mScribbleArea->update(mCanvasCursorPainter.dirtyRect().adjusted(-2, -2, 2, 2));
286 mCanvasCursorPainter.clearDirty();
287 }
288 return;
289 }
290
291 // Adjusted to account for some pixel bleeding outside the update rect
292 mScribbleArea->update(updateRect.united(dirtyRect).adjusted(-2, -2, 2, 2));
293}
294
295bool StrokeTool::startAdjusting(Qt::KeyboardModifiers modifiers)
296{
297 if (!mQuickSizingProperties.contains(modifiers))
298 {
299 return false;
300 }
301
302 const QPointF& currentPressPoint = getCurrentPressPoint();
303 const QPointF& currentPoint = getCurrentPoint();
304 auto propertyType = mQuickSizingProperties.value(modifiers);
305 switch (propertyType) {
306 case WIDTH: {
307 const qreal factor = 0.5;
308 const qreal rad = properties.width * factor;
309 const qreal distance = QLineF(currentPressPoint - QPointF(rad, rad), currentPoint).length();
310 mAdjustPosition = currentPressPoint - QPointF(distance * factor, distance * factor);
311 break;
312 }
313 case FEATHER: {
314 const qreal factor = 0.5;
315 const qreal cursorRad = properties.width * factor;
316 const qreal featherWidthFactor = MathUtils::normalize(properties.feather, 0.0, FEATHER_MAX);
317 const qreal offset = (cursorRad * featherWidthFactor) * factor;
318 const qreal distance = QLineF(currentPressPoint - QPointF(offset, offset), currentPoint).length();
319 mAdjustPosition = currentPressPoint - QPointF(distance, distance);
320 break;
321 }
322 default:
323 Q_UNREACHABLE();
324 qWarning() << "Unhandled quick sizing property for tool" << typeName();
325 return false;
326 }
327
328 msIsAdjusting = true;
329 updateCanvasCursor();
330 return true;
331}
332
333void StrokeTool::stopAdjusting()
334{
335 msIsAdjusting = false;
336 mAdjustPosition = QPointF();
337
338 updateCanvasCursor();
339}
340
341void StrokeTool::adjustCursor(Qt::KeyboardModifiers modifiers)
342{
343 switch (mQuickSizingProperties.value(modifiers))
344 {
345 case WIDTH: {
346 // The adjusted position is based on the radius of the circle, so in order to
347 // map it back to its original value, we can multiply by the factor we divided with
348 const qreal newValue = QLineF(mAdjustPosition, getCurrentPoint()).length() * 2.0;
349
350 mEditor->tools()->setWidth(qBound(WIDTH_MIN, newValue, WIDTH_MAX));
351 break;
352 }
353 case FEATHER: {
354 // The radius of the width is the max value we can get
355 const qreal inputMin = 0.0;
356 const qreal inputMax = properties.width * 0.5;
357 const qreal distance = QLineF(mAdjustPosition, getCurrentPoint()).length();
358 const qreal outputMax = FEATHER_MAX;
359 const qreal outputMin = 0.0;
360
361 // We flip min and max here in order to get the inverted value for the UI
362 const qreal mappedValue = MathUtils::map(distance, inputMin, inputMax, outputMax, outputMin);
363
364 mEditor->tools()->setFeather(qBound(FEATHER_MIN, mappedValue, FEATHER_MAX));
365 break;
366 }
367 default:
368 Q_UNREACHABLE();
369 qWarning() << "Unhandled quick sizing property for tool" << typeName();
370 }
371 updateCanvasCursor();
372}
373
374void StrokeTool::paint(QPainter& painter, const QRect& blitRect)
375{
376 mCanvasCursorPainter.paint(painter, blitRect);
377}
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:74
StrokeTool::enteringThisTool
bool enteringThisTool() override
Setup active connections here that should only emit while tool is active leavingThisTool will handle ...
Definition: stroketool.cpp:68
StrokeTool::isActive
bool isActive() const override
Check if the tool is active.
Definition: stroketool.h:55
StrokeTool::loadSettings
void loadSettings() override
Definition: stroketool.cpp:58
StrokeTool::emptyFrameActionEnabled
virtual bool emptyFrameActionEnabled()
Whether to enable the "drawing on empty frame" preference.
Definition: stroketool.cpp:167
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
QHash::value
const T value(const Key &key) const const
QKeyEvent
QLineF
QLineF::length
qreal length() const const
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
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::center
QPointF center() const const
QSizeF
Qt::Key_Alt
Key_Alt
Qt::AltModifier
AltModifier
Qt::LeftButton
LeftButton
QTransform::mapRect
QRect mapRect(const QRect &rectangle) const const
QWidget::update
void update()
CanvasCursorPainterOptions
Definition: canvascursorpainter.h:25
Generated on Thu May 8 2025 04:47:53 for Pencil2D by doxygen 1.9.6 based on revision 4513250b1d5b1a3676ec0e67b06b7a885ceaae39