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 | #include "app/snap_to_grid.h" |
9 | #include "base/gcd.h" |
10 | #include "base/pi.h" |
11 | |
12 | #include <algorithm> |
13 | #include <cmath> |
14 | |
15 | namespace app { |
16 | namespace tools { |
17 | |
18 | using namespace gfx; |
19 | |
20 | // Shared logic between controllers that can move/displace all points |
21 | // using the space bar. |
22 | class MoveOriginCapability : public Controller { |
23 | public: |
24 | void pressButton(ToolLoop* loop, Stroke& stroke, const Stroke::Pt& pt) override { |
25 | m_last = pt; |
26 | } |
27 | |
28 | protected: |
29 | bool isMovingOrigin(ToolLoop* loop, Stroke& stroke, const Stroke::Pt& pt) { |
30 | bool used = false; |
31 | |
32 | if (int(loop->getModifiers()) & int(ToolLoopModifiers::kMoveOrigin)) { |
33 | Point delta(pt.x - m_last.x, |
34 | pt.y - m_last.y); |
35 | stroke.offset(delta); |
36 | |
37 | onMoveOrigin(delta); |
38 | used = true; |
39 | } |
40 | |
41 | m_last = pt; |
42 | return used; |
43 | } |
44 | |
45 | virtual void onMoveOrigin(const Point& delta) { |
46 | // Do nothing |
47 | } |
48 | |
49 | private: |
50 | // Last known mouse position used to calculate delta values (dx, dy) |
51 | // with the new mouse position to displace all points. |
52 | Stroke::Pt m_last; |
53 | }; |
54 | |
55 | // Controls clicks for tools like pencil |
56 | class FreehandController : public Controller { |
57 | public: |
58 | bool isFreehand() override { return true; } |
59 | |
60 | Stroke::Pt getLastPoint() const override { return m_last; } |
61 | |
62 | void pressButton(ToolLoop* loop, Stroke& stroke, const Stroke::Pt& pt) override { |
63 | m_last = pt; |
64 | stroke.addPoint(pt); |
65 | } |
66 | |
67 | bool releaseButton(Stroke& stroke, const Stroke::Pt& pt) override { |
68 | return false; |
69 | } |
70 | |
71 | void movement(ToolLoop* loop, Stroke& stroke, const Stroke::Pt& pt) override { |
72 | m_last = pt; |
73 | stroke.addPoint(pt); |
74 | } |
75 | |
76 | void getStrokeToInterwine(const Stroke& input, Stroke& output) override { |
77 | if (input.size() == 1) { |
78 | output.addPoint(input[0]); |
79 | } |
80 | else if (input.size() >= 2) { |
81 | // The freehand controller returns only the last two points to |
82 | // interwine because we accumulate (TracePolicy::Accumulate) the |
83 | // previously painted points (i.e. don't want to redraw all the |
84 | // stroke from the very beginning) |
85 | output.addPoint(input[input.size()-2]); |
86 | output.addPoint(input[input.size()-1]); |
87 | } |
88 | } |
89 | |
90 | void getStatusBarText(ToolLoop* loop, const Stroke& stroke, std::string& text) override { |
91 | ASSERT(!stroke.empty()); |
92 | if (stroke.empty()) |
93 | return; |
94 | |
95 | gfx::Point offset = loop->statusBarPositionOffset(); |
96 | char buf[1024]; |
97 | sprintf(buf, ":start: %d %d :end: %d %d" , |
98 | stroke.firstPoint().x+offset.x, |
99 | stroke.firstPoint().y+offset.y, |
100 | stroke.lastPoint().x+offset.x, |
101 | stroke.lastPoint().y+offset.y); |
102 | text = buf; |
103 | } |
104 | |
105 | private: |
106 | Stroke::Pt m_last; |
107 | }; |
108 | |
109 | // Controls clicks for tools like line |
110 | class TwoPointsController : public MoveOriginCapability { |
111 | public: |
112 | bool isTwoPoints() override { return true; } |
113 | |
114 | void pressButton(ToolLoop* loop, Stroke& stroke, const Stroke::Pt& pt) override { |
115 | MoveOriginCapability::pressButton(loop, stroke, pt); |
116 | |
117 | m_first = m_center = pt; |
118 | m_angle = 0.0; |
119 | |
120 | stroke.addPoint(pt); |
121 | stroke.addPoint(pt); |
122 | |
123 | if (loop->isSelectingTiles()) |
124 | snapPointsToGridTiles(loop, stroke); |
125 | } |
126 | |
127 | bool releaseButton(Stroke& stroke, const Stroke::Pt& pt) override { |
128 | return false; |
129 | } |
130 | |
131 | void movement(ToolLoop* loop, Stroke& stroke, const Stroke::Pt& pt) override { |
132 | ASSERT(stroke.size() >= 2); |
133 | if (stroke.size() < 2) |
134 | return; |
135 | |
136 | if (MoveOriginCapability::isMovingOrigin(loop, stroke, pt)) |
137 | return; |
138 | |
139 | if (!loop->getIntertwine()->snapByAngle() && |
140 | int(loop->getModifiers()) & int(ToolLoopModifiers::kRotateShape)) { |
141 | if ((int(loop->getModifiers()) & int(ToolLoopModifiers::kFromCenter))) { |
142 | m_center = m_first; |
143 | } |
144 | else { |
145 | m_center.x = (stroke[0].x+stroke[1].x)/2; |
146 | m_center.y = (stroke[0].y+stroke[1].y)/2; |
147 | } |
148 | m_angle = std::atan2(static_cast<double>(pt.y-m_center.y), |
149 | static_cast<double>(pt.x-m_center.x)); |
150 | return; |
151 | } |
152 | |
153 | stroke[0] = m_first; |
154 | stroke[1] = pt; |
155 | |
156 | bool isoAngle = false; |
157 | |
158 | if ((int(loop->getModifiers()) & int(ToolLoopModifiers::kSquareAspect))) { |
159 | int dx = stroke[1].x - m_first.x; |
160 | int dy = stroke[1].y - m_first.y; |
161 | int minsize = std::min(ABS(dx), ABS(dy)); |
162 | int maxsize = std::max(ABS(dx), ABS(dy)); |
163 | |
164 | // Lines |
165 | if (loop->getIntertwine()->snapByAngle()) { |
166 | double angle = 180.0 * std::atan(static_cast<double>(-dy) / |
167 | static_cast<double>(dx)) / PI; |
168 | angle = ABS(angle); |
169 | |
170 | // Snap horizontally |
171 | if (angle < 18.0) { |
172 | stroke[1].y = m_first.y; |
173 | } |
174 | // Snap at 26.565 |
175 | else if (angle < 36.0) { |
176 | stroke[1].x = m_first.x + SGN(dx)*maxsize; |
177 | stroke[1].y = m_first.y + SGN(dy)*maxsize/2; |
178 | isoAngle = true; |
179 | } |
180 | // Snap at 45 |
181 | else if (angle < 54.0) { |
182 | stroke[1].x = m_first.x + SGN(dx)*minsize; |
183 | stroke[1].y = m_first.y + SGN(dy)*minsize; |
184 | } |
185 | // Snap at 63.435 |
186 | else if (angle < 72.0) { |
187 | stroke[1].x = m_first.x + SGN(dx)*maxsize/2; |
188 | stroke[1].y = m_first.y + SGN(dy)*maxsize; |
189 | isoAngle = true; |
190 | } |
191 | // Snap vertically |
192 | else { |
193 | stroke[1].x = m_first.x; |
194 | } |
195 | } |
196 | // Rectangles and ellipses |
197 | else { |
198 | stroke[1].x = m_first.x + SGN(dx)*minsize; |
199 | stroke[1].y = m_first.y + SGN(dy)*minsize; |
200 | } |
201 | } |
202 | |
203 | if (hasAngle()) { |
204 | int rx = stroke[1].x - m_center.x; |
205 | int ry = stroke[1].y - m_center.y; |
206 | stroke[0].x = m_center.x - rx; |
207 | stroke[0].y = m_center.y - ry; |
208 | stroke[1].x = m_center.x + rx; |
209 | stroke[1].y = m_center.y + ry; |
210 | } |
211 | else if ((int(loop->getModifiers()) & int(ToolLoopModifiers::kFromCenter))) { |
212 | int rx = stroke[1].x - m_first.x; |
213 | int ry = stroke[1].y - m_first.y; |
214 | stroke[0].x = m_first.x - rx + (isoAngle && ABS(rx) > ABS(ry) ? SGN(rx)*(rx & 1): 0); |
215 | stroke[0].y = m_first.y - ry + (isoAngle && ABS(rx) < ABS(ry) ? SGN(ry)*(ry & 1): 0); |
216 | stroke[1].x = m_first.x + rx; |
217 | stroke[1].y = m_first.y + ry; |
218 | } |
219 | |
220 | // Adjust points for selection like tools (so we can select tiles) |
221 | if (loop->getController()->canSnapToGrid() && |
222 | loop->getSnapToGrid()) { |
223 | auto bounds = loop->getBrush()->bounds(); |
224 | |
225 | if (loop->isSelectingTiles()) { |
226 | snapPointsToGridTiles(loop, stroke); |
227 | } |
228 | else { |
229 | if (stroke[0].x < stroke[1].x) |
230 | stroke[1].x -= bounds.w; |
231 | else if (stroke[0].x > stroke[1].x) |
232 | stroke[0].x -= bounds.w; |
233 | |
234 | if (stroke[0].y < stroke[1].y) |
235 | stroke[1].y -= bounds.h; |
236 | else if (stroke[0].y > stroke[1].y) |
237 | stroke[0].y -= bounds.h; |
238 | } |
239 | } |
240 | } |
241 | |
242 | void getStrokeToInterwine(const Stroke& input, Stroke& output) override { |
243 | ASSERT(input.size() >= 2); |
244 | if (input.size() < 2) |
245 | return; |
246 | |
247 | output.addPoint(input[0]); |
248 | output.addPoint(input[1]); |
249 | } |
250 | |
251 | void getStatusBarText(ToolLoop* loop, const Stroke& stroke, std::string& text) override { |
252 | ASSERT(stroke.size() >= 2); |
253 | if (stroke.size() < 2) |
254 | return; |
255 | |
256 | int w = ABS(stroke[1].x-stroke[0].x)+1; |
257 | int h = ABS(stroke[1].y-stroke[0].y)+1; |
258 | |
259 | gfx::Point offset = loop->statusBarPositionOffset(); |
260 | char buf[1024]; |
261 | int gcd = base::gcd(w, h); |
262 | sprintf(buf, ":start: %d %d :end: %d %d :size: %d %d :distance: %.1f" , |
263 | stroke[0].x+offset.x, stroke[0].y+offset.y, |
264 | stroke[1].x+offset.x, stroke[1].y+offset.y, |
265 | w, h, std::sqrt(w*w + h*h)); |
266 | |
267 | if (hasAngle() || |
268 | loop->getIntertwine()->snapByAngle()) { |
269 | double angle; |
270 | if (hasAngle()) |
271 | angle = m_angle; |
272 | else |
273 | angle = std::atan2(static_cast<double>(stroke[0].y-stroke[1].y), |
274 | static_cast<double>(stroke[1].x-stroke[0].x)); |
275 | sprintf(buf+strlen(buf), " :angle: %.1f" , 180.0 * angle / PI); |
276 | } |
277 | |
278 | // Aspect ratio at the end |
279 | sprintf(buf+strlen(buf), " :aspect_ratio: %d:%d" , |
280 | w/gcd, h/gcd); |
281 | |
282 | text = buf; |
283 | } |
284 | |
285 | double getShapeAngle() const override { |
286 | return m_angle; |
287 | } |
288 | |
289 | private: |
290 | void snapPointsToGridTiles(ToolLoop* loop, Stroke& stroke) { |
291 | auto grid = loop->getGridBounds(); |
292 | |
293 | Rect a(snap_to_grid(grid, stroke[0].toPoint(), PreferSnapTo::BoxOrigin), |
294 | snap_to_grid(grid, stroke[0].toPoint(), PreferSnapTo::BoxEnd)); |
295 | Rect b(snap_to_grid(grid, stroke[1].toPoint(), PreferSnapTo::BoxOrigin), |
296 | snap_to_grid(grid, stroke[1].toPoint(), PreferSnapTo::BoxEnd)); |
297 | |
298 | a |= b; |
299 | |
300 | stroke[0] = a.origin(); |
301 | stroke[1] = a.point2() - gfx::Point(1, 1); |
302 | } |
303 | |
304 | bool hasAngle() const { |
305 | return (ABS(m_angle) > 0.001); |
306 | } |
307 | |
308 | void onMoveOrigin(const Point& delta) override { |
309 | m_first.x += delta.x; |
310 | m_first.y += delta.y; |
311 | m_center.x += delta.x; |
312 | m_center.y += delta.y; |
313 | } |
314 | |
315 | Stroke::Pt m_first; |
316 | Stroke::Pt m_center; |
317 | double m_angle; |
318 | }; |
319 | |
320 | // Controls clicks for tools like polygon |
321 | class PointByPointController : public MoveOriginCapability { |
322 | public: |
323 | |
324 | void pressButton(ToolLoop* loop, Stroke& stroke, const Stroke::Pt& pt) override { |
325 | MoveOriginCapability::pressButton(loop, stroke, pt); |
326 | |
327 | stroke.addPoint(pt); |
328 | stroke.addPoint(pt); |
329 | } |
330 | |
331 | bool releaseButton(Stroke& stroke, const Stroke::Pt& pt) override { |
332 | ASSERT(!stroke.empty()); |
333 | if (stroke.empty()) |
334 | return false; |
335 | |
336 | if (stroke[stroke.size()-2] == pt && |
337 | stroke[stroke.size()-1] == pt) |
338 | return false; // Click in the same point (no-drag), we are done |
339 | else |
340 | return true; // Continue adding points |
341 | } |
342 | |
343 | void movement(ToolLoop* loop, Stroke& stroke, const Stroke::Pt& pt) override { |
344 | ASSERT(!stroke.empty()); |
345 | if (stroke.empty()) |
346 | return; |
347 | |
348 | if (MoveOriginCapability::isMovingOrigin(loop, stroke, pt)) |
349 | return; |
350 | |
351 | stroke[stroke.size()-1] = pt; |
352 | } |
353 | |
354 | void getStrokeToInterwine(const Stroke& input, Stroke& output) override { |
355 | output = input; |
356 | } |
357 | |
358 | void getStatusBarText(ToolLoop* loop, const Stroke& stroke, std::string& text) override { |
359 | ASSERT(!stroke.empty()); |
360 | if (stroke.empty()) |
361 | return; |
362 | |
363 | gfx::Point offset = loop->statusBarPositionOffset(); |
364 | char buf[1024]; |
365 | sprintf(buf, ":start: %d %d :end: %d %d" , |
366 | stroke.firstPoint().x+offset.x, |
367 | stroke.firstPoint().y+offset.y, |
368 | stroke.lastPoint().x+offset.x, |
369 | stroke.lastPoint().y+offset.y); |
370 | text = buf; |
371 | } |
372 | |
373 | }; |
374 | |
375 | class OnePointController : public Controller { |
376 | public: |
377 | // Do not apply grid to "one point tools" (e.g. magic wand, flood fill, etc.) |
378 | bool canSnapToGrid() override { return false; } |
379 | bool isOnePoint() override { return true; } |
380 | |
381 | void pressButton(ToolLoop* loop, Stroke& stroke, const Stroke::Pt& pt) override { |
382 | if (stroke.size() == 0) |
383 | stroke.addPoint(pt); |
384 | } |
385 | |
386 | bool releaseButton(Stroke& stroke, const Stroke::Pt& pt) override { |
387 | return false; |
388 | } |
389 | |
390 | void movement(ToolLoop* loop, Stroke& stroke, const Stroke::Pt& pt) override { |
391 | // Do nothing |
392 | } |
393 | |
394 | void getStrokeToInterwine(const Stroke& input, Stroke& output) override { |
395 | output = input; |
396 | } |
397 | |
398 | void getStatusBarText(ToolLoop* loop, const Stroke& stroke, std::string& text) override { |
399 | ASSERT(!stroke.empty()); |
400 | if (stroke.empty()) |
401 | return; |
402 | |
403 | gfx::Point offset = loop->statusBarPositionOffset(); |
404 | char buf[1024]; |
405 | sprintf(buf, ":pos: %d %d" , |
406 | stroke[0].x+offset.x, |
407 | stroke[0].y+offset.y); |
408 | text = buf; |
409 | } |
410 | |
411 | }; |
412 | |
413 | class FourPointsController : public MoveOriginCapability { |
414 | public: |
415 | |
416 | void pressButton(ToolLoop* loop, Stroke& stroke, const Stroke::Pt& pt) override { |
417 | MoveOriginCapability::pressButton(loop, stroke, pt); |
418 | |
419 | if (stroke.size() == 0) { |
420 | stroke.reset(4, pt); |
421 | m_clickCounter = 0; |
422 | } |
423 | else |
424 | m_clickCounter++; |
425 | } |
426 | |
427 | bool releaseButton(Stroke& stroke, const Stroke::Pt& pt) override { |
428 | m_clickCounter++; |
429 | return m_clickCounter < 4; |
430 | } |
431 | |
432 | void movement(ToolLoop* loop, Stroke& stroke, const Stroke::Pt& pt) override { |
433 | if (MoveOriginCapability::isMovingOrigin(loop, stroke, pt)) |
434 | return; |
435 | |
436 | switch (m_clickCounter) { |
437 | case 0: |
438 | for (int i=1; i<stroke.size(); ++i) |
439 | stroke[i] = pt; |
440 | break; |
441 | case 1: |
442 | case 2: |
443 | stroke[1] = pt; |
444 | stroke[2] = pt; |
445 | break; |
446 | case 3: |
447 | stroke[2] = pt; |
448 | break; |
449 | } |
450 | } |
451 | |
452 | void getStrokeToInterwine(const Stroke& input, Stroke& output) override { |
453 | output = input; |
454 | } |
455 | |
456 | void getStatusBarText(ToolLoop* loop, const Stroke& stroke, std::string& text) override { |
457 | ASSERT(stroke.size() >= 4); |
458 | if (stroke.size() < 4) |
459 | return; |
460 | |
461 | gfx::Point offset = loop->statusBarPositionOffset(); |
462 | char buf[1024]; |
463 | sprintf(buf, ":start: %d %d :end: %d %d (%d %d - %d %d)" , |
464 | stroke[0].x+offset.x, stroke[0].y+offset.y, |
465 | stroke[3].x+offset.x, stroke[3].y+offset.y, |
466 | stroke[1].x+offset.x, stroke[1].y+offset.y, |
467 | stroke[2].x+offset.x, stroke[2].y+offset.y); |
468 | |
469 | text = buf; |
470 | } |
471 | |
472 | private: |
473 | int m_clickCounter; |
474 | }; |
475 | |
476 | // Controls the shift+click to draw a two-points line and then |
477 | // freehand until the mouse is released. |
478 | class LineFreehandController : public Controller { |
479 | public: |
480 | bool isFreehand() override { return true; } |
481 | |
482 | Stroke::Pt getLastPoint() const override { return m_last; } |
483 | |
484 | void prepareController(ToolLoop* loop) override { |
485 | m_controller = nullptr; |
486 | } |
487 | |
488 | void pressButton(ToolLoop* loop, Stroke& stroke, const Stroke::Pt& pt) override { |
489 | m_last = pt; |
490 | |
491 | if (m_controller == nullptr) |
492 | m_controller = &m_twoPoints; |
493 | else if (m_controller == &m_twoPoints) { |
494 | if ((int(loop->getModifiers()) & int(ToolLoopModifiers::kSquareAspect))) { |
495 | // Don't switch to freehand, just continue with lines if we |
496 | // have the square aspect key pressed (e.g. Ctrl+Shift). In |
497 | // this way we avoid to paint two straight lines: 1) from the |
498 | // very beginning, and 2) from the end of the first straight |
499 | // line to the new "pt". |
500 | } |
501 | else { |
502 | m_controller = &m_freehand; |
503 | } |
504 | return; // Don't send first pressButton() click to the freehand controller |
505 | } |
506 | |
507 | m_controller->pressButton(loop, stroke, pt); |
508 | } |
509 | |
510 | bool releaseButton(Stroke& stroke, const Stroke::Pt& pt) override { |
511 | if (!stroke.empty()) |
512 | m_last = stroke.lastPoint(); |
513 | return false; |
514 | } |
515 | |
516 | void movement(ToolLoop* loop, Stroke& stroke, const Stroke::Pt& pt) override { |
517 | m_last = pt; |
518 | m_controller->movement(loop, stroke, pt); |
519 | } |
520 | |
521 | void getStrokeToInterwine(const Stroke& input, Stroke& output) override { |
522 | m_controller->getStrokeToInterwine(input, output); |
523 | } |
524 | |
525 | void getStatusBarText(ToolLoop* loop, const Stroke& stroke, std::string& text) override { |
526 | m_controller->getStatusBarText(loop, stroke, text); |
527 | } |
528 | |
529 | bool handleTracePolicy() const override { |
530 | return (m_controller == &m_twoPoints); |
531 | } |
532 | |
533 | TracePolicy getTracePolicy() const override { |
534 | return TracePolicy::Last; |
535 | } |
536 | |
537 | private: |
538 | Stroke::Pt m_last; |
539 | TwoPointsController m_twoPoints; |
540 | FreehandController m_freehand; |
541 | Controller* m_controller; |
542 | }; |
543 | |
544 | } // namespace tools |
545 | } // namespace app |
546 | |