• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

CyberZHG / SVGDiagram / 20888412495

11 Jan 2026 02:56AM UTC coverage: 99.479% (-0.3%) from 99.778%
20888412495

Pull #49

github

web-flow
Merge b6044cf27 into ab7ec6118
Pull Request #49: Add edge connection to fields

67 of 74 new or added lines in 3 files covered. (90.54%)

2293 of 2305 relevant lines covered (99.48%)

2047.52 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.32
/src/diagram/svg_node.cpp
1
#include "svg_nodes.h"
2

3
#include <format>
4
#include <cmath>
5
#include <functional>
6

7
#include "attribute_utils.h"
8
#include "svg_text_size.h"
9
#include "geometry_utils.h"
10
using namespace std;
11
using namespace svg_diagram;
12

13
SVGNode::SVGNode(const double cx, const double cy) {
60✔
14
    _cx = cx;
60✔
15
    _cy = cy;
60✔
16
}
60✔
17

18
void SVGNode::setAttributeIfNotExist(const string_view &key, const string &value) {
6,138✔
19
    if (attributes().contains(key)) {
6,138✔
20
        return;
3,963✔
21
    }
22
    if (parent() != nullptr) {
2,175✔
23
        if (const auto ret = parent()->defaultNodeAttribute(key); ret.has_value()) {
2,175✔
24
            return;
128✔
25
        }
26
    }
27
    setAttribute(key, value);
2,047✔
28
}
29

30
const string& SVGNode::getAttribute(const std::string_view& key) const {
17,681✔
31
    static const string EMPTY_STRING;
17,681✔
32
    if (const auto it = attributes().find(key); it != attributes().end()) {
17,681✔
33
        return it->second;
14,569✔
34
    }
35
    if (parent() != nullptr) {
3,112✔
36
        if (const auto ret = parent()->defaultNodeAttribute(key); ret.has_value()) {
3,112✔
37
            return ret.value();
247✔
38
        }
39
    }
40
    return EMPTY_STRING;
2,865✔
41
}
42

43
void SVGNode::setShape(const string& shape) {
213✔
44
    setAttribute(ATTR_KEY_SHAPE, shape);
213✔
45
}
213✔
46

47
void SVGNode::setShape(const string_view& shape) {
213✔
48
    setShape(string(shape));
213✔
49
}
213✔
50

51
void SVGNode::setCenter(const double cx, const double cy) {
206✔
52
    _cx = cx;
206✔
53
    _cy = cy;
206✔
54
}
206✔
55

56
pair<double, double> SVGNode::center() const {
1,149✔
57
    return {_cx, _cy};
1,149✔
58
}
59

NEW
60
pair<double, double> SVGNode::fieldCenter(const string& fieldId) const {
×
NEW
61
    const auto shape = getAttribute(ATTR_KEY_SHAPE);
×
NEW
62
    if (shape != SHAPE_RECORD || !_recordPositions.contains(fieldId)) {
×
NEW
63
        return center();
×
64
    }
NEW
65
    const auto& [cx, cy, width, height] = _recordPositions.at(fieldId);
×
NEW
66
    return {cx, cy};
×
NEW
67
}
×
68

69
void SVGNode::adjustNodeSize() {
427✔
70
    setAttributeIfNotExist(ATTR_KEY_SHAPE, string(SHAPE_DEFAULT));
854✔
71
    setAttributeIfNotExist(ATTR_KEY_FONT_NAME, string(ATTR_DEF_FONT_NAME));
854✔
72
    setAttributeIfNotExist(ATTR_KEY_FONT_SIZE, string(ATTR_DEF_FONT_SIZE));
427✔
73
    const auto shape = getAttribute(ATTR_KEY_SHAPE);
427✔
74
    if (shape == SHAPE_CIRCLE) {
427✔
75
        adjustNodeSizeCircle();
137✔
76
    } else if (shape == SHAPE_DOUBLE_CIRCLE) {
290✔
77
        adjustNodeSizeDoubleCircle();
33✔
78
    } else if (shape == SHAPE_NONE || shape == SHAPE_RECT) {
257✔
79
        adjustNodeSizeRect();
52✔
80
    } else if (shape == SHAPE_ELLIPSE) {
205✔
81
        adjustNodeSizeEllipse();
161✔
82
    } else if (shape == SHAPE_RECORD) {
44✔
83
        adjustNodeSizeRecord();
44✔
84
    }
85
}
427✔
86

