data/RelationGraphForce.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "RelationGraphForce.h" | ||
| 2 | |||
| 3 | #include <QCryptographicHash> | ||
| 4 | #include <algorithm> | ||
| 5 | #include <cmath> | ||
| 6 | #include <numeric> | ||
| 7 | #include <vector> | ||
| 8 | |||
| 9 | namespace RelationGraphForce { | ||
| 10 | |||
| 11 | 2 | QPointF seeded(const QString &key, double spread) { | |
| 12 |
2/2✓ Branch 2 → 3 taken 2 times.
✓ Branch 4 → 5 taken 2 times.
|
2 | const QByteArray hash = QCryptographicHash::hash(key.toUtf8(), QCryptographicHash::Sha1); |
| 13 | 2 | const quint32 a = quint32(quint8(hash[0])) << 8 | quint8(hash[1]); | |
| 14 | 2 | const quint32 b = quint32(quint8(hash[2])) << 8 | quint8(hash[3]); | |
| 15 | 2 | const double angle = 2 * M_PI * double(a) / 65535.0, radius = spread * std::sqrt(double(b) / 65535.0); | |
| 16 | 4 | return {radius * std::cos(angle), radius * std::sin(angle)}; | |
| 17 | 2 | } | |
| 18 | |||
| 19 | 1 | void layout(QList<QPointF> *positions, const QList<QPair<int, int>> &edges, const QList<bool> &pinned, | |
| 20 | double idealDistance, int iterations, double temperature, const QList<double> &radius) { | ||
| 21 | 1 | QList<QPointF> &pos = *positions; | |
| 22 | 1 | const int n = int(pos.size()); | |
| 23 |
2/4✓ Branch 3 → 4 taken 1 time.
✗ Branch 3 → 5 not taken.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 1 time.
|
1 | if (n <= 0 || iterations <= 0) |
| 24 | ✗ | return; | |
| 25 | 1 | const double k = idealDistance; | |
| 26 | 1 | const double cooling = temperature / (iterations + 1); | |
| 27 | 1 | const bool grid = n > 300; | |
| 28 | // A cell holds a typical disc (the median) and the reach of its forces. | ||
| 29 |
1/1✓ Branch 10 → 11 taken 1 time.
|
1 | std::vector<double> sorted(radius.cbegin(), radius.cend()); |
| 30 |
1/1✓ Branch 14 → 15 taken 1 time.
|
1 | std::sort(sorted.begin(), sorted.end()); |
| 31 |
1/2✓ Branch 16 → 17 taken 1 time.
✗ Branch 16 → 18 not taken.
|
1 | const double typical = sorted.empty() ? 0 : sorted[sorted.size() / 2]; |
| 32 | 1 | const double cell = 2.5 * k + 2 * typical; | |
| 33 | // The gap between two rims, never below a small positive distance. | ||
| 34 | 400 | const auto gap = [&radius](int i, int j, double centres) { | |
| 35 |
2/2✓ Branch 2 → 3 taken 400 times.
✓ Branch 3 → 4 taken 400 times.
|
400 | return std::max(centres - radius.value(i) - radius.value(j), 1.0); |
| 36 | 1 | }; | |
| 37 | 1 | std::vector<int> large; // discs too big for the grid's neighbourhood | |
| 38 |
2/2✓ Branch 26 → 22 taken 2 times.
✓ Branch 26 → 27 taken 1 time.
|
3 | for (int i = 0; i < n; ++i) |
| 39 |
2/3✓ Branch 22 → 23 taken 2 times.
✗ Branch 23 → 24 not taken.
✓ Branch 23 → 25 taken 2 times.
|
2 | if (radius.value(i) > cell / 2) |
| 40 | ✗ | large.push_back(i); | |
| 41 |
1/1✓ Branch 27 → 28 taken 1 time.
|
1 | QList<QPointF> displacement(n); |
| 42 |
2/2✓ Branch 194 → 29 taken 200 times.
✓ Branch 194 → 195 taken 1 time.
|
201 | for (int iteration = 0; iteration < iterations; ++iteration) { |
| 43 |
3/3✓ Branch 30 → 31 taken 200 times.
✓ Branch 31 → 32 taken 200 times.
✓ Branch 32 → 33 taken 200 times.
|
200 | std::fill(displacement.begin(), displacement.end(), QPointF()); |
| 44 | // Repulsion: all pairs, or the neighbouring cells of a flat uniform grid | ||
| 45 | // above 300 points (rebuilt per iteration over the current bounds). | ||
| 46 | 200 | double minX = 0, minY = 0; | |
| 47 | 200 | int columns = 1, rows = 1; | |
| 48 | 200 | std::vector<int> cellStart, cellItems, cellOf; | |
| 49 |
1/2✗ Branch 33 → 34 not taken.
✓ Branch 33 → 91 taken 200 times.
|
200 | if (grid) { |
| 50 | ✗ | double maxX = pos.at(0).x(), maxY = pos.at(0).y(); | |
| 51 | ✗ | minX = maxX; | |
| 52 | ✗ | minY = maxY; | |
| 53 | ✗ | for (const QPointF &p : std::as_const(pos)) { | |
| 54 | ✗ | minX = std::min(minX, p.x()); | |
| 55 | ✗ | minY = std::min(minY, p.y()); | |
| 56 | ✗ | maxX = std::max(maxX, p.x()); | |
| 57 | ✗ | maxY = std::max(maxY, p.y()); | |
| 58 | } | ||
| 59 | ✗ | columns = std::clamp(int((maxX - minX) / cell) + 1, 1, 4096); | |
| 60 | ✗ | rows = std::clamp(int((maxY - minY) / cell) + 1, 1, 4096); | |
| 61 | ✗ | cellOf.resize(size_t(n)); | |
| 62 | ✗ | cellStart.assign(size_t(columns) * rows + 1, 0); | |
| 63 | ✗ | for (int i = 0; i < n; ++i) { | |
| 64 | ✗ | const int cx = std::min(int((pos.at(i).x() - minX) / cell), columns - 1); | |
| 65 | ✗ | const int cy = std::min(int((pos.at(i).y() - minY) / cell), rows - 1); | |
| 66 | ✗ | cellOf[size_t(i)] = cy * columns + cx; | |
| 67 | ✗ | ++cellStart[size_t(cellOf[size_t(i)]) + 1]; | |
| 68 | } | ||
| 69 | ✗ | for (size_t c = 1; c < cellStart.size(); ++c) | |
| 70 | ✗ | cellStart[c] += cellStart[c - 1]; | |
| 71 | ✗ | cellItems.resize(size_t(n)); | |
| 72 | ✗ | std::vector<int> fill(cellStart.begin(), cellStart.end() - 1); | |
| 73 | ✗ | for (int i = 0; i < n; ++i) | |
| 74 | ✗ | cellItems[size_t(fill[size_t(cellOf[size_t(i)])]++)] = i; | |
| 75 | ✗ | } | |
| 76 |
2/2✓ Branch 133 → 92 taken 400 times.
✓ Branch 133 → 134 taken 200 times.
|
600 | for (int i = 0; i < n; ++i) { |
| 77 | 200 | const auto push = [&](int j) { | |
| 78 | 200 | double dx = pos.at(i).x() - pos.at(j).x(); | |
| 79 | 200 | double dy = pos.at(i).y() - pos.at(j).y(); | |
| 80 | 200 | double centres = std::hypot(dx, dy); | |
| 81 |
1/2✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 200 times.
|
200 | if (centres < 1e-2) { |
| 82 | ✗ | dx = 0.01 * (i - j); | |
| 83 | ✗ | dy = 0.01; | |
| 84 | ✗ | centres = std::hypot(dx, dy); | |
| 85 | } | ||
| 86 | // |force| = k² / gap along the unit vector. | ||
| 87 | 200 | const double scale = k * k / gap(i, j, centres) / centres; | |
| 88 |
1/1✓ Branch 14 → 15 taken 200 times.
|
200 | displacement[i] += QPointF(dx * scale, dy * scale); |
| 89 |
1/1✓ Branch 17 → 18 taken 200 times.
|
200 | displacement[j] -= QPointF(dx * scale, dy * scale); |
| 90 | 200 | }; | |
| 91 | 200 | const auto repel = [&](int j) { | |
| 92 | // Large discs meet everyone below; a pair of them once. | ||
| 93 |
4/12✓ Branch 2 → 3 taken 200 times.
✗ Branch 2 → 10 not taken.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 200 times.
✗ Branch 5 → 6 not taken.
✗ Branch 5 → 10 not taken.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 9 taken 200 times.
✗ Branch 8 → 9 not taken.
✗ Branch 8 → 10 not taken.
✓ Branch 11 → 12 taken 200 times.
✗ Branch 11 → 13 not taken.
|
200 | if (j > i && !(grid && radius.value(i) > cell / 2) && !(grid && radius.value(j) > cell / 2)) |
| 94 | 200 | push(j); | |
| 95 | 600 | }; | |
| 96 |
1/2✗ Branch 92 → 93 not taken.
✓ Branch 92 → 108 taken 400 times.
|
400 | if (grid) |
| 97 | ✗ | for (int j : large) | |
| 98 | ✗ | if (j != i && (radius.value(i) <= cell / 2 || j > i)) | |
| 99 | ✗ | push(j); | |
| 100 |
1/2✗ Branch 108 → 109 not taken.
✓ Branch 108 → 128 taken 400 times.
|
400 | if (grid) { |
| 101 | ✗ | const int cx = cellOf[size_t(i)] % columns; | |
| 102 | ✗ | const int cy = cellOf[size_t(i)] / columns; | |
| 103 | ✗ | for (int y = std::max(cy - 1, 0); y <= std::min(cy + 1, rows - 1); ++y) | |
| 104 | ✗ | for (int x = std::max(cx - 1, 0); x <= std::min(cx + 1, columns - 1); ++x) { | |
| 105 | ✗ | const size_t c = size_t(y) * columns + x; | |
| 106 | ✗ | for (int item = cellStart[c]; item < cellStart[c + 1]; ++item) | |
| 107 | ✗ | repel(cellItems[size_t(item)]); | |
| 108 | } | ||
| 109 | } else { | ||
| 110 |
2/2✓ Branch 131 → 129 taken 200 times.
✓ Branch 131 → 132 taken 400 times.
|
600 | for (int j = i + 1; j < n; ++j) |
| 111 |
1/1✓ Branch 129 → 130 taken 200 times.
|
200 | repel(j); |
| 112 | } | ||
| 113 | } | ||
| 114 | // Attraction along edges. | ||
| 115 |
2/2✓ Branch 161 → 136 taken 200 times.
✓ Branch 161 → 162 taken 200 times.
|
400 | for (const auto &[from, to] : edges) { |
| 116 |
5/10✓ Branch 139 → 140 taken 200 times.
✗ Branch 139 → 144 not taken.
✓ Branch 140 → 141 taken 200 times.
✗ Branch 140 → 144 not taken.
✓ Branch 141 → 142 taken 200 times.
✗ Branch 141 → 144 not taken.
✓ Branch 142 → 143 taken 200 times.
✗ Branch 142 → 144 not taken.
✗ Branch 143 → 144 not taken.
✓ Branch 143 → 145 taken 200 times.
|
200 | if (from < 0 || to < 0 || from >= n || to >= n || from == to) |
| 117 | ✗ | continue; | |
| 118 | 200 | const QPointF d = pos.at(from) - pos.at(to); | |
| 119 | 200 | const double dist = std::max(std::hypot(d.x(), d.y()), 0.01); | |
| 120 |
1/1✓ Branch 151 → 152 taken 200 times.
|
200 | const double rim = gap(from, to, dist); |
| 121 | 200 | const QPointF force = d / dist * (rim * rim / k); | |
| 122 |
1/1✓ Branch 154 → 155 taken 200 times.
|
200 | displacement[from] -= force; |
| 123 |
1/1✓ Branch 156 → 157 taken 200 times.
|
200 | displacement[to] += force; |
| 124 | } | ||
| 125 | // Gravity keeps separate components on screen. | ||
| 126 |
2/2✓ Branch 168 → 163 taken 400 times.
✓ Branch 168 → 169 taken 200 times.
|
600 | for (int i = 0; i < n; ++i) |
| 127 |
1/1✓ Branch 165 → 166 taken 400 times.
|
400 | displacement[i] -= pos.at(i) * 0.02; |
| 128 |
2/2✓ Branch 188 → 170 taken 400 times.
✓ Branch 188 → 189 taken 200 times.
|
600 | for (int i = 0; i < n; ++i) { |
| 129 | 400 | const double length = std::hypot(displacement.at(i).x(), displacement.at(i).y()); | |
| 130 |
4/7✓ Branch 174 → 175 taken 400 times.
✗ Branch 174 → 178 not taken.
✓ Branch 175 → 176 taken 400 times.
✓ Branch 176 → 177 taken 400 times.
✗ Branch 176 → 178 not taken.
✓ Branch 179 → 180 taken 400 times.
✗ Branch 179 → 187 not taken.
|
400 | if (length > 0 && !pinned.value(i)) |
| 131 |
1/1✓ Branch 184 → 185 taken 400 times.
|
400 | pos[i] += displacement.at(i) / length * std::min(length, temperature); |
| 132 | } | ||
| 133 | 200 | temperature = std::max(temperature - cooling, 0.5); | |
| 134 | 200 | } | |
| 135 | 1 | } | |
| 136 | |||
| 137 | 7 | void separate(QList<QPointF> *positions, const QList<QSizeF> &sizes, const QList<bool> &pinned, int passes) { | |
| 138 | 7 | QList<QPointF> &pos = *positions; | |
| 139 | 7 | const int n = int(pos.size()); | |
| 140 |
1/1✓ Branch 5 → 6 taken 7 times.
|
7 | std::vector<int> order(static_cast<size_t>(n)); |
| 141 |
1/2✓ Branch 88 → 8 taken 8 times.
✗ Branch 88 → 89 not taken.
|
8 | for (int pass = 0; pass < passes; ++pass) { |
| 142 | // Sweep by left edge: only pairs that overlap horizontally are compared. | ||
| 143 | 8 | std::iota(order.begin(), order.end(), 0); | |
| 144 |
1/1✓ Branch 13 → 14 taken 8 times.
|
8 | std::sort(order.begin(), order.end(), [&](int a, int b) { |
| 145 | 136 | const double la = pos.at(a).x() - sizes.at(a).width() / 2, lb = pos.at(b).x() - sizes.at(b).width() / 2; | |
| 146 |
1/2✓ Branch 10 → 11 taken 136 times.
✗ Branch 10 → 12 not taken.
|
136 | return la != lb ? la < lb : a < b; |
| 147 | }); | ||
| 148 | 8 | bool moved = false; | |
| 149 |
2/2✓ Branch 84 → 15 taken 37 times.
✓ Branch 84 → 85 taken 8 times.
|
45 | for (size_t oi = 0; oi < order.size(); ++oi) { |
| 150 | 37 | const int i = order[oi]; | |
| 151 |
2/2✓ Branch 81 → 17 taken 107 times.
✓ Branch 81 → 82 taken 15 times.
|
122 | for (size_t oj = oi + 1; oj < order.size(); ++oj) { |
| 152 | 107 | const int j = order[oj]; | |
| 153 | 107 | const double dx = pos.at(j).x() - pos.at(i).x(), dy = pos.at(j).y() - pos.at(i).y(); | |
| 154 | 107 | const double ox = (sizes.at(i).width() + sizes.at(j).width()) / 2 - std::abs(dx); | |
| 155 |
2/2✓ Branch 39 → 40 taken 22 times.
✓ Branch 39 → 41 taken 85 times.
|
107 | if (pos.at(j).x() - sizes.at(j).width() / 2 >= pos.at(i).x() + sizes.at(i).width() / 2) |
| 156 | 22 | break; // sorted by left edge: nobody further right overlaps i | |
| 157 | 85 | const double oy = (sizes.at(i).height() + sizes.at(j).height()) / 2 - std::abs(dy); | |
| 158 |
4/4✓ Branch 46 → 47 taken 84 times.
✓ Branch 46 → 48 taken 1 time.
✓ Branch 47 → 48 taken 83 times.
✓ Branch 47 → 49 taken 1 time.
|
85 | if (ox <= 0 || oy <= 0) |
| 159 | 84 | continue; | |
| 160 |
2/2✓ Branch 49 → 50 taken 1 time.
✓ Branch 50 → 51 taken 1 time.
|
1 | const bool pi = pinned.value(i), pj = pinned.value(j); |
| 161 |
1/4✗ Branch 51 → 52 not taken.
✓ Branch 51 → 54 taken 1 time.
✗ Branch 52 → 53 not taken.
✗ Branch 52 → 54 not taken.
|
1 | if (pi && pj) |
| 162 | ✗ | continue; | |
| 163 | // Along the smaller overlap; ties and coincident centres go right/down. | ||
| 164 |
2/6✓ Branch 54 → 55 taken 1 time.
✗ Branch 54 → 59 not taken.
✓ Branch 55 → 56 taken 1 time.
✗ Branch 55 → 57 not taken.
✗ Branch 59 → 60 not taken.
✗ Branch 59 → 61 not taken.
|
1 | QPointF step = ox < oy ? QPointF(dx >= 0 ? ox : -ox, 0) : QPointF(0, dy >= 0 ? oy : -oy); |
| 165 |
2/4✓ Branch 63 → 64 taken 1 time.
✗ Branch 63 → 72 not taken.
✓ Branch 64 → 65 taken 1 time.
✗ Branch 64 → 72 not taken.
|
1 | if (!pi && !pj) { |
| 166 |
1/1✓ Branch 66 → 67 taken 1 time.
|
1 | pos[i] -= step / 2; |
| 167 |
1/1✓ Branch 69 → 70 taken 1 time.
|
1 | pos[j] += step / 2; |
| 168 | ✗ | } else if (pi) { | |
| 169 | ✗ | pos[j] += step; | |
| 170 | } else { | ||
| 171 | ✗ | pos[i] -= step; | |
| 172 | } | ||
| 173 | 1 | moved = true; | |
| 174 | } | ||
| 175 | } | ||
| 176 |
2/2✓ Branch 85 → 86 taken 7 times.
✓ Branch 85 → 87 taken 1 time.
|
8 | if (!moved) |
| 177 | 7 | break; | |
| 178 | } | ||
| 179 | // What still overlaps (crowds where pushes cancel out) moves to the nearest | ||
| 180 | // free place on a spiral around it: pinned blocks first, then from the | ||
| 181 | // middle outwards, each against those already placed. | ||
| 182 | 50 | const auto overlap = [&](QPointF at, int i, int j) { | |
| 183 |
2/2✓ Branch 10 → 11 taken 11 times.
✓ Branch 10 → 21 taken 39 times.
|
61 | return std::abs(at.x() - pos.at(j).x()) < (sizes.at(i).width() + sizes.at(j).width()) / 2 - 0.01 && |
| 184 |
1/2✗ Branch 19 → 20 not taken.
✓ Branch 19 → 21 taken 11 times.
|
61 | std::abs(at.y() - pos.at(j).y()) < (sizes.at(i).height() + sizes.at(j).height()) / 2 - 0.01; |
| 185 | 7 | }; | |
| 186 | 7 | std::iota(order.begin(), order.end(), 0); | |
| 187 |
1/1✓ Branch 94 → 95 taken 7 times.
|
7 | std::sort(order.begin(), order.end(), [&](int a, int b) { |
| 188 |
2/2✓ Branch 4 → 5 taken 26 times.
✓ Branch 4 → 7 taken 137 times.
|
163 | if (pinned.value(a) != pinned.value(b)) |
| 189 | 26 | return pinned.value(a); | |
| 190 | 137 | const double da = std::hypot(pos.at(a).x(), pos.at(a).y()), db = std::hypot(pos.at(b).x(), pos.at(b).y()); | |
| 191 |
1/2✓ Branch 15 → 16 taken 137 times.
✗ Branch 15 → 17 not taken.
|
137 | return da != db ? da < db : a < b; |
| 192 | }); | ||
| 193 | 7 | std::vector<int> placed; | |
| 194 |
2/2✓ Branch 130 → 97 taken 35 times.
✓ Branch 130 → 131 taken 7 times.
|
42 | for (const int i : order) { |
| 195 | 4 | const auto isFree = [&](QPointF at) { | |
| 196 | 54 | return std::none_of(placed.cbegin(), placed.cend(), [&](int j) { return overlap(at, i, j); }); | |
| 197 | 35 | }; | |
| 198 |
6/8✓ Branch 98 → 99 taken 35 times.
✓ Branch 99 → 100 taken 4 times.
✓ Branch 99 → 104 taken 31 times.
✓ Branch 101 → 102 taken 4 times.
✗ Branch 102 → 103 not taken.
✓ Branch 102 → 104 taken 4 times.
✗ Branch 105 → 106 not taken.
✓ Branch 105 → 127 taken 35 times.
|
35 | if (!pinned.value(i) && !isFree(pos.at(i))) { |
| 199 | ✗ | const double step = std::min(sizes.at(i).width(), sizes.at(i).height()) / 4 + 10; | |
| 200 | ✗ | bool found = false; | |
| 201 | ✗ | for (int ring = 1; ring <= 400 && !found; ++ring) | |
| 202 | ✗ | for (int k = 0; k < 8 * ring && !found; ++k) { | |
| 203 | ✗ | const double angle = 2 * M_PI * k / (8 * ring); | |
| 204 | ✗ | const QPointF at = pos.at(i) + QPointF(std::cos(angle), std::sin(angle)) * (ring * step); | |
| 205 | ✗ | if (isFree(at)) { | |
| 206 | ✗ | pos[i] = at; | |
| 207 | ✗ | found = true; | |
| 208 | } | ||
| 209 | } | ||
| 210 | } | ||
| 211 |
1/1✓ Branch 127 → 128 taken 35 times.
|
35 | placed.push_back(i); |
| 212 | } | ||
| 213 | 7 | } | |
| 214 | |||
| 215 | } // namespace RelationGraphForce | ||
| 216 |