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
15namespace app {
16namespace tools {
17
18using namespace gfx;
19
20// Shared logic between controllers that can move/displace all points
21// using the space bar.
22class MoveOriginCapability : public Controller {
23public:
24 void pressButton(ToolLoop* loop, Stroke& stroke, const Stroke::Pt& pt) override {
25 m_last = pt;
26 }
27
28protected:
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
49private:
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
56class FreehandController : public Controller {
57public:
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
105private:
106 Stroke::Pt m_last;
107};
108
109// Controls clicks for tools like line
110class TwoPointsController : public MoveOriginCapability {
111public:
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
289private:
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
321class PointByPointController : public MoveOriginCapability {
322public:
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
375class OnePointController : public Controller {
376public:
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
413class FourPointsController : public MoveOriginCapability {
414public:
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
472private:
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.
478class LineFreehandController : public Controller {
479public:
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
537private:
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