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
57StrokeTool::~StrokeTool()
58{
59}
60
61void StrokeTool::loadSettings()
62{
63 mQuickSizingEnabled = mEditor->preference()->isOn(SETTING::QUICK_SIZING);
64 mCanvasCursorEnabled = mEditor->preference()->isOn(SETTING::CANVAS_CURSOR);
65
66 QSettings pencilSettings(PENCIL2D, PENCIL2D);
67 QHash<int, PropertyInfo> info;
68 info[StrokeToolProperties::WIDTH_VALUE] = { WIDTH_MIN, WIDTH_MAX, 24.0 };
69 info[StrokeToolProperties::FEATHER_VALUE] = { FEATHER_MIN, FEATHER_MAX, 48.0 };
70 info[StrokeToolProperties::FEATHER_ENABLED] = false;
71 info[StrokeToolProperties::PRESSURE_ENABLED] = false;
72 info[StrokeToolProperties::INVISIBILITY_ENABLED] = false;
73 info[StrokeToolProperties::STABILIZATION_VALUE] = { StabilizationLevel::NONE, StabilizationLevel::STRONG, StabilizationLevel::STRONG };
74 info[StrokeToolProperties::ANTI_ALIASING_ENABLED] = false;
75 info[StrokeToolProperties::FILLCONTOUR_ENABLED] = false;
76
77 toolProperties().insertProperties(info);
78 toolProperties().loadFrom(typeName(), pencilSettings);
79
82 connect(mEditor->preference(), &PreferenceManager::optionChanged, this, &StrokeTool::onPreferenceChanged);
83
84 connect(&mWidthSizingTool, &RadialOffsetTool::offsetChanged, this, [=](qreal offset) {
85 setWidth(offset * 2.0);
86 });
87
88 connect(&mFeatherSizingTool, &RadialOffsetTool::offsetChanged, this, [=](qreal offset){
89 const qreal inputMin = FEATHER_MIN;
90 const qreal inputMax = strokeToolProperties().width() * 0.5;
91 const qreal outputMax = FEATHER_MAX;
92 const qreal outputMin = inputMin;
93
94 // We map the feather value to a value between the min width and max width
95 const qreal mappedValue = MathUtils::map(offset, inputMin, inputMax, outputMax, outputMin);
96
97 setFeather(mappedValue);
98 });
99}
100
101bool StrokeTool::enteringThisTool()
102{
103 mActiveConnections.append(connect(mEditor->view(), &ViewManager::viewChanged, this, &StrokeTool::onViewUpdated));
104 return true;
105}
106
107bool StrokeTool::leavingThisTool()
108{
109 return BaseTool::leavingThisTool();
110}
111
112void StrokeTool::onPreferenceChanged(SETTING setting)
113{
114 if (setting == SETTING::QUICK_SIZING) {
115 mQuickSizingEnabled = mEditor->preference()->isOn(setting);
116 } else if (setting == SETTING::CANVAS_CURSOR) {
117 mCanvasCursorEnabled = mEditor->preference()->isOn(setting);
118 }
119}
120
121void StrokeTool::onViewUpdated()
122{
123 updateCanvasCursor();
124}
125
126QPointF StrokeTool::getCurrentPressPixel() const
127{
128 return mInterpolator.getCurrentPressPixel();
129}
130
131QPointF StrokeTool::getCurrentPressPoint() const
132{
133 return mEditor->view()->mapScreenToCanvas(mInterpolator.getCurrentPressPixel());
134}
135
136QPointF StrokeTool::getCurrentPixel() const
137{
138 return mInterpolator.getCurrentPixel();
139}
140
141QPointF StrokeTool::getCurrentPoint() const
142{
143 return mEditor->view()->mapScreenToCanvas(getCurrentPixel());
144}
145
146QPointF StrokeTool::getLastPixel() const
147{
148 return mInterpolator.getLastPixel();
149}
150
151QPointF StrokeTool::getLastPoint() const
152{
153 return mEditor->view()->mapScreenToCanvas(getLastPixel());
154}
155
156void StrokeTool::startStroke(PointerEvent::InputType inputType)
157{
158 if (emptyFrameActionEnabled())
159 {
160 mScribbleArea->handleDrawingOnEmptyFrame();
161 }
162
163 mFirstDraw = true;
164 mLastPixel = getCurrentPixel();
165
166 mStrokePoints.clear();
167
168 //Experimental
169 QPointF startStrokes = mInterpolator.interpolateStart(mLastPixel);
170 mStrokePoints << mEditor->view()->mapScreenToCanvas(startStrokes);
171
172 mStrokePressures.clear();
173 mStrokePressures << mInterpolator.getPressure();
174
175 mCurrentInputType = inputType;
176 mUndoSaveState = mEditor->undoRedo()->state(UndoRedoRecordType::KEYFRAME_MODIFY);
177
178 disableCoalescing();
179}
180
181bool StrokeTool::keyPressEvent(QKeyEvent *event)
182{
183 switch (event->key()) {
184 case Qt::Key_Alt:
185 if (mEditor->tools()->setTemporaryTool(EYEDROPPER, {}, Qt::AltModifier))
186 {
187 return true;
188 }
189 break;
190 case Qt::Key_Space:
191 if (mEditor->tools()->setTemporaryTool(HAND, Qt::Key_Space, Qt::NoModifier))
192 {
193 return true;
194 }
195 break;
196 }
197 return BaseTool::keyPressEvent(event);
198}
199
200bool StrokeTool::emptyFrameActionEnabled()
201{
202 return true;
203}
204
205void StrokeTool::endStroke()
206{
207 mInterpolator.interpolateEnd();
208 mStrokePressures << mInterpolator.getPressure();
209 mStrokePoints.clear();
210 mStrokePressures.clear();
211
212 enableCoalescing();
213
214 mEditor->setModified(mEditor->currentLayerIndex(), mEditor->currentFrame());
215 mScribbleArea->endStroke();
216
217 mEditor->undoRedo()->record(mUndoSaveState, typeName());
218}
219
220void StrokeTool::drawStroke()
221{
222 QPointF pixel = getCurrentPixel();
223 if (pixel != mLastPixel || !mFirstDraw)
224 {
225 // get last pixel before interpolation initializes
226 QPointF startStrokes = mInterpolator.interpolateStart(getLastPixel());
227 mStrokePoints << mEditor->view()->mapScreenToCanvas(startStrokes);
228 mStrokePressures << mInterpolator.getPressure();
229 }
230 else
231 {
232 mFirstDraw = false;
233 }
234}
235
236bool StrokeTool::handleQuickSizing(PointerEvent* event)
237{
238 if (!mQuickSizingEnabled) { return false; }
239
240 if (!mQuickSizingProperties.contains(event->modifiers())) {
241 mWidthSizingTool.stopAdjusting();
242 mFeatherSizingTool.stopAdjusting();
243 return false;
244 }
245
246 StrokeToolProperties::Type setting = static_cast<StrokeToolProperties::Type>(mQuickSizingProperties[event->modifiers()]);
247 if (event->eventType() == PointerEvent::Press) {
248 switch (setting) {
249 case StrokeToolProperties::WIDTH_VALUE: {
250 mWidthSizingTool.setOffset(strokeToolProperties().width() * 0.5);
251 break;
252 }
253 case StrokeToolProperties::FEATHER_VALUE: {
254 const qreal factor = 0.5;
255 const qreal cursorRad = strokeToolProperties().width() * factor;
256
257 // Pull feather handle closer to center as feather increases
258 const qreal featherWidthFactor = MathUtils::normalize(strokeToolProperties().feather(), FEATHER_MIN, FEATHER_MAX);
259 const qreal offset = (cursorRad * featherWidthFactor);
260 mFeatherSizingTool.setOffset(offset);
261 break;
262 }
263 default: break;
264 }
265 }
266
267 switch (setting) {
268 case StrokeToolProperties::WIDTH_VALUE: {
269 mWidthSizingTool.pointerEvent(event);
270 break;
271 }
272 case StrokeToolProperties::FEATHER_VALUE: {
273 mFeatherSizingTool.pointerEvent(event);
274 break;
275 }
276 default: break;
277 }
278
279 updateCanvasCursor();
280 return true;
281}
282
283void StrokeTool::pointerPressEvent(PointerEvent*)
284{
285 updateCanvasCursor();
286}
287
288void StrokeTool::pointerMoveEvent(PointerEvent*)
289{
290 updateCanvasCursor();
291}
292
293void StrokeTool::pointerReleaseEvent(PointerEvent*)
294{
295 updateCanvasCursor();
296}
297
298bool StrokeTool::enterEvent(QEnterEvent*)
299{
300 mCanvasCursorEnabled = mEditor->preference()->isOn(SETTING::CANVAS_CURSOR);
301 return true;
302}
303
304bool StrokeTool::leaveEvent(QEvent*)
305{
306 if (isActive())
307 {
308 return false;
309 }
310
311 mCanvasCursorEnabled = false;
312 updateCanvasCursor();
313 return true;
314}
315
316
317QRectF StrokeTool::cursorRect(StrokeToolProperties::Type settingType, const QPointF& point)
318{
319 const qreal brushWidth = strokeToolProperties().width();
320 const qreal brushFeather = strokeToolProperties().feather();
321
322 const QPointF& cursorPos = point;
323 const qreal cursorRad = brushWidth * 0.5;
324 const QPointF& widthCursorTopLeft = QPointF(cursorPos.x() - cursorRad, cursorPos.y() - cursorRad);
325
326 const QRectF widthCircleRect = QRectF(widthCursorTopLeft, QSizeF(brushWidth, brushWidth));
327 if (settingType == StrokeToolProperties::WIDTH_VALUE) {
328 return widthCircleRect;
329 } else if (settingType == StrokeToolProperties::FEATHER_VALUE) {
330 const qreal featherWidthFactor = MathUtils::normalize(brushFeather, FEATHER_MIN, FEATHER_MAX);
331 QRectF featherRect = QRectF(widthCircleRect.center().x() - (cursorRad * featherWidthFactor),
332 widthCircleRect.center().y() - (cursorRad * featherWidthFactor),
333 brushWidth * featherWidthFactor,
334 brushWidth * featherWidthFactor);
335
336 // Adjust the feather rect so it doesn't colide with the width rect;
337 // as this cancels out both circles when painted
338 return featherRect.adjusted(2, 2, -2, -2);
339 }
340
341 return QRectF();
342}
343
344
345void StrokeTool::updateCanvasCursor()
346{
347 CanvasCursorPainterOptions widthOptions;
348 widthOptions.circleRect = cursorRect(StrokeToolProperties::WIDTH_VALUE, mWidthSizingTool.isAdjusting() ? mWidthSizingTool.offsetPoint() : getCurrentPoint());
349 widthOptions.showCursor = mCanvasCursorEnabled;
350 widthOptions.showCross = true;
351
352 CanvasCursorPainterOptions featherOptions;
353 featherOptions.circleRect = cursorRect(StrokeToolProperties::FEATHER_VALUE, mFeatherSizingTool.isAdjusting() ? mFeatherSizingTool.offsetPoint() : getCurrentPoint());
354 featherOptions.showCursor = mCanvasCursorEnabled;
355 featherOptions.showCross = false;
356
357 if (mFeatherSizingTool.isAdjusting()) {
358 widthOptions.circleRect = cursorRect(StrokeToolProperties::WIDTH_VALUE, mFeatherSizingTool.offsetPoint());
359 } else if (mWidthSizingTool.isAdjusting()) {
360 featherOptions.circleRect = cursorRect(StrokeToolProperties::FEATHER_VALUE, mWidthSizingTool.offsetPoint());
361 }
362
363 mWidthCursorPainter.preparePainter(widthOptions);
364 mFeatherCursorPainter.preparePainter(featherOptions);
365
366 const QRect& dirtyRect = mWidthCursorPainter.dirtyRect();
367
368 // We know that the width rect is bigger than the feather rect
369 // so we don't need to change this
370 const QRect& updateRect = widthOptions.circleRect.toAlignedRect();
371
372 // Adjusted to account for some pixel bleeding outside the update rect
373 mScribbleArea->update(mEditor->view()->getView().mapRect(updateRect.united(dirtyRect).adjusted(-2, -2, 2, 2)));
374 mWidthCursorPainter.clearDirty();
375}
376
377void StrokeTool::paint(QPainter& painter, const QRect& blitRect)
378{
379 painter.save();
380 painter.setTransform(mEditor->view()->getView());
381
382 if (strokeToolProperties().featherEnabled()) {
383 mFeatherCursorPainter.paint(painter, blitRect);
384 }
385
386 mWidthCursorPainter.paint(painter, blitRect);
387
388 painter.restore();
389}
390
391void StrokeTool::setStablizationLevel(int level)
392{
393 toolProperties().setBaseValue(StrokeToolProperties::STABILIZATION_VALUE, level);
394 emit stabilizationLevelChanged(level);
395}
396
397void StrokeTool::setFeatherEnabled(bool enabled)
398{
399 toolProperties().setBaseValue(StrokeToolProperties::FEATHER_ENABLED, enabled);
400 emit featherEnabledChanged(enabled);
401}
402
403void StrokeTool::setFeather(qreal feather)
404{
405 toolProperties().setBaseValue(StrokeToolProperties::FEATHER_VALUE, feather);
406 emit featherChanged(strokeToolProperties().feather());
407}
408
409void StrokeTool::setWidth(qreal width)
410{
411 toolProperties().setBaseValue(StrokeToolProperties::WIDTH_VALUE, width);
412 emit widthChanged(strokeToolProperties().width());
413}
414
415void StrokeTool::setPressureEnabled(bool enabled)
416{
417 toolProperties().setBaseValue(StrokeToolProperties::PRESSURE_ENABLED, enabled);
418 emit pressureEnabledChanged(enabled);
419}
420
421void StrokeTool::setFillContourEnabled(bool enabled)
422{
423 toolProperties().setBaseValue(StrokeToolProperties::FILLCONTOUR_ENABLED, enabled);
424 emit fillContourEnabledChanged(enabled);
425}
426
427void StrokeTool::setAntiAliasingEnabled(bool enabled)
428{
429 toolProperties().setBaseValue(StrokeToolProperties::ANTI_ALIASING_ENABLED, enabled);
430 emit antiAliasingEnabledChanged(enabled);
431}
432
433void StrokeTool::setStrokeInvisibleEnabled(bool enabled)
434{
435 toolProperties().setBaseValue(StrokeToolProperties::INVISIBILITY_ENABLED, enabled);
436 emit invisibleStrokeEnabledChanged(enabled);
437}
BaseTool
Definition: basetool.h:47
BaseTool::leavingThisTool
virtual bool leavingThisTool()
Will clean up active connections.
Definition: basetool.cpp:91
PointerEvent
Definition: pointerevent.h:8
ScribbleArea::handleDrawingOnEmptyFrame
void handleDrawingOnEmptyFrame()
Call this when starting to use a paint tool.
Definition: scribblearea.cpp:812
StrokeTool::leavingThisTool
bool leavingThisTool() override
Will clean up active connections.
Definition: stroketool.cpp:107
StrokeTool::enteringThisTool
bool enteringThisTool() override
Setup active connections here that should only emit while tool is active leavingThisTool will handle ...
Definition: stroketool.cpp:101
StrokeTool::isActive
bool isActive() const override
Check if the tool is active.
Definition: stroketool.h:54
StrokeTool::loadSettings
void loadSettings() override
Definition: stroketool.cpp:61
StrokeTool::emptyFrameActionEnabled
virtual bool emptyFrameActionEnabled()
Whether to enable the "drawing on empty frame" preference.
Definition: stroketool.cpp:200
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
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
QSettings
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 Tue Jan 6 2026 11:38:51 for Pencil2D by doxygen 1.9.6 based on revision f91a96748ec6712509b9b0ff47979db9c34d556f