1// Aseprite
2// Copyright (C) 2019-2022 Igara Studio S.A.
3// Copyright (C) 2001-2018 David Capello
4//
5// This program is distributed under the terms of
6// the End-User License Agreement for Aseprite.
7
8#ifdef HAVE_CONFIG_H
9#include "config.h"
10#endif
11
12#include "app/tools/tool_loop_manager.h"
13
14#include "app/context.h"
15#include "app/snap_to_grid.h"
16#include "app/tools/controller.h"
17#include "app/tools/ink.h"
18#include "app/tools/intertwine.h"
19#include "app/tools/point_shape.h"
20#include "app/tools/symmetry.h"
21#include "app/tools/tool_loop.h"
22#include "app/tools/velocity.h"
23#include "doc/brush.h"
24#include "doc/image.h"
25#include "doc/primitives.h"
26#include "doc/sprite.h"
27#include "gfx/point_io.h"
28#include "gfx/rect_io.h"
29#include "gfx/region.h"
30
31#include <algorithm>
32#include <climits>
33#include <cmath>
34
35#define TOOL_TRACE(...) // TRACEARGS(__VA_ARGS__)
36
37namespace app {
38namespace tools {
39
40using namespace gfx;
41using namespace doc;
42using namespace filters;
43
44ToolLoopManager::ToolLoopManager(ToolLoop* toolLoop)
45 : m_toolLoop(toolLoop)
46 , m_canceled(false)
47 , m_brush0(*toolLoop->getBrush())
48 , m_dynamics(toolLoop->getDynamics())
49{
50}
51
52ToolLoopManager::~ToolLoopManager()
53{
54}
55
56bool ToolLoopManager::isCanceled() const
57{
58 return m_canceled;
59}
60
61void ToolLoopManager::cancel()
62{
63 m_canceled = true;
64}
65
66void ToolLoopManager::end()
67{
68 if (m_canceled)
69 m_toolLoop->rollback();
70 else
71 m_toolLoop->commit();
72}
73
74void ToolLoopManager::prepareLoop(const Pointer& pointer)
75{
76 // Start with no points at all
77 m_stroke.reset();
78
79 // Prepare the ink
80 m_toolLoop->getInk()->prepareInk(m_toolLoop);
81 m_toolLoop->getController()->prepareController(m_toolLoop);
82 m_toolLoop->getIntertwine()->prepareIntertwine(m_toolLoop);
83 m_toolLoop->getPointShape()->preparePointShape(m_toolLoop);
84}
85
86void ToolLoopManager::notifyToolLoopModifiersChange()
87{
88 if (isCanceled())
89 return;
90
91 if (m_lastPointer.button() != Pointer::None)
92 movement(m_lastPointer);
93}
94
95void ToolLoopManager::pressButton(const Pointer& pointer)
96{
97 TOOL_TRACE("ToolLoopManager::pressButton", pointer.point());
98
99 // A little patch to memorize initial Trace Policy in the
100 // current function execution.
101 // When the initial trace policy is "Last" and then
102 // changes to different trace policy at the end of
103 // this function, the user confirms a line draw while he
104 // is holding the SHIFT key.
105 bool tracePolicyWasLast = false;
106 if (m_toolLoop->getTracePolicy() == TracePolicy::Last)
107 tracePolicyWasLast = true;
108
109 m_lastPointer = pointer;
110
111 if (isCanceled())
112 return;
113
114 // If the user pressed the other mouse button...
115 if ((m_toolLoop->getMouseButton() == ToolLoop::Left && pointer.button() == Pointer::Right) ||
116 (m_toolLoop->getMouseButton() == ToolLoop::Right && pointer.button() == Pointer::Left)) {
117 // Cancel the tool-loop (the destination image should be completely discarded)
118 cancel();
119 return;
120 }
121
122 m_stabilizerCenter = pointer.point();
123
124 Stroke::Pt spritePoint = getSpriteStrokePt(pointer);
125 m_toolLoop->getController()->pressButton(m_toolLoop, m_stroke, spritePoint);
126
127 std::string statusText;
128 m_toolLoop->getController()->getStatusBarText(m_toolLoop, m_stroke, statusText);
129 m_toolLoop->updateStatusBar(statusText.c_str());
130
131 // We evaluate if the trace policy has changed compared with
132 // the initial trace policy.
133 if (!(m_toolLoop->getTracePolicy() == TracePolicy::Last) &&
134 tracePolicyWasLast) {
135 // Do nothing. We do not need execute an additional doLoopStep
136 // (which it want to accumulate more points in m_pts in function
137 // joinStroke() from intertwiners.h)
138 // This avoid double print of a line while the user holds down
139 // the SHIFT key.
140 }
141 else
142 doLoopStep(false);
143}
144
145bool ToolLoopManager::releaseButton(const Pointer& pointer)
146{
147 TOOL_TRACE("ToolLoopManager::releaseButton", pointer.point());
148
149 m_lastPointer = pointer;
150
151 if (isCanceled())
152 return false;
153
154 Stroke::Pt spritePoint = getSpriteStrokePt(pointer);
155 bool res = m_toolLoop->getController()->releaseButton(m_stroke, spritePoint);
156
157 if (!res && (m_toolLoop->getTracePolicy() == TracePolicy::Last ||
158 m_toolLoop->getInk()->isSelection() ||
159 m_toolLoop->getInk()->isSlice() ||
160 m_toolLoop->getFilled())) {
161 m_toolLoop->getInk()->setFinalStep(m_toolLoop, true);
162 doLoopStep(true);
163 m_toolLoop->getInk()->setFinalStep(m_toolLoop, false);
164 }
165
166 return res;
167}
168
169void ToolLoopManager::movement(Pointer pointer)
170{
171 // Filter points with the stabilizer
172 if (m_dynamics.stabilizerFactor > 0) {
173 const double f = m_dynamics.stabilizerFactor;
174 const gfx::Point delta = (pointer.point() - m_stabilizerCenter);
175 const double distance = std::sqrt(delta.x*delta.x + delta.y*delta.y);
176
177 const double angle = std::atan2(delta.y, delta.x);
178 const gfx::PointF newPoint(m_stabilizerCenter.x + distance/f*std::cos(angle),
179 m_stabilizerCenter.y + distance/f*std::sin(angle));
180
181 m_stabilizerCenter = newPoint;
182
183 pointer = Pointer(gfx::Point(newPoint),
184 pointer.velocity(),
185 pointer.button(),
186 pointer.type(),
187 pointer.pressure());
188 }
189
190 m_lastPointer = pointer;
191
192 if (isCanceled())
193 return;
194
195 Stroke::Pt spritePoint = getSpriteStrokePt(pointer);
196 m_toolLoop->getController()->movement(m_toolLoop, m_stroke, spritePoint);
197
198 std::string statusText;
199 m_toolLoop->getController()->getStatusBarText(m_toolLoop, m_stroke, statusText);
200 m_toolLoop->updateStatusBar(statusText.c_str());
201
202 doLoopStep(false);
203}
204
205void ToolLoopManager::doLoopStep(bool lastStep)
206{
207 // Original set of points to interwine (original user stroke,
208 // relative to sprite origin).
209 Stroke main_stroke;
210 if (!lastStep)
211 m_toolLoop->getController()->getStrokeToInterwine(m_stroke, main_stroke);
212 else
213 main_stroke = m_stroke;
214
215 // Calculate the area to be updated in all document observers.
216 Symmetry* symmetry = m_toolLoop->getSymmetry();
217 Strokes strokes;
218 if (symmetry)
219 symmetry->generateStrokes(main_stroke, strokes, m_toolLoop);
220 else
221 strokes.push_back(main_stroke);
222
223 calculateDirtyArea(strokes);
224
225 // If we are not in the last step (when the mouse button is
226 // released) we are only showing a preview of the tool, so we can
227 // limit the dirty area to the visible viewport bounds. In this way
228 // the area using in validateoDstImage() can be a lot smaller.
229 if (m_toolLoop->getTracePolicy() == TracePolicy::Last &&
230 !lastStep &&
231 // We cannot limit the dirty area for LineFreehandController (or
232 // in any case that the trace policy is handled by the
233 // controller) just in case the user is using the Pencil tool
234 // and used Shift+Click to draw a line and the origin point of
235 // the line is not in the viewport area (e.g. for a very long
236 // line, or with a lot of zoom in the end so the origin is not
237 // viewable, etc.).
238 !m_toolLoop->getController()->handleTracePolicy()) {
239 m_toolLoop->limitDirtyAreaToViewport(m_dirtyArea);
240 }
241
242 // Validate source image area.
243 if (m_toolLoop->getInk()->needsSpecialSourceArea()) {
244 gfx::Region srcArea;
245 m_toolLoop->getInk()->createSpecialSourceArea(m_dirtyArea, srcArea);
246 m_toolLoop->validateSrcImage(srcArea);
247 }
248 else {
249 m_toolLoop->validateSrcImage(m_dirtyArea);
250 }
251
252 m_toolLoop->getInk()->prepareForStrokes(m_toolLoop, strokes);
253
254 // True when we have to fill
255 const bool fillStrokes =
256 (m_toolLoop->getFilled() &&
257 (lastStep || m_toolLoop->getPreviewFilled()));
258
259 // Invalidate the whole destination image area.
260 if (m_toolLoop->getTracePolicy() == TracePolicy::Last ||
261 fillStrokes) {
262 // Copy source to destination (reset all the previous
263 // traces). Useful for tools like Line and Ellipse (we keep the
264 // last trace only) or to draw the final result in contour tool
265 // (the final result is filled).
266 m_toolLoop->invalidateDstImage();
267 }
268
269 m_toolLoop->validateDstImage(m_dirtyArea);
270
271 // Join or fill user points
272 if (fillStrokes)
273 m_toolLoop->getIntertwine()->fillStroke(m_toolLoop, main_stroke);
274 else
275 m_toolLoop->getIntertwine()->joinStroke(m_toolLoop, main_stroke);
276
277 if (m_toolLoop->getTracePolicy() == TracePolicy::Overlap) {
278 // Copy destination to source (yes, destination to source). In
279 // this way each new trace overlaps the previous one.
280 m_toolLoop->copyValidDstToSrcImage(m_dirtyArea);
281 }
282
283 if (!m_dirtyArea.isEmpty()) {
284 m_toolLoop->validateDstTileset(m_dirtyArea);
285 m_toolLoop->updateDirtyArea(m_dirtyArea);
286 }
287
288 TOOL_TRACE("ToolLoopManager::doLoopStep dirtyArea", m_dirtyArea.bounds());
289}
290
291// Applies the grid settings to the specified sprite point.
292void ToolLoopManager::snapToGrid(Stroke::Pt& pt)
293{
294 if (!m_toolLoop->getController()->canSnapToGrid() ||
295 !m_toolLoop->getSnapToGrid() ||
296 m_toolLoop->isSelectingTiles())
297 return;
298
299 gfx::Point point(pt.x, pt.y);
300 point = snap_to_grid(m_toolLoop->getGridBounds(), point,
301 PreferSnapTo::ClosestGridVertex);
302 point += m_toolLoop->getBrush()->center();
303 pt.x = point.x;
304 pt.y = point.y;
305}
306
307// Strokes are relative to sprite origin.
308void ToolLoopManager::calculateDirtyArea(const Strokes& strokes)
309{
310 // Save the current dirty area if it's needed
311 Region prevDirtyArea;
312 if (m_toolLoop->getTracePolicy() == TracePolicy::Last)
313 prevDirtyArea = m_nextDirtyArea;
314
315 // Start with a fresh dirty area
316 m_dirtyArea.clear();
317
318 for (auto& stroke : strokes) {
319 gfx::Rect strokeBounds =
320 m_toolLoop->getIntertwine()->getStrokeBounds(m_toolLoop, stroke);
321
322 if (strokeBounds.isEmpty())
323 continue;
324
325 // Expand the dirty-area with the pen width
326 Rect r1, r2;
327
328 m_toolLoop->getPointShape()->getModifiedArea(
329 m_toolLoop,
330 strokeBounds.x,
331 strokeBounds.y, r1);
332
333 m_toolLoop->getPointShape()->getModifiedArea(
334 m_toolLoop,
335 strokeBounds.x+strokeBounds.w-1,
336 strokeBounds.y+strokeBounds.h-1, r2);
337
338 m_dirtyArea.createUnion(m_dirtyArea, Region(r1.createUnion(r2)));
339 }
340
341 // Merge new dirty area with the previous one (for tools like line
342 // or rectangle it's needed to redraw the previous position and
343 // the new one)
344 if (m_toolLoop->getTracePolicy() == TracePolicy::Last) {
345 m_nextDirtyArea = m_dirtyArea;
346 m_dirtyArea.createUnion(m_dirtyArea, prevDirtyArea);
347 }
348
349 // Apply tiled mode
350 TiledMode tiledMode = m_toolLoop->getTiledMode();
351 if (tiledMode != TiledMode::NONE) {
352 m_toolLoop->getTiledModeHelper().wrapPosition(m_dirtyArea);
353 m_toolLoop->getTiledModeHelper().collapseRegionByTiledMode(m_dirtyArea);
354 }
355}
356
357Stroke::Pt ToolLoopManager::getSpriteStrokePt(const Pointer& pointer)
358{
359 // Convert the screen point to a sprite point
360 Stroke::Pt spritePoint = pointer.point();
361 spritePoint.size = m_brush0.size();
362 spritePoint.angle = m_brush0.angle();
363
364 // Center the input to some grid point if needed
365 snapToGrid(spritePoint);
366
367 // Control dynamic parameters through sensors
368 if (useDynamics()) {
369 adjustPointWithDynamics(pointer, spritePoint);
370 }
371
372 // Inform the original velocity vector to the ToolLoop
373 m_toolLoop->setSpeed(gfx::Point(pointer.velocity().x,
374 pointer.velocity().y));
375
376 return spritePoint;
377}
378
379bool ToolLoopManager::useDynamics() const
380{
381 return (m_dynamics.isDynamic() &&
382 !m_toolLoop->getFilled() &&
383 m_toolLoop->getController()->isFreehand());
384}
385
386void ToolLoopManager::adjustPointWithDynamics(const Pointer& pointer,
387 Stroke::Pt& pt)
388{
389 int size = pt.size;
390 int angle = pt.angle;
391
392 // Pressure
393 bool hasP = (pointer.type() == Pointer::Type::Pen ||
394 pointer.type() == Pointer::Type::Eraser);
395 float p = 1.0f;
396 if (hasP) {
397 p = pointer.pressure();
398 if (p < m_dynamics.minPressureThreshold) {
399 p = 0.0f;
400 }
401 else if (p > m_dynamics.maxPressureThreshold ||
402 // To avoid div by zero
403 m_dynamics.minPressureThreshold == m_dynamics.maxPressureThreshold) {
404 p = 1.0f;
405 }
406 else {
407 p =
408 (p - m_dynamics.minPressureThreshold) /
409 (m_dynamics.maxPressureThreshold - m_dynamics.minPressureThreshold);
410 }
411 }
412 ASSERT(p >= 0.0f && p <= 1.0f);
413 p = std::clamp(p, 0.0f, 1.0f);
414
415 // Velocity
416 float v = pointer.velocity().magnitude() / VelocitySensor::kScreenPixelsForFullVelocity;
417 v = std::clamp(v, 0.0f, 1.0f);
418 if (v < m_dynamics.minVelocityThreshold) {
419 v = 0.0f;
420 }
421 else if (v > m_dynamics.maxVelocityThreshold ||
422 // To avoid div by zero
423 m_dynamics.minVelocityThreshold == m_dynamics.maxVelocityThreshold) {
424 v = 1.0f;
425 }
426 else {
427 v =
428 (v - m_dynamics.minVelocityThreshold) /
429 (m_dynamics.maxVelocityThreshold - m_dynamics.minVelocityThreshold);
430 }
431 ASSERT(v >= 0.0f && v <= 1.0f);
432 v = std::clamp(v, 0.0f, 1.0f);
433
434 switch (m_dynamics.size) {
435 case DynamicSensor::Pressure:
436 if (hasP) size = (1.0f-p)*m_dynamics.minSize + p*size;
437 break;
438 case DynamicSensor::Velocity:
439 size = (1.0f-v)*m_dynamics.minSize + v*size;
440 break;
441 }
442
443 switch (m_dynamics.angle) {
444 case DynamicSensor::Pressure:
445 if (hasP) angle = (1.0f-p)*m_dynamics.minAngle + p*angle;
446 break;
447 case DynamicSensor::Velocity:
448 angle = (1.0f-v)*m_dynamics.minAngle + v*angle;
449 break;
450 }
451
452 switch (m_dynamics.gradient) {
453 case DynamicSensor::Pressure:
454 pt.gradient = p;
455 break;
456 case DynamicSensor::Velocity:
457 pt.gradient = v;
458 break;
459 }
460
461 pt.size = std::clamp(size, int(Brush::kMinBrushSize), int(Brush::kMaxBrushSize));
462 pt.angle = std::clamp(angle, -180, 180);
463}
464
465} // namespace tools
466} // namespace app
467