87
vector<unique_ptr<SVGDraw>> SVGNode::produceSVGDraws() {
427✔
88
    setAttributeIfNotExist(ATTR_KEY_SHAPE, string(SHAPE_DEFAULT));
1,281✔
89
    setAttributeIfNotExist(ATTR_KEY_COLOR, string(ATTR_DEF_COLOR));
854✔
90
    setAttributeIfNotExist(ATTR_KEY_FILL_COLOR, string(ATTR_DEF_FILL_COLOR));
854✔
91
    setAttributeIfNotExist(ATTR_KEY_FONT_COLOR, string(ATTR_DEF_FONT_COLOR));
854✔
92
    setAttributeIfNotExist(ATTR_KEY_FONT_NAME, string(ATTR_DEF_FONT_NAME));
854✔
93
    setAttributeIfNotExist(ATTR_KEY_FONT_SIZE, string(ATTR_DEF_FONT_SIZE));
854✔
94
    setAttributeIfNotExist(ATTR_KEY_STYLE, string(ATTR_DEF_STYLE));
427✔
95
    const auto shape = getAttribute(ATTR_KEY_SHAPE);
427✔
96
    if (shape == SHAPE_CIRCLE) {
427✔
97
        return produceSVGDrawsCircle();
137✔
98
    }
99
    if (shape == SHAPE_DOUBLE_CIRCLE) {
290✔
100
        return produceSVGDrawsDoubleCircle();
33✔
101
    }
102
    if (shape == SHAPE_RECT) {
257✔
103
        return produceSVGDrawsRect();
49✔
104
    }
105
    if (shape == SHAPE_ELLIPSE) {
208✔
106
        return produceSVGDrawsEllipse();
161✔
107
    }
108
    if (shape == SHAPE_RECORD) {
47✔
109
        return produceSVGDrawsRecord();
44✔
110
    }
111
    return produceSVGDrawsNone();
3✔
112
}
427✔
113

114
pair<double, double> SVGNode::computeConnectionPoint(const double angle) {
1,169✔
115
    setAttributeIfNotExist(ATTR_KEY_SHAPE, string(SHAPE_DEFAULT));
1,169✔
116
    const auto shape = getAttribute(ATTR_KEY_SHAPE);
1,169✔
117
    if (shape == SHAPE_CIRCLE) {
1,169✔
118
        return computeConnectionPointCircle(angle);
224✔
119
    }
120
    if (shape == SHAPE_DOUBLE_CIRCLE) {
945✔
121
        return computeConnectionPointDoubleCircle(angle);
208✔
122
    }
123
    if (shape == SHAPE_RECT || shape == SHAPE_NONE) {
737✔
124
        return computeConnectionPointRect(angle);
275✔
125
    }
126
    if (shape == SHAPE_RECORD) {
462✔
127
        return computeConnectionPointRecord(angle);
33✔
128
    }
129
    return computeConnectionPointEllipse(angle);
429✔
130
}
1,169✔
131

132
pair<double, double> SVGNode::computeFieldConnectionPoint(const string& fieldId, const double angle) {
1,187✔
133
    const auto shape = getAttribute(ATTR_KEY_SHAPE);
1,187✔
134
    if (shape != SHAPE_RECORD || !_recordPositions.contains(fieldId)) {
1,187✔
135
        return computeConnectionPoint(angle);
1,169✔
136
    }
137
    const auto& [cx, cy, width, height] = _recordPositions.at(fieldId);
18✔
138
    return computeConnectionPointRect(cx, cy, width, height, angle);
18✔
139
}
1,187✔
140

