/src/libetonyek/src/lib/IWORKShape.cpp
Line | Count | Source |
1 | | /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
2 | | /* |
3 | | * This file is part of the libetonyek project. |
4 | | * |
5 | | * This Source Code Form is subject to the terms of the Mozilla Public |
6 | | * License, v. 2.0. If a copy of the MPL was not distributed with this |
7 | | * file, You can obtain one at http://mozilla.org/MPL/2.0/. |
8 | | */ |
9 | | |
10 | | #include "IWORKShape.h" |
11 | | |
12 | | #include <algorithm> |
13 | | #include <cmath> |
14 | | #include <deque> |
15 | | #include <iterator> |
16 | | |
17 | | #include <glm/glm.hpp> |
18 | | #include <memory> |
19 | | |
20 | | #include "IWORKDocumentInterface.h" |
21 | | #include "IWORKPath.h" |
22 | | #include "IWORKText.h" |
23 | | #include "IWORKTypes.h" |
24 | | #include "IWORKTransformation.h" |
25 | | |
26 | | using std::deque; |
27 | | |
28 | | namespace libetonyek |
29 | | { |
30 | | |
31 | | IWORKShape::IWORKShape() |
32 | 3.38k | : m_geometry() |
33 | 3.38k | , m_style() |
34 | 3.38k | , m_order() |
35 | 3.38k | , m_resizeFlags() |
36 | 3.38k | , m_path() |
37 | 3.38k | , m_text() |
38 | 3.38k | , m_locked(false) |
39 | 3.38k | { |
40 | 3.38k | } |
41 | | |
42 | | namespace |
43 | | { |
44 | | |
45 | | struct Point |
46 | | { |
47 | | double x; |
48 | | double y; |
49 | | |
50 | | Point(double x_, double y_); |
51 | | }; |
52 | | |
53 | | Point::Point(const double x_, const double y_) |
54 | 3.01k | : x(x_) |
55 | 3.01k | , y(y_) |
56 | 3.01k | { |
57 | 3.01k | } |
58 | | |
59 | | bool approxEqual(const Point &left, const Point &right, const double eps = ETONYEK_EPSILON) |
60 | 4.59k | { |
61 | 4.59k | using libetonyek::approxEqual; |
62 | 4.59k | return approxEqual(left.x, right.x, eps) && approxEqual(left.y, right.y, eps); |
63 | 4.59k | } |
64 | | |
65 | | bool operator==(const Point &left, const Point &right) |
66 | 4.59k | { |
67 | 4.59k | return approxEqual(left, right); |
68 | 4.59k | } |
69 | | |
70 | | } |
71 | | |
72 | | namespace |
73 | | { |
74 | | |
75 | | using namespace transformations; |
76 | | |
77 | | deque<Point> rotatePoint(const Point &point, const unsigned n) |
78 | 109 | { |
79 | 109 | deque<Point> points; |
80 | | |
81 | | |
82 | 109 | const double angle = etonyek_two_pi / n; |
83 | | |
84 | 109 | points.push_back(point); |
85 | 535 | for (unsigned i = 1; i < n; ++i) |
86 | 426 | { |
87 | 426 | Point pt(point); |
88 | 426 | const glm::dmat3 rot(rotate(i * angle)); |
89 | 426 | glm::dvec3 vec = rot * glm::dvec3(pt.x, pt.y, 1); |
90 | 426 | pt.x = vec[0]; |
91 | 426 | pt.y = vec[1]; |
92 | 426 | points.push_back(pt); |
93 | 426 | } |
94 | | |
95 | 109 | return points; |
96 | 109 | } |
97 | | |
98 | | deque<Point> drawArrowHalf(const double headWidth, const double stemThickness) |
99 | 340 | { |
100 | | // user space canvas: [0:1] x [0:1] |
101 | | |
102 | 340 | deque<Point> points; |
103 | 340 | points.push_back(Point(0, stemThickness)); |
104 | 340 | points.push_back(Point(1 - headWidth, stemThickness)); |
105 | 340 | points.push_back(Point(1 - headWidth, 1)); |
106 | 340 | points.push_back(Point(1, 0)); |
107 | | |
108 | 340 | return points; |
109 | 340 | } |
110 | | |
111 | | IWORKPathPtr_t makePolyLine(const deque<Point> inputPoints, bool close = true) |
112 | 449 | { |
113 | 449 | IWORKPathPtr_t path; |
114 | | |
115 | | // need at least 2 points to make a polyline |
116 | 449 | if (inputPoints.size() < 2) |
117 | 0 | return path; |
118 | | |
119 | | // remove multiple points |
120 | 449 | deque<Point> points; |
121 | 449 | std::unique_copy(inputPoints.begin(), inputPoints.end(), back_inserter(points)); |
122 | | |
123 | | // close path if the first and last points are equal |
124 | 449 | if (points.front() == points.back()) |
125 | 135 | { |
126 | 135 | points.pop_back(); |
127 | 135 | close = true; |
128 | 135 | } |
129 | | |
130 | | // ... but there must be at least 3 points to make a closed path |
131 | 449 | if (points.size() < 3) |
132 | 2 | close = false; |
133 | | |
134 | | // need at least 2 points to make a polyline |
135 | 449 | if (points.size() < 2) |
136 | 2 | return path; |
137 | | |
138 | 447 | path = std::make_shared<IWORKPath>(); |
139 | | |
140 | 447 | deque<Point>::const_iterator it = points.begin(); |
141 | 447 | path->appendMoveTo(it->x, it->y); |
142 | 447 | ++it; |
143 | 3.72k | for (; it != points.end(); ++it) |
144 | 3.27k | path->appendLineTo(it->x, it->y); |
145 | | |
146 | 447 | if (close) |
147 | 447 | path->appendClose(); |
148 | | |
149 | 447 | return path; |
150 | 449 | } |
151 | | |
152 | | struct TransformPoint |
153 | | { |
154 | | explicit TransformPoint(const glm::dmat3 &tr) |
155 | 1.08k | : m_tr(tr) |
156 | 1.08k | { |
157 | 1.08k | } |
158 | | |
159 | | void operator()(Point &point) const |
160 | 8.84k | { |
161 | 8.84k | glm::dvec3 vec = m_tr * glm::dvec3(point.x, point.y, 1); |
162 | 8.84k | point.x = vec[0]; |
163 | 8.84k | point.y = vec[1]; |
164 | 8.84k | } |
165 | | |
166 | | private: |
167 | | const glm::dmat3 &m_tr; |
168 | | }; |
169 | | |
170 | | void transform(deque<Point> &points, const glm::dmat3 &tr) |
171 | 1.08k | { |
172 | 1.08k | for_each(points.begin(), points.end(), TransformPoint(tr)); |
173 | 1.08k | } |
174 | | |
175 | | } |
176 | | |
177 | | IWORKPathPtr_t makePolygonPath(const IWORKSize &size, const unsigned edges) |
178 | 35 | { |
179 | | // user space canvas: [-1:1] x [-1:1] |
180 | | |
181 | 35 | deque<Point> points = rotatePoint(Point(0, -1), edges); |
182 | | |
183 | | // FIXME: the shape should probably be scaled to whole width/height. |
184 | | // Check. |
185 | 35 | transform(points, scale(size.m_width, size.m_height) * scale(0.5, 0.5) * translate(1, 1)); |
186 | 35 | const IWORKPathPtr_t path = makePolyLine(points); |
187 | | |
188 | 35 | return path; |
189 | 35 | } |
190 | | |
191 | | IWORKPathPtr_t makeRoundedRectanglePath(const IWORKSize &size, const double radius) |
192 | 74 | { |
193 | 74 | if (radius<=0) |
194 | 7 | { |
195 | | // user space canvas: [-1:1] x [-1:1] |
196 | 7 | deque<Point> points= rotatePoint(Point(1, 1), 4); |
197 | | |
198 | 7 | transform(points, scale(size.m_width, size.m_height) * scale(0.5, 0.5) * translate(1, 1)); |
199 | 7 | const IWORKPathPtr_t path = makePolyLine(points); |
200 | | |
201 | 7 | return path; |
202 | 7 | } |
203 | 67 | double wRadius=2*radius<size.m_width ? radius : size.m_width/2; |
204 | 67 | double hRadius=2*radius<size.m_height ? radius : size.m_height/2; |
205 | 67 | IWORKPathPtr_t path(new IWORKPath); |
206 | 67 | path->appendMoveTo(size.m_width-wRadius,0); |
207 | 67 | path->appendQCurveTo(size.m_width,0, size.m_width,hRadius); |
208 | 67 | path->appendLineTo(size.m_width,size.m_height-hRadius); |
209 | 67 | path->appendQCurveTo(size.m_width,size.m_height, size.m_width-wRadius,size.m_height); |
210 | 67 | path->appendLineTo(wRadius,size.m_height); |
211 | 67 | path->appendQCurveTo(0,size.m_height, 0,size.m_height-hRadius); |
212 | 67 | path->appendLineTo(0,hRadius); |
213 | 67 | path->appendQCurveTo(0,0, wRadius,0); |
214 | 67 | path->appendClose(); |
215 | 67 | return path; |
216 | 74 | } |
217 | | |
218 | | IWORKPathPtr_t makeArrowPath(const IWORKSize &size, const double headWidth, const double stemThickness) |
219 | 214 | { |
220 | 214 | deque<Point> points = drawArrowHalf(size.m_width > 0 ? headWidth / size.m_width : 1., 1 - 2 * stemThickness); |
221 | | |
222 | | // mirror around the x axis |
223 | 214 | deque<Point> mirroredPoints = points; |
224 | 214 | transform(mirroredPoints, flip(false, true)); |
225 | | |
226 | | // join the two point sets |
227 | 214 | copy(mirroredPoints.rbegin(), mirroredPoints.rend(), back_inserter(points)); |
228 | | |
229 | | // transform and create path |
230 | 214 | transform(points, scale(size.m_width, size.m_height) * scale(1, 0.5) * translate(0, 1)); |
231 | 214 | const IWORKPathPtr_t path = makePolyLine(points); |
232 | 214 | return path; |
233 | 214 | } |
234 | | |
235 | | IWORKPathPtr_t makeDoubleArrowPath(const IWORKSize &size, const double headWidth, const double stemThickness) |
236 | 126 | { |
237 | 126 | deque<Point> points = drawArrowHalf(size.m_width > 0 ? 2 * headWidth / size.m_width : 1., 1 - 2 * stemThickness); |
238 | | |
239 | 126 | { |
240 | | // mirror around the y axis |
241 | 126 | deque<Point> mirroredPoints = points; |
242 | 126 | transform(mirroredPoints, flip(true, false)); |
243 | | |
244 | 126 | copy(mirroredPoints.begin(), mirroredPoints.end(), front_inserter(points)); |
245 | 126 | } |
246 | | |
247 | 126 | { |
248 | | // mirror around the x axis |
249 | 126 | deque<Point> mirroredPoints = points; |
250 | 126 | transform(mirroredPoints, flip(false, true)); |
251 | | |
252 | 126 | copy(mirroredPoints.rbegin(), mirroredPoints.rend(), back_inserter(points)); |
253 | 126 | } |
254 | | |
255 | 126 | transform(points, scale(size.m_width, size.m_height) * scale(0.5, 0.5) * translate(1, 1)); |
256 | 126 | const IWORKPathPtr_t path = makePolyLine(points); |
257 | 126 | return path; |
258 | 126 | } |
259 | | |
260 | | IWORKPathPtr_t makeStarPath(const IWORKSize &size, const unsigned points, const double innerRadius) |
261 | 67 | { |
262 | | // user space canvas: [-1:1] x [-1:1] |
263 | | |
264 | | // create outer points |
265 | 67 | const deque<Point> outerPoints = rotatePoint(Point(0, -1), points); |
266 | | |
267 | | // create inner points |
268 | 67 | const double angle = etonyek_two_pi / points; |
269 | 67 | deque<Point> innerPoints(outerPoints); |
270 | 67 | transform(innerPoints, scale(innerRadius, innerRadius) * rotate(angle / 2)); |
271 | | |
272 | | // merge them together |
273 | 67 | deque<Point> pathPoints; |
274 | 67 | assert(outerPoints.size() == innerPoints.size()); |
275 | 67 | for (deque<Point>::const_iterator itO = outerPoints.begin(), itI = innerPoints.begin(); |
276 | 399 | (itO != outerPoints.end()) && (itI != innerPoints.end()); |
277 | 332 | ++itO, ++itI) |
278 | 332 | { |
279 | 332 | pathPoints.push_back(*itO); |
280 | 332 | pathPoints.push_back(*itI); |
281 | 332 | } |
282 | | |
283 | | // create the path |
284 | 67 | transform(pathPoints, scale(size.m_width, size.m_height) * scale(0.5, 0.5) * translate(1, 1)); |
285 | 67 | const IWORKPathPtr_t path = makePolyLine(pathPoints); |
286 | | |
287 | 67 | return path; |
288 | 67 | } |
289 | | |
290 | | IWORKPathPtr_t makeCalloutPath(const IWORKSize &size, const double radius, const double tailSize, const double tailX, const double tailY) |
291 | 106 | { |
292 | 106 | double wRadius=2*radius<size.m_width ? radius : size.m_width/2; |
293 | 106 | double hRadius=2*radius<size.m_height ? radius : size.m_height/2; |
294 | 106 | double tail[2]= {tailX-size.m_width/2, tailY-size.m_height/2}; |
295 | 106 | double xPos[4]= {-size.m_width/2,-size.m_width/2+wRadius,size.m_width/2-wRadius,size.m_width/2}; |
296 | 106 | double yPos[4]= {-size.m_height/2,-size.m_height/2+hRadius,size.m_height/2-hRadius,size.m_height/2}; |
297 | 106 | const double tSize=tailSize<0 ? -tailSize : tailSize; |
298 | 106 | glm::dmat3 trans=translate(size.m_width/2, size.m_height/2); |
299 | | // reorient figure so that the tail is in RB |
300 | 106 | trans=trans*scale(tail[0]<0 ? -1 : 1,tail[1]<0 ? -1 : 1); |
301 | 106 | if (tail[0]<0) tail[0]*=-1; |
302 | 106 | if (tail[1]<0) tail[1]*=-1; |
303 | | // first check if tail is in the round rect |
304 | 106 | if (tailX>=0 && tailX<=size.m_width && tailY>=0 && tailY<=size.m_height && |
305 | 0 | (tail[0]-xPos[2])*(tail[0]-xPos[2])*hRadius*hRadius+ |
306 | 0 | (tail[1]-yPos[2])*(tail[1]-yPos[2])*wRadius*wRadius |
307 | 0 | <= wRadius*wRadius*hRadius*hRadius) |
308 | 0 | return makeRoundedRectanglePath(size,radius); |
309 | 106 | if (tail[0]*yPos[3]<tail[1]*xPos[3]) |
310 | 1 | { |
311 | 1 | using std::swap; |
312 | 1 | swap(tail[0],tail[1]); |
313 | 1 | swap(wRadius,hRadius); |
314 | 5 | for (int i=0; i<4; ++i) swap(xPos[i], yPos[i]); |
315 | 1 | trans=trans*glm::dmat3(0, 1, 0, 1, 0, 0, 0, 0, 1); |
316 | 1 | } |
317 | 106 | deque<Point> points; |
318 | 106 | std::vector<char> orders; |
319 | 106 | orders.push_back('M'); |
320 | 106 | points.push_back(Point(xPos[1],yPos[3])); |
321 | 106 | orders.push_back('Q'); |
322 | 106 | points.push_back(Point(xPos[0],yPos[3])); |
323 | 106 | points.push_back(Point(xPos[0],yPos[2])); |
324 | 106 | orders.push_back('L'); |
325 | 106 | points.push_back(Point(xPos[0],yPos[1])); |
326 | 106 | orders.push_back('Q'); |
327 | 106 | points.push_back(Point(xPos[0],yPos[0])); |
328 | 106 | points.push_back(Point(xPos[1],yPos[0])); |
329 | 106 | orders.push_back('L'); |
330 | 106 | points.push_back(Point(xPos[2],yPos[0])); |
331 | 106 | orders.push_back('Q'); |
332 | 106 | points.push_back(Point(xPos[3],yPos[0])); |
333 | 106 | points.push_back(Point(xPos[3],yPos[1])); |
334 | | |
335 | | // ok first compute the intersection of OT with the rectangle side x=xPos[3] |
336 | 106 | double y1=tail[0]>0 ? tail[1]*xPos[3]/tail[0] : 0; |
337 | | // go to the y1-tSize |
338 | 106 | if (y1-tSize <= yPos[1]) // ok |
339 | 12 | ; |
340 | 94 | else if (y1-tSize <= yPos[2]) |
341 | 47 | { |
342 | 47 | orders.push_back('L'); |
343 | 47 | points.push_back(Point(xPos[3],y1-tSize)); |
344 | 47 | } |
345 | 47 | else |
346 | 47 | { |
347 | 47 | orders.push_back('L'); |
348 | 47 | points.push_back(Point(xPos[3],yPos[2])); |
349 | | |
350 | 47 | double delta[2]= {wRadius,y1-tSize-yPos[2]}; |
351 | 47 | double alpha=std::atan2(wRadius*delta[1],hRadius*delta[0]); |
352 | 47 | orders.push_back('Q'); |
353 | 47 | points.push_back(Point(xPos[3],yPos[2]+hRadius*std::tan(alpha/2))); |
354 | 47 | points.push_back(Point(xPos[2]+wRadius*std::cos(alpha),yPos[2]+hRadius*std::sin(alpha))); |
355 | 47 | } |
356 | | // the tail |
357 | 106 | orders.push_back('L'); |
358 | 106 | points.push_back(Point(tail[0],tail[1])); |
359 | | // after the tail |
360 | 106 | if (y1+tSize <= yPos[2]) |
361 | 27 | { |
362 | 27 | orders.push_back('L'); |
363 | 27 | points.push_back(Point(xPos[3],y1+tSize)); |
364 | 27 | orders.push_back('L'); |
365 | 27 | points.push_back(Point(xPos[3],yPos[2])); |
366 | 27 | orders.push_back('Q'); |
367 | 27 | points.push_back(Point(xPos[3],yPos[3])); |
368 | 27 | points.push_back(Point(xPos[2],yPos[3])); |
369 | 27 | } |
370 | 79 | else if (y1+tSize < yPos[3]) |
371 | 58 | { |
372 | 58 | double delta[2]= {wRadius,y1+tSize-yPos[2]}; |
373 | 58 | double alpha=std::atan2(wRadius*delta[1],hRadius*delta[0]); |
374 | | // go back to circle |
375 | 58 | orders.push_back('L'); |
376 | 58 | points.push_back(Point(xPos[2]+wRadius*std::cos(alpha),yPos[2]+hRadius*std::sin(alpha))); |
377 | | // end circle |
378 | 58 | orders.push_back('Q'); |
379 | 58 | points.push_back(Point(xPos[2]+wRadius*std::tan((1.5707963268-alpha)/2),yPos[3])); |
380 | 58 | points.push_back(Point(xPos[2],yPos[3])); |
381 | 58 | } |
382 | 21 | else if (xPos[2]-(y1+tSize-yPos[3])>xPos[1]) |
383 | 16 | { |
384 | | // clearly to small but... |
385 | 16 | points.push_back(Point(xPos[2]-(y1+tSize-yPos[3]),yPos[3])); |
386 | 16 | orders.push_back('L'); |
387 | 16 | } |
388 | 106 | transform(points, trans); |
389 | 106 | IWORKPathPtr_t path(new IWORKPath); |
390 | 106 | size_t numPoints=points.size(); |
391 | 1.20k | for (size_t i=0, pPos=0; i<orders.size(); ++i) |
392 | 1.09k | { |
393 | 1.09k | switch (orders[i]) |
394 | 1.09k | { |
395 | 106 | case 'M': |
396 | 106 | if (pPos>=numPoints) |
397 | 0 | { |
398 | 0 | ETONYEK_DEBUG_MSG(("makeCalloutPath[IWORKShape]: can not find a point\n")); |
399 | 0 | return IWORKPathPtr_t(); |
400 | 0 | } |
401 | 106 | path->appendMoveTo(points[pPos].x, points[pPos].y); |
402 | 106 | ++pPos; |
403 | 106 | break; |
404 | 540 | case 'L': |
405 | 540 | if (pPos>=numPoints) |
406 | 0 | { |
407 | 0 | ETONYEK_DEBUG_MSG(("makeCalloutPath[IWORKShape]: can not find a point\n")); |
408 | 0 | return IWORKPathPtr_t(); |
409 | 0 | } |
410 | 540 | path->appendLineTo(points[pPos].x, points[pPos].y); |
411 | 540 | ++pPos; |
412 | 540 | break; |
413 | 450 | case 'Q': |
414 | 450 | if (pPos+1>=numPoints) |
415 | 0 | { |
416 | 0 | ETONYEK_DEBUG_MSG(("makeCalloutPath[IWORKShape]: can not find a point\n")); |
417 | 0 | return IWORKPathPtr_t(); |
418 | 0 | } |
419 | 450 | path->appendQCurveTo(points[pPos].x, points[pPos].y, points[pPos+1].x, points[pPos+1].y); |
420 | 450 | pPos+=2; |
421 | 450 | break; |
422 | 0 | default: |
423 | 0 | ETONYEK_DEBUG_MSG(("makeCalloutPath[IWORKShape]: unknown order %c\n", orders[i])); |
424 | 0 | return IWORKPathPtr_t(); |
425 | 1.09k | } |
426 | 1.09k | } |
427 | 106 | path->appendClose(); |
428 | | |
429 | 106 | return path; |
430 | 106 | } |
431 | | |
432 | | IWORKPathPtr_t makeQuoteBubblePath(const IWORKSize &size, const double radius, const double tailSize, const double tailX, const double tailY) |
433 | 61 | { |
434 | | // TODO: really draw this instead of just approximating |
435 | 61 | return makeCalloutPath(size, radius, tailSize, tailX, tailY); |
436 | 61 | } |
437 | | |
438 | | } |
439 | | |
440 | | /* vim:set shiftwidth=2 softtabstop=2 expandtab: */ |