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 | |
37 | namespace app { |
38 | namespace tools { |
39 | |
40 | using namespace gfx; |
41 | using namespace doc; |
42 | using namespace filters; |
43 | |
44 | ToolLoopManager::ToolLoopManager(ToolLoop* toolLoop) |
45 | : m_toolLoop(toolLoop) |
46 | , m_canceled(false) |
47 | , m_brush0(*toolLoop->getBrush()) |
48 | , m_dynamics(toolLoop->getDynamics()) |
49 | { |
50 | } |
51 | |
52 | ToolLoopManager::~ToolLoopManager() |
53 | { |
54 | } |
55 | |
56 | bool ToolLoopManager::isCanceled() const |
57 | { |
58 | return m_canceled; |
59 | } |
60 | |
61 | void ToolLoopManager::cancel() |
62 | { |
63 | m_canceled = true; |
64 | } |
65 | |
66 | void ToolLoopManager::end() |
67 | { |
68 | if (m_canceled) |
69 | m_toolLoop->rollback(); |
70 | else |
71 | m_toolLoop->commit(); |
72 | } |
73 | |
74 | void 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 | |
86 | void ToolLoopManager::notifyToolLoopModifiersChange() |
87 | { |
88 | if (isCanceled()) |
89 | return; |
90 | |
91 | if (m_lastPointer.button() != Pointer::None) |
92 | movement(m_lastPointer); |
93 | } |
94 | |
95 | void 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 | |
145 | bool 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 | |
169 | void 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 | |
205 | void 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. |
292 | void 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. |
308 | void 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 | |
357 | Stroke::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 | |
379 | bool ToolLoopManager::useDynamics() const |
380 | { |
381 | return (m_dynamics.isDynamic() && |
382 | !m_toolLoop->getFilled() && |
383 | m_toolLoop->getController()->isFreehand()); |
384 | } |
385 | |
386 | void 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 | |