141
double SVGNode::computeAngle(const double x, const double y) const {
1,106✔
142
    return atan2(y - _cy, x - _cx);
1,106✔
143
}
144

145
double SVGNode::computeAngle(const pair<double, double>& p) const {
1,106✔
146
    return computeAngle(p.first, p.second);
1,106✔
147
}
148

149
double SVGNode::computeFieldAngle(const string &fieldId, const pair<double, double>& p) const {
1,118✔
150
    const auto shape = getAttribute(ATTR_KEY_SHAPE);
1,118✔
151
    if (shape != SHAPE_RECORD || !_recordPositions.contains(fieldId)) {
1,118✔
152
        return computeAngle(p);
1,106✔
153
    }
154
    const auto& [cx, cy, width, height] = _recordPositions.at(fieldId);
12✔
155
    return atan2(p.second - cy, p.first - cx);
12✔
156
}
1,118✔
157

158
bool SVGNode::isFixedSize() const {
427✔
159
    return AttributeUtils::parseBool(getAttribute(ATTR_KEY_FIXED_SIZE));
427✔
160
}
161

162
void SVGNode::updateNodeSize(const double width, const double height) {
427✔
163
    if (isFixedSize()) {
427✔
164
        setDoubleAttributeIfNotExist(ATTR_KEY_WIDTH, AttributeUtils::pointToInch(width));
46✔
165
        setDoubleAttributeIfNotExist(ATTR_KEY_HEIGHT, AttributeUtils::pointToInch(height));
46✔
166
    } else {
167
        setWidth(width);
381✔
168
        setHeight(height);
381✔
169
    }
170
}
427✔
171

172
void SVGNode::updateNodeSize(const pair<double, double>& size) {
52✔
173
    updateNodeSize(size.first, size.second);
52✔
174
}
52✔
175

176
void SVGNode::appendSVGDrawsLabel(vector<unique_ptr<SVGDraw>>& svgDraws) {
383✔
177
    appendSVGDrawsLabelWithCenter(svgDraws, _cx, _cy);
383✔
178
}
383✔
179

180
vector<unique_ptr<SVGDraw>> SVGNode::produceSVGDrawsNone() {
3✔
181
    vector<unique_ptr<SVGDraw>> svgDraws;
3✔
182
    appendSVGDrawsLabel(svgDraws);
3✔
183
    return svgDraws;
6✔
184
}
3✔
185

186
void SVGNode::adjustNodeSizeCircle() {
170✔
187
    const auto diameter = GeometryUtils::distance(computeTextSizeWithMargin());
170✔
188
    updateNodeSize(diameter, diameter);
170✔
189
}
170✔
190

191
vector<unique_ptr<SVGDraw>> SVGNode::produceSVGDrawsCircle() {
137✔
192
    vector<unique_ptr<SVGDraw>> svgDraws;
137✔
193
    auto circle = make_unique<SVGDrawCircle>(_cx, _cy, max(width(), height()) / 2.0);
137✔
194
    setStrokeStyles(circle.get());
137✔
195
    setFillStyles(circle.get(), svgDraws);
137✔
196
    svgDraws.emplace_back(std::move(circle));
137✔
197
    appendSVGDrawsLabel(svgDraws);
137✔
198
    return svgDraws;
274✔
199
}
137✔
200

201
pair<double, double> SVGNode::computeConnectionPointCircle(const double angle) const {
224✔
202
    const double radius = (width() + penWidth()) / 2.0;
224✔
203
    return {_cx + radius * cos(angle), _cy + radius * sin(angle)};
224✔
204
}
205

206
void SVGNode::adjustNodeSizeDoubleCircle() {
33✔
207
    adjustNodeSizeCircle();
33✔
208
}
33✔
209

