Coverage Report

Created: 2026-04-29 07:28

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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: */