presentation/GraphAreasItem.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "GraphAreasItem.h" | ||
| 2 | |||
| 3 | #include <QElapsedTimer> | ||
| 4 | #include <QLoggingCategory> | ||
| 5 | #include <QSGGeometryNode> | ||
| 6 | |||
| 7 | #include <cmath> | ||
| 8 | #include <functional> | ||
| 9 | |||
| 10 |
4/7✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 8 taken 30 times.
✓ Branch 4 → 5 taken 2 times.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 2 times.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 12 not taken.
|
32 | Q_LOGGING_CATEGORY(lcGraphAreas, "mailjd.graph.areas") |
| 11 | |||
| 12 | |||
| 13 | namespace { | ||
| 14 | |||
| 15 | // The outline through its edges' midpoints with quadratic corners, as the | ||
| 16 | // Canvas rounded it (world px, ±0.3 px). | ||
| 17 | 32 | QList<QPointF> smoothLoop(const QList<QPointF> &loop) { | |
| 18 | 32 | const int n = int(loop.size()); | |
| 19 |
1/2✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 32 times.
|
32 | if (n < 3) |
| 20 | ✗ | return loop; | |
| 21 | 32 | QList<QPointF> out; | |
| 22 |
2/2✓ Branch 22 → 6 taken 1198 times.
✓ Branch 22 → 23 taken 32 times.
|
1230 | for (int k = 0; k < n; ++k) { |
| 23 | 1198 | const QPointF before = (loop.at((k - 1 + n) % n) + loop.at(k)) / 2; | |
| 24 | 1198 | const QPointF after = (loop.at(k) + loop.at((k + 1) % n)) / 2; | |
| 25 |
1/1✓ Branch 15 → 16 taken 1198 times.
|
1198 | const QList<QPointF> corner = GraphGeometry::quadratic(before, loop.at(k), after, 0.3); |
| 26 |
2/2✓ Branch 17 → 18 taken 1198 times.
✓ Branch 18 → 19 taken 1198 times.
|
1198 | out.append(corner.mid(0, corner.size() - 1)); // the next corner starts with this end |
| 27 | 1198 | } | |
| 28 | 32 | return out; | |
| 29 | 32 | } | |
| 30 | |||
| 31 | 7849 | QPointF leftNormal(QPointF a, QPointF b) { | |
| 32 | 7849 | const QPointF d = b - a; | |
| 33 | 7849 | const double l = std::hypot(d.x(), d.y()); | |
| 34 |
1/2✓ Branch 5 → 6 taken 7849 times.
✗ Branch 5 → 9 not taken.
|
7849 | return l > 1e-9 ? QPointF(-d.y() / l, d.x() / l) : QPointF(0, 0); |
| 35 | } | ||
| 36 | |||
| 37 | } // namespace | ||
| 38 | |||
| 39 |
1/1✓ Branch 9 → 10 taken 14 times.
|
14 | GraphAreasItem::GraphAreasItem(QQuickItem *parent) : QQuickItem(parent) { |
| 40 |
1/1✓ Branch 10 → 11 taken 14 times.
|
14 | setFlag(ItemHasContents, true); |
| 41 |
1/1✓ Branch 11 → 12 taken 14 times.
|
14 | connect(this, &GraphAreasItem::changed, this, [this] { |
| 42 | 68 | m_contentChanged = true; | |
| 43 | 68 | update(); | |
| 44 | 68 | }); | |
| 45 |
1/1✓ Branch 13 → 14 taken 14 times.
|
14 | m_refresh.setSingleShot(true); |
| 46 |
1/1✓ Branch 14 → 15 taken 14 times.
|
14 | m_refresh.setInterval(120); |
| 47 |
1/1✓ Branch 15 → 16 taken 14 times.
|
14 | connect(&m_refresh, &QTimer::timeout, this, [this] { |
| 48 | 2 | m_contentChanged = true; | |
| 49 | 2 | update(); | |
| 50 | 2 | }); | |
| 51 | 14 | } | |
| 52 | |||
| 53 | ✗ | QObject *GraphAreasItem::graph() const { return m_graph.data(); } | |
| 54 | |||
| 55 | 14 | void GraphAreasItem::setGraph(QObject *graph) { | |
| 56 |
1/1✓ Branch 2 → 3 taken 14 times.
|
14 | auto *typed = qobject_cast<QuickContactGraph *>(graph); |
| 57 |
1/2✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 14 times.
|
14 | if (typed == m_graph) |
| 58 | ✗ | return; | |
| 59 |
1/2✗ Branch 7 → 8 not taken.
✓ Branch 7 → 10 taken 14 times.
|
14 | if (m_graph) |
| 60 | ✗ | disconnect(m_graph, nullptr, this, nullptr); | |
| 61 |
1/1✓ Branch 10 → 11 taken 14 times.
|
14 | m_graph = typed; |
| 62 | 14 | m_prepared = false; | |
| 63 |
1/2✓ Branch 12 → 13 taken 14 times.
✗ Branch 12 → 20 not taken.
|
14 | if (m_graph) { |
| 64 |
1/1✓ Branch 14 → 15 taken 14 times.
|
14 | connect(m_graph, &QuickContactGraph::viewChanged, this, &QQuickItem::update); |
| 65 |
1/1✓ Branch 17 → 18 taken 14 times.
|
14 | connect(m_graph, &QuickContactGraph::territoriesChanged, this, [this] { |
| 66 | 33 | m_prepared = false; | |
| 67 | 33 | m_contentChanged = true; | |
| 68 | 33 | update(); | |
| 69 | 33 | }); | |
| 70 | } | ||
| 71 |
1/1✓ Branch 20 → 21 taken 14 times.
|
14 | emit changed(); |
| 72 | } | ||
| 73 | |||
| 74 | 48 | void GraphAreasItem::setColours(const QVariantList &values) { | |
| 75 | 48 | m_coloursIn = values; | |
| 76 | 48 | m_colours.clear(); | |
| 77 |
2/2✓ Branch 11 → 6 taken 35 times.
✓ Branch 11 → 12 taken 48 times.
|
83 | for (const QVariant &value : values) |
| 78 |
2/2✓ Branch 7 → 8 taken 35 times.
✓ Branch 8 → 9 taken 35 times.
|
35 | m_colours.append(value.value<QColor>()); |
| 79 | 48 | emit changed(); | |
| 80 | 48 | } | |
| 81 | |||
| 82 | 29 | void GraphAreasItem::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) { | |
| 83 | 29 | QQuickItem::geometryChange(newGeometry, oldGeometry); | |
| 84 | 29 | update(); | |
| 85 | 29 | } | |
| 86 | |||
| 87 | // Once per set of territories: simplified, rounded outlines, which side of | ||
| 88 | // each is inside (asked of the territory's own field), and the fill. | ||
| 89 | 32 | void GraphAreasItem::prepare() { | |
| 90 |
1/1✓ Branch 2 → 3 taken 32 times.
|
32 | m_shapes.clear(); |
| 91 | 32 | m_prepared = true; | |
| 92 |
1/2✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 32 times.
|
32 | if (!m_graph) |
| 93 | ✗ | return; | |
| 94 | 32 | QElapsedTimer timer; | |
| 95 | 32 | timer.start(); | |
| 96 |
2/2✓ Branch 119 → 11 taken 31 times.
✓ Branch 119 → 120 taken 32 times.
|
63 | for (const RelationGraphTerritories::Territory &territory : m_graph->territoryShapes()) { |
| 97 | 31 | Shape shape; | |
| 98 |
2/2✓ Branch 111 → 14 taken 32 times.
✓ Branch 111 → 112 taken 31 times.
|
63 | for (const QList<QPointF> &loop : territory.loops) { |
| 99 |
2/2✓ Branch 15 → 16 taken 32 times.
✓ Branch 16 → 17 taken 32 times.
|
32 | const QList<QPointF> smooth = smoothLoop(GraphGeometry::simplified(loop, 0.6)); |
| 100 |
1/2✗ Branch 19 → 20 not taken.
✓ Branch 19 → 21 taken 32 times.
|
32 | if (smooth.size() < 3) |
| 101 | ✗ | continue; | |
| 102 | // Most points decide (a narrow neck may sample the other side). | ||
| 103 | 32 | int inside = 0, samples = 0; | |
| 104 | 32 | const double probe = std::max(1.0, territory.field.step); | |
| 105 |
2/2✓ Branch 39 → 23 taken 893 times.
✓ Branch 39 → 40 taken 32 times.
|
925 | for (int i = 0; i < smooth.size(); i += std::max(1, int(smooth.size()) / 24)) { |
| 106 | 893 | const QPointF a = smooth.at(i), b = smooth.at((i + 1) % smooth.size()); | |
| 107 | 893 | const QPointF at = (a + b) / 2 + leftNormal(a, b) * probe; | |
| 108 |
2/3✓ Branch 31 → 32 taken 893 times.
✗ Branch 32 → 33 not taken.
✓ Branch 32 → 34 taken 893 times.
|
893 | inside += territory.field.at(at) > 0 ? 1 : 0; |
| 109 | 893 | ++samples; | |
| 110 | } | ||
| 111 |
1/2✗ Branch 40 → 41 not taken.
✓ Branch 40 → 42 taken 32 times.
|
32 | const double sign = 2 * inside >= samples ? 1 : -1; |
| 112 | // The glow along the inside never leaves the territory (the Canvas | ||
| 113 | // clipped it): per point the depth the field stays positive along the | ||
| 114 | // inward normal, up to the glow's 22 px at the strongest zoom-in. | ||
| 115 | 32 | QList<QPointF> normals; | |
| 116 | 32 | QList<double> depth; | |
| 117 | 32 | const int n = int(smooth.size()); | |
| 118 |
2/2✓ Branch 73 → 45 taken 3478 times.
✓ Branch 73 → 74 taken 32 times.
|
3510 | for (int i = 0; i < n; ++i) { |
| 119 | 3478 | QPointF normal = leftNormal(smooth.at((i - 1 + n) % n), smooth.at(i)) + leftNormal(smooth.at(i), smooth.at((i + 1) % n)); | |
| 120 | 3478 | const double length = std::hypot(normal.x(), normal.y()); | |
| 121 |
1/2✓ Branch 54 → 55 taken 3478 times.
✗ Branch 54 → 57 not taken.
|
3478 | normal = length > 1e-9 ? normal / length * sign : leftNormal(smooth.at(i), smooth.at((i + 1) % n)) * sign; |
| 122 | 3478 | double reach = 0; | |
| 123 |
2/2✓ Branch 69 → 62 taken 202194 times.
✓ Branch 69 → 70 taken 2945 times.
|
205139 | for (double t = probe / 2; t <= 120; t += probe / 2) { |
| 124 |
3/3✓ Branch 65 → 66 taken 202194 times.
✓ Branch 66 → 67 taken 533 times.
✓ Branch 66 → 68 taken 201661 times.
|
202194 | if (territory.field.at(smooth.at(i) + normal * t) <= 0) |
| 125 | 533 | break; | |
| 126 | 201661 | reach = t; | |
| 127 | } | ||
| 128 |
1/1✓ Branch 70 → 71 taken 3478 times.
|
3478 | normals.append(normal); |
| 129 |
1/1✓ Branch 71 → 72 taken 3478 times.
|
3478 | depth.append(reach); |
| 130 | } | ||
| 131 | // Neighbouring depths may differ a lot at a corner: the band's quads | ||
| 132 | // would fold over each other (a darker wedge). The smallest depth | ||
| 133 | // nearby, then the mean nearby, keeps them in order. | ||
| 134 |
2/2✓ Branch 74 → 75 taken 32 times.
✓ Branch 75 → 76 taken 32 times.
|
32 | QList<double> eroded(n), even(n); |
| 135 |
2/2✓ Branch 85 → 77 taken 3478 times.
✓ Branch 85 → 86 taken 32 times.
|
3510 | for (int i = 0; i < n; ++i) { |
| 136 | 3478 | double least = depth.at(i); | |
| 137 |
2/2✓ Branch 82 → 79 taken 31302 times.
✓ Branch 82 → 83 taken 3478 times.
|
34780 | for (int d = -4; d <= 4; ++d) |
| 138 | 31302 | least = std::min(least, depth.at((i + d + n) % n)); | |
| 139 |
1/1✓ Branch 83 → 84 taken 3478 times.
|
3478 | eroded[i] = least; |
| 140 | } | ||
| 141 |
2/2✓ Branch 93 → 87 taken 3478 times.
✓ Branch 93 → 94 taken 32 times.
|
3510 | for (int i = 0; i < n; ++i) { |
| 142 | 3478 | double sum = 0; | |
| 143 |
2/2✓ Branch 90 → 88 taken 17390 times.
✓ Branch 90 → 91 taken 3478 times.
|
20868 | for (int d = -2; d <= 2; ++d) |
| 144 | 17390 | sum += eroded.at((i + d + n) % n); | |
| 145 |
1/1✓ Branch 91 → 92 taken 3478 times.
|
3478 | even[i] = sum / 5; |
| 146 | } | ||
| 147 | 32 | depth = even; | |
| 148 |
1/1✓ Branch 95 → 96 taken 32 times.
|
32 | shape.loops.append(smooth); |
| 149 |
1/1✓ Branch 96 → 97 taken 32 times.
|
32 | shape.inward.append(sign); |
| 150 |
1/1✓ Branch 97 → 98 taken 32 times.
|
32 | shape.normals.append(normals); |
| 151 |
1/1✓ Branch 98 → 99 taken 32 times.
|
32 | shape.depth.append(depth); |
| 152 | 32 | } | |
| 153 |
1/1✓ Branch 112 → 113 taken 31 times.
|
31 | shape.fill = GraphGeometry::triangulate(shape.loops); |
| 154 |
1/1✓ Branch 115 → 116 taken 31 times.
|
31 | m_shapes.append(shape); |
| 155 | 31 | } | |
| 156 |
8/8✓ Branch 120 → 121 taken 32 times.
✓ Branch 124 → 125 taken 32 times.
✓ Branch 126 → 127 taken 32 times.
✓ Branch 127 → 128 taken 32 times.
✓ Branch 129 → 130 taken 32 times.
✓ Branch 130 → 131 taken 32 times.
✓ Branch 134 → 122 taken 32 times.
✓ Branch 134 → 135 taken 32 times.
|
64 | qCDebug(lcGraphAreas) << m_shapes.size() << "territories prepared in" << timer.elapsed() << "ms"; |
| 157 | } | ||
| 158 | |||
| 159 | // A faint tint, a glow along the inside of the border and a crisp border. | ||
| 160 | // `emphasis` 0 normal, 1 pointed at, 2 its name pointed at; `faded` while | ||
| 161 | // another family's name is pointed at. | ||
| 162 | 49 | void GraphAreasItem::paintTerritory(GraphGeometry::Sink *mesh, int index, int emphasis, bool faded) const { | |
| 163 | 49 | const Shape &shape = m_shapes.at(index); | |
| 164 |
2/4✓ Branch 4 → 5 taken 49 times.
✗ Branch 4 → 10 not taken.
✓ Branch 7 → 8 taken 49 times.
✗ Branch 7 → 10 not taken.
|
49 | const QColor colour = index < m_colours.size() && m_colours.at(index).isValid() ? m_colours.at(index) : QColor(Qt::gray); |
| 165 |
1/2✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 49 times.
|
49 | const double dim = faded ? 0.35 : 1; |
| 166 | 49 | QList<QPointF> fill; | |
| 167 |
1/1✓ Branch 15 → 16 taken 49 times.
|
49 | fill.reserve(shape.fill.size()); |
| 168 |
2/2✓ Branch 24 → 18 taken 17034 times.
✓ Branch 24 → 25 taken 49 times.
|
17083 | for (const QPointF &p : shape.fill) |
| 169 |
1/1✓ Branch 21 → 22 taken 17034 times.
|
17034 | fill.append(p * m_k + m_t); |
| 170 |
4/5✓ Branch 25 → 26 taken 49 times.
✗ Branch 25 → 30 not taken.
✓ Branch 26 → 27 taken 2 times.
✓ Branch 26 → 28 taken 47 times.
✓ Branch 31 → 32 taken 49 times.
|
49 | mesh->fill(fill, colour, (emphasis == 2 ? 0.16 : emphasis == 1 ? 0.12 : 0.075) * dim); |
| 171 | // The Canvas glow: six clipped strokes of 0.03 each, 44 → 4 px wide, so | ||
| 172 | // 0.18 at the border fading to nothing 22 px inside. | ||
| 173 |
2/2✓ Branch 32 → 33 taken 2 times.
✓ Branch 32 → 34 taken 47 times.
|
49 | const double glow = 0.18 * (emphasis > 0 ? 1.5 : 1) * dim; |
| 174 |
2/2✓ Branch 69 → 36 taken 50 times.
✓ Branch 69 → 70 taken 49 times.
|
99 | for (int l = 0; l < shape.loops.size(); ++l) { |
| 175 | 50 | const QList<QPointF> &loop = shape.loops.at(l); | |
| 176 | 50 | QList<QPointF> outer, inner; | |
| 177 | 50 | const int n = int(loop.size()); | |
| 178 |
1/1✓ Branch 38 → 39 taken 50 times.
|
50 | outer.reserve(n); |
| 179 |
1/1✓ Branch 39 → 40 taken 50 times.
|
50 | inner.reserve(n); |
| 180 |
2/2✓ Branch 54 → 41 taken 5778 times.
✓ Branch 54 → 55 taken 50 times.
|
5828 | for (int i = 0; i < n; ++i) { |
| 181 | 5778 | const QPointF at = loop.at(i) * m_k + m_t; | |
| 182 |
1/1✓ Branch 44 → 45 taken 5778 times.
|
5778 | outer.append(at); |
| 183 |
1/1✓ Branch 52 → 53 taken 5778 times.
|
5778 | inner.append(at + shape.normals.at(l).at(i) * std::min(22.0, shape.depth.at(l).at(i) * m_k)); |
| 184 | } | ||
| 185 |
1/1✓ Branch 55 → 56 taken 50 times.
|
50 | mesh->band(outer, inner, colour, glow, 0, true); |
| 186 |
5/5✓ Branch 56 → 57 taken 2 times.
✓ Branch 56 → 58 taken 48 times.
✓ Branch 60 → 61 taken 2 times.
✓ Branch 60 → 62 taken 48 times.
✓ Branch 63 → 64 taken 50 times.
|
50 | mesh->stroke(outer, emphasis > 0 ? 2.5 : 1.5, {}, colour, (emphasis > 0 ? 1 : 0.7) * dim, nullptr, true); |
| 187 | 50 | } | |
| 188 | 49 | } | |
| 189 | |||
| 190 | 285 | QSGNode *GraphAreasItem::updatePaintNode(QSGNode *old, UpdatePaintNodeData *) { | |
| 191 |
2/2✓ Branch 2 → 3 taken 32 times.
✓ Branch 2 → 4 taken 253 times.
|
285 | if (!m_prepared) |
| 192 |
1/1✓ Branch 3 → 4 taken 32 times.
|
32 | prepare(); |
| 193 |
1/2✓ Branch 5 → 6 taken 285 times.
✗ Branch 5 → 8 not taken.
|
285 | m_k = m_graph ? m_graph->viewScale() : 1; |
| 194 |
3/4✓ Branch 10 → 11 taken 285 times.
✗ Branch 10 → 16 not taken.
✓ Branch 12 → 13 taken 285 times.
✓ Branch 14 → 15 taken 285 times.
|
285 | m_t = m_graph ? QPointF(m_graph->viewX(), m_graph->viewY()) : QPointF(); |
| 195 | 285 | bool refresh = false; | |
| 196 | 285 | int vertices = m_vertexCount; | |
| 197 |
1/1✓ Branch 18 → 19 taken 285 times.
|
285 | QSGNode *node = m_cache.update(this, old, m_k, m_t, m_contentChanged, [this](GraphGeometry::Sink *sink) { |
| 198 |
1/2✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 41 times.
|
41 | if (!m_graph) |
| 199 | ✗ | return; | |
| 200 |
2/2✓ Branch 11 → 6 taken 49 times.
✓ Branch 11 → 12 taken 41 times.
|
90 | for (int a = 0; a < m_shapes.size(); ++a) |
| 201 |
3/4✓ Branch 6 → 7 taken 47 times.
✓ Branch 6 → 9 taken 2 times.
✓ Branch 7 → 8 taken 47 times.
✗ Branch 7 → 9 not taken.
|
49 | if (a != m_pointed && a != m_hovered) |
| 202 | 47 | paintTerritory(sink, a, 0, m_hovered >= 0); | |
| 203 | // The pointed territory last, so its border lies on top of a shared one. | ||
| 204 |
6/8✓ Branch 12 → 13 taken 2 times.
✓ Branch 12 → 17 taken 39 times.
✓ Branch 13 → 14 taken 2 times.
✗ Branch 13 → 17 not taken.
✓ Branch 15 → 16 taken 2 times.
✗ Branch 15 → 17 not taken.
✓ Branch 18 → 19 taken 2 times.
✓ Branch 18 → 20 taken 39 times.
|
41 | if (m_pointed >= 0 && m_pointed != m_hovered && m_pointed < m_shapes.size()) |
| 205 | 2 | paintTerritory(sink, m_pointed, 1, m_hovered >= 0); | |
| 206 |
2/6✗ Branch 20 → 21 not taken.
✓ Branch 20 → 24 taken 41 times.
✗ Branch 22 → 23 not taken.
✗ Branch 22 → 24 not taken.
✗ Branch 25 → 26 not taken.
✓ Branch 25 → 27 taken 41 times.
|
41 | if (m_hovered >= 0 && m_hovered < m_shapes.size()) |
| 207 | ✗ | paintTerritory(sink, m_hovered, 2, false); | |
| 208 | }, &vertices, &refresh); | ||
| 209 | 285 | m_contentChanged = false; | |
| 210 | 285 | m_refreshPending = refresh; | |
| 211 | 285 | m_vertexCount = vertices; | |
| 212 |
1/1✓ Branch 20 → 21 taken 285 times.
|
285 | QMetaObject::invokeMethod(this, [this, refresh] { |
| 213 |
2/2✓ Branch 2 → 3 taken 242 times.
✓ Branch 2 → 4 taken 42 times.
|
284 | if (refresh) |
| 214 | 242 | m_refresh.start(); | |
| 215 | 284 | emit built(); | |
| 216 | 284 | }, Qt::QueuedConnection); | |
| 217 | 285 | return node; | |
| 218 | } | ||
| 219 |