210
vector<unique_ptr<SVGDraw>> SVGNode::produceSVGDrawsDoubleCircle() {
33✔
211
    vector<unique_ptr<SVGDraw>> svgDraws;
33✔
212
    const double radius = max(width(), height()) / 2.0;
33✔
213
    auto circleInner = make_unique<SVGDrawCircle>(_cx, _cy, radius);
33✔
214
    setStrokeStyles(circleInner.get());
33✔
215
    setFillStyles(circleInner.get(), svgDraws);
33✔
216
    svgDraws.emplace_back(std::move(circleInner));
33✔
217
    auto circleOuter = make_unique<SVGDrawCircle>(_cx, _cy, radius + DOUBLE_BORDER_MARGIN);
33✔
218
    setStrokeStyles(circleOuter.get());
33✔
219
    circleOuter->setFill("none");
66✔
220
    svgDraws.emplace_back(std::move(circleOuter));
33✔
221
    appendSVGDrawsLabel(svgDraws);
33✔
222
    return svgDraws;
66✔
223
}
33✔
224

225
std::pair<double, double> SVGNode::computeConnectionPointDoubleCircle(const double angle) const {
208✔
226
    const double radius = (width() + penWidth()) / 2.0 + DOUBLE_BORDER_MARGIN;
208✔
227
    return {_cx + radius * cos(angle), _cy + radius * sin(angle)};
208✔
228
}
229

230
void SVGNode::adjustNodeSizeRect() {
52✔
231
    updateNodeSize(computeTextSizeWithMargin());
52✔
232
}
52✔
233

234
vector<unique_ptr<SVGDraw>> SVGNode::produceSVGDrawsRect() {
49✔
235
    vector<unique_ptr<SVGDraw>> svgDraws;
49✔
236
    auto rect = make_unique<SVGDrawRect>(_cx, _cy, width(), height());
49✔
237
    setStrokeStyles(rect.get());
49✔
238
    setFillStyles(rect.get(), svgDraws);
49✔
239
    svgDraws.emplace_back(std::move(rect));
49✔
240
    appendSVGDrawsLabel(svgDraws);
49✔
241
    return svgDraws;
98✔
242
}
49✔
243

244
pair<double, double> SVGNode::computeConnectionPointRect(const double angle) const {
308✔
245
    return computeConnectionPointRect(_cx, _cy, width(), height(), angle);
308✔
246
}
247

248
pair<double, double> SVGNode::computeConnectionPointRect(const double cx, const double cy, const double width, const double height, const double angle) const {
326✔
249
    const double strokeWidth = penWidth();
326✔
250
    const double totalWidth = width + strokeWidth;
326✔
251
    const double totalHeight = height + strokeWidth;
326✔
252
    double x1 = -totalWidth / 2, y1 = -totalHeight / 2;
326✔
253
    double x2 = totalWidth / 2, y2 = totalHeight / 2;
326✔
254
    const auto vertices = vector<pair<double, double>>{{x1, y1}, {x2, y1}, {x2, y2}, {x1, y2}};
652✔
255
    for (const auto& [x, y] : vertices) {
1,618✔
256
        if (GeometryUtils::isSameAngle(angle, x, y)) {
1,296✔
257
            return {cx + x, cy + y};
4✔
258
        }
259
    }
260
    double x = 0.0, y = 0.0;
322✔
261
    for (int i = 0; i < static_cast<int>(vertices.size()); ++i) {
1,610✔
262
        x1 = vertices[i].first;
1,288✔
263
        y1 = vertices[i].second;
1,288✔
264
        x2 = vertices[(i + 1) % vertices.size()].first;
1,288✔
265
        y2 = vertices[(i + 1) % vertices.size()].second;
1,288✔
266
        if (const auto intersect = GeometryUtils::intersect(angle, x1, y1, x2, y2); intersect != nullopt) {
1,288✔
267
            x = _cx + intersect.value().first;
322✔
268
            y = _cy + intersect.value().second;
322✔
269
        }
270
    }
271
    return {x, y};
322✔
272
}
326✔
273

274
void SVGNode::adjustNodeSizeEllipse() {
161✔
275
    const auto [textWidth, textHeight] = computeTextSize();
161✔
276
    const auto [marginX, marginY] = computeMargin();
161✔
277
    updateNodeSize((textWidth + marginX * 2) * sqrt(2.0), (textHeight + marginY * 2) * sqrt(2.0));
161✔
278
}
161✔
279

280
vector<unique_ptr<SVGDraw>> SVGNode::produceSVGDrawsEllipse() {
161✔
281
    vector<unique_ptr<SVGDraw>> svgDraws;
161✔
282
    auto ellipse = make_unique<SVGDrawEllipse>(_cx, _cy, width(), height());
161✔
283
    setStrokeStyles(ellipse.get());
161✔
284
    setFillStyles(ellipse.get(), svgDraws);
161✔
285
    svgDraws.emplace_back(std::move(ellipse));
161✔
286
    appendSVGDrawsLabel(svgDraws);
161✔
287
    return svgDraws;
322✔
288
}
161✔
289

290
pair<double, double> SVGNode::computeConnectionPointEllipse(const double angle) const {
429✔
291
    const double strokeWidth = penWidth();
429✔
292
    const double totalWidth = width() + strokeWidth;
429✔
293
    const double totalHeight = height() + strokeWidth;
429✔
294
    const double rx = totalWidth / 2, ry = totalHeight / 2;
429✔
295
    const double base = sqrt(ry * ry * cos(angle) * cos(angle) + rx * rx * sin(angle) * sin(angle));
429✔
296
    const double x = rx * ry * cos(angle) / base;
429✔
297
    const double y = rx * ry * sin(angle) / base;
429✔
298
    return {_cx + x, _cy + y};
429✔
299
}
300

301
void SVGNode::adjustNodeSizeRecord() {
44✔
302
    _recordSizes.clear();
44✔
303
    function<pair<double, double>(const unique_ptr<RecordLabel>&, bool)> adjustRecordSize;
44✔
304
    adjustRecordSize = [&](const unique_ptr<RecordLabel>& recordLabel, const bool horizontal) -> pair<double, double> {
354✔
305
        if (recordLabel->children.empty()) {
266✔
306
            const SVGTextSize textSize;
163✔
307
            const double fontSize = stod(getAttribute(ATTR_KEY_FONT_SIZE));
163✔
308
            const string fontFamily = getAttribute(ATTR_KEY_FONT_NAME);
163✔
309
            auto [width, height] = textSize.computeTextSize(recordLabel->label, fontSize, fontFamily);
163✔
310
            if (width == 0.0) {
163✔
311
                width = fontSize * SVGTextSize::DEFAULT_APPROXIMATION_WIDTH_SCALE;
80✔
312
            }
313
            if (height == 0.0) {
163✔
314
                height = fontSize * SVGTextSize::DEFAULT_APPROXIMATION_HEIGHT_SCALE;
80✔
315
            }
316
            const auto [marginX, marginY] = computeMargin();
163✔
317
            width += marginX * 2;
163✔
318
            height += marginY * 2;
163✔
319
            _recordSizes[reinterpret_cast<uintptr_t>(recordLabel.get())] = {width, height};
163✔
320
            return {width, height};
163✔
321
        }
163✔
322
        double width = 0.0, height = 0.0;
103✔
323
        for (const auto& child : recordLabel->children) {
325✔
324
            const auto [subWidth, subHeight] = adjustRecordSize(child, !horizontal);
222✔
325
            if (horizontal) {
222✔
326
                width += subWidth;
126✔
327
                height = max(height, subHeight);
126✔
328
            } else {
329
                height += subHeight;
96✔
330
                width = max(width, subWidth);
96✔
331
            }
332
        }
333
        _recordSizes[reinterpret_cast<uintptr_t>(recordLabel.get())] = {width, height};
103✔
334
        return {width, height};
103✔
335
    };
44✔
336
    _recordLabel = AttributeUtils::parseRecordLabel(getAttribute(ATTR_KEY_LABEL));
44✔
337
    const auto [width, height] = adjustRecordSize(_recordLabel, true);
44✔
338
    updateNodeSize(width, height);
44✔
339
}
44✔
340

341
std::vector<std::unique_ptr<SVGDraw>> SVGNode::produceSVGDrawsRecord() {
44✔
342
    const auto nodeWidth = width();
44✔
343
    const auto nodeHeight = height();
44✔
344
    vector<unique_ptr<SVGDraw>> svgDraws;
44✔
345
    auto rect = make_unique<SVGDrawRect>(_cx, _cy, nodeWidth, nodeHeight);
44✔
346
    setStrokeStyles(rect.get());
44✔
347
    setFillStyles(rect.get(), svgDraws);
44✔
348
    svgDraws.emplace_back(std::move(rect));
44✔
349

350
    _recordPositions.clear();
44✔
351
    function<void(const unique_ptr<RecordLabel>&, bool, double, double, double, double)> drawRecordLabel;
44✔
352
    drawRecordLabel = [&](const unique_ptr<RecordLabel>& recordLabel, const bool horizontal, double x1, double y1, const double x2, const double y2) {
×
353
        const double cx = (x1 + x2) / 2;
266✔
354
        const double cy = (y1 + y2) / 2;
266✔
355
        if (!recordLabel->fieldId.empty()) {
266✔
356
            _recordPositions[recordLabel->fieldId] = {cx, cy, x2 - x1, y2 - y1};
18✔
357
        }
358
        if (recordLabel->children.empty()) {
266✔
359
            appendSVGDrawsLabelWithCenter(svgDraws, recordLabel->label, cx, cy);
163✔
360
            return;
163✔
361
        }
362
        bool first = true;
103✔
363
        for (const auto& child : recordLabel->children) {
325✔
364
            if (first) {
222✔
365
                first = false;
103✔
366
            } else {
367
                if (horizontal) {
119✔
368
                    auto line = make_unique<SVGDrawLine>(x1, y1, x1, y2);
59✔
369
                    setStrokeStyles(line.get());
59✔
370
                    svgDraws.emplace_back(std::move(line));
59✔
371
                } else {
59✔
372
                    auto line = make_unique<SVGDrawLine>(x1, y1, x2, y1);
60✔
373
                    setStrokeStyles(line.get());
60✔
374
                    svgDraws.emplace_back(std::move(line));
60✔
375
                }
60✔
376
            }
377
            const auto [subWidth, subHeight] = _recordSizes[reinterpret_cast<uintptr_t>(child.get())];
222✔
378
            if (horizontal) {
222✔
379
                drawRecordLabel(child, false, x1, y1, x1 + subWidth, y2);
126✔
380
                x1 += subWidth;
126✔
381
            } else {
382
                drawRecordLabel(child, true, x1, y1, x2, y1 + subHeight);
96✔
383
                y1 += subHeight;
96✔
384
            }
385
        }
386
    };
44✔
387
    const double x1 = _cx - nodeWidth / 2, y1 = _cy - nodeHeight / 2;
44✔
388
    const double x2 = _cx + nodeWidth / 2, y2 = _cy + nodeHeight / 2;
44✔
389
    drawRecordLabel(_recordLabel, true, x1, y1, x2, y2);
44✔
390

391
    return svgDraws;
88✔
392
}
44✔
393

394
std::pair<double, double> SVGNode::computeConnectionPointRecord(const double angle) const {
33✔
395
    return computeConnectionPointRect(angle);
33✔
396
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc