Physics Simulation & Visualization Tool 0.1
A C++ physics simulation engine with real-time 3D visualization
Loading...
Searching...
No Matches
FrameGraphCanvas.cpp
Go to the documentation of this file.
1#include <algorithm>
2#include <cmath>
3#include <limits>
4#include <thread>
5#include <QMouseEvent>
6#include <QPainter>
7#include <QPainterPath>
8#include <QPointer>
9#include <QToolTip>
10
11#include "FrameGraphCanvas.h"
12
13namespace {
14 constexpr int kMinCanvasHeight = 110;
15 constexpr float kLineWidth = 2.0f;
16 constexpr float kHoverLineWidth = 1.0f;
17 constexpr float kHoverPointRadius = 4.0f;
18 constexpr int kPlotMarginLeft = 8;
19 constexpr int kPlotMarginRight = 8;
20 constexpr int kPlotMarginTop = 6;
21 constexpr int kPlotMarginBottomOffset = 4;
22 constexpr int kLabelOffset = 2;
23 constexpr int kGridLines = 3;
24 constexpr int kGraphBucketsPerPixel = 2;
25
26 int nearestIndexByX(const std::vector<QPointF>& pts, qreal mx) {
27 if (pts.empty()) return -1;
28 const auto it = std::lower_bound(pts.begin(), pts.end(), mx,
29 [](const QPointF& p, qreal x) { return p.x() < x; });
30 if (it == pts.begin()) {
31 return 0;
32 }
33 if (it == pts.end()) {
34 return static_cast<int>(pts.size() - 1);
35 }
36 const int i1 = static_cast<int>(it - pts.begin());
37 const int i0 = i1 - 1;
38 const qreal d0 = std::abs(pts[static_cast<size_t>(i0)].x() - mx);
39 const qreal d1 = std::abs(pts[static_cast<size_t>(i1)].x() - mx);
40 return d0 <= d1 ? i0 : i1;
41 }
42}
43
45 : QWidget(parent) {
46 setMouseTracking(true);
47 setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
48 setMinimumHeight(kMinCanvasHeight);
49 resizeRebuildTimer.setSingleShot(true);
50 resizeRebuildTimer.setInterval(250);
51 connect(&resizeRebuildTimer, &QTimer::timeout, this, [this]() {
52 requestPointRebuild();
53 });
54}
55
56void FrameGraphCanvas::setSharedData(std::shared_ptr<const std::vector<ObjectSnapshot>> frames,
57 const std::array<std::pair<float, float>, kPlottableMetricCount>& valueMinMax, float tMinP, float tMaxP) {
58 framesData = (frames && !frames->empty()) ? std::move(frames) : nullptr;
59 if (framesData) {
60 valueMinMaxPerMetric = valueMinMax;
61 tMin = tMinP;
62 tMax = tMaxP;
63 } else {
64 valueMinMaxPerMetric = {};
65 tMin = 0.0f;
66 tMax = 0.0f;
67 }
68 requestPointRebuild();
69}
70
72 framesData.reset();
73 valueMinMaxPerMetric = {};
74 tMin = 0.0f;
75 tMax = 0.0f;
76 graphPoints.clear();
77 graphPointFrameIndices.clear();
78 hoverIndex = -1;
79 rebuildGeneration.fetch_add(1, std::memory_order_relaxed);
80 invalidateCache();
81 QToolTip::hideText();
82 update();
83}
84
86 currentMetric = metric;
87 requestPointRebuild();
88}
89
90void FrameGraphCanvas::paintEvent(QPaintEvent* event) {
91 QWidget::paintEvent(event);
92 if (cacheDirty || baseCache.isNull()) {
93 rebuildBaseCache();
94 }
95
96 QPainter painter(this);
97 painter.drawPixmap(0, 0, baseCache);
98
99 if (hoverIndex >= 0 && hoverIndex < static_cast<int>(graphPoints.size())) {
100 painter.setRenderHint(QPainter::Antialiasing, true);
101 const QPalette pal = palette();
102 const QRect rect = plotRect();
103 const QColor hoverColor = pal.color(QPalette::Highlight);
104 const QPointF point = graphPoints[hoverIndex];
105 painter.setPen(QPen(hoverColor, kHoverLineWidth, Qt::DashLine));
106 painter.drawLine(QPointF(point.x(), rect.top()), QPointF(point.x(), rect.bottom()));
107 painter.setBrush(hoverColor);
108 painter.setPen(Qt::NoPen);
109 painter.drawEllipse(point, kHoverPointRadius, kHoverPointRadius);
110 }
111}
112
113void FrameGraphCanvas::rebuildBaseCache() {
114 const qreal ratio = devicePixelRatioF();
115 baseCache = QPixmap(size() * ratio);
116 baseCache.setDevicePixelRatio(ratio);
117 baseCache.fill(Qt::transparent);
118
119 QPainter painter(&baseCache);
120 painter.setRenderHint(QPainter::Antialiasing, true);
121 const QRect rect = plotRect();
122 const QPalette pal = palette();
123 const QColor panelColor = pal.color(QPalette::Window);
124 const QColor plotColor = pal.color(QPalette::Base);
125 const QColor borderColor = pal.color(QPalette::Mid);
126 const QColor textColor = pal.color(QPalette::Text);
127 const QColor mutedText = pal.color(QPalette::Midlight);
128 const QColor lineColor = pal.color(QPalette::Highlight);
129 painter.fillRect(this->rect(), panelColor);
130 painter.fillRect(rect, plotColor);
131 painter.setPen(borderColor);
132 painter.drawRect(rect);
133 painter.setPen(mutedText);
134 for (int i = 1; i < kGridLines; ++i) {
135 const int y = rect.top() + (rect.height() * i) / kGridLines;
136 painter.drawLine(rect.left(), y, rect.right(), y);
137 }
138 painter.setPen(textColor);
139 painter.drawText(QRect(rect.left(), rect.bottom() + kLabelOffset, rect.width(), bottomLabelHeight()),
140 Qt::AlignRight | Qt::AlignVCenter,
141 tr("Time (s)"));
142 if (graphPoints.empty() || !framesData) {
143 painter.setPen(mutedText);
144 painter.drawText(rect, Qt::AlignCenter, tr("Select a simulated object to view its history."));
145 cacheDirty = false;
146 return;
147 }
148
149 QPainterPath path;
150 path.moveTo(graphPoints.front());
151 for (size_t i = 1; i < graphPoints.size(); ++i) {
152 path.lineTo(graphPoints[i]);
153 }
154 painter.setPen(QPen(lineColor, kLineWidth));
155 painter.drawPath(path);
156 cacheDirty = false;
157}
158
159void FrameGraphCanvas::mouseMoveEvent(QMouseEvent* event) {
160 const QRect rect = plotRect();
161 if (graphPoints.empty() || !rect.contains(event->position().toPoint())) {
162 if (hoverIndex != -1) {
163 hoverIndex = -1;
164 QToolTip::hideText();
165 update();
166 }
167 return;
168 }
169 const int nearestIndex = nearestIndexByX(graphPoints, event->position().x());
170 if (nearestIndex < 0) return;
171 if (nearestIndex == hoverIndex) return;
172
173 hoverIndex = nearestIndex;
174 const ObjectSnapshot& sample = (*framesData)[graphPointFrameIndices[static_cast<size_t>(nearestIndex)]];
175 const float value = objectSnapshotValue(currentMetric, sample);
176 QToolTip::showText(event->globalPosition().toPoint(),
177 tr("t=%1 s\n%2=%3")
178 .arg(sample.time, 0, 'f', 3)
179 .arg(metricLabel(currentMetric))
180 .arg(value, 0, 'f', 4),
181 this,
182 rect);
183 update();
184}
185
186void FrameGraphCanvas::leaveEvent(QEvent* event) {
187 QWidget::leaveEvent(event);
188 hoverIndex = -1;
189 QToolTip::hideText();
190 update();
191}
192
193void FrameGraphCanvas::resizeEvent(QResizeEvent* event) {
194 QWidget::resizeEvent(event);
195 hoverIndex = -1;
196 QToolTip::hideText();
197 resizeRebuildTimer.start();
198}
199
200int FrameGraphCanvas::bottomLabelHeight() const {
201 return fontMetrics().height() + kLabelOffset;
202}
203
204QRect FrameGraphCanvas::plotRect() const {
205 const int bottom = bottomLabelHeight() + kPlotMarginBottomOffset;
206 return rect().adjusted(kPlotMarginLeft, kPlotMarginTop, -kPlotMarginRight, -bottom);
207}
208
209void FrameGraphCanvas::requestPointRebuild() {
210 hoverIndex = -1;
211 const auto frames = framesData;
212 const uint64_t generation = rebuildGeneration.fetch_add(1, std::memory_order_relaxed) + 1;
213 const QRect rect = plotRect();
214 if (!frames || frames->empty() || rect.width() <= 1 || rect.height() <= 1) {
215 applyRebuiltPoints(generation, {}, {});
216 return;
217 }
218
219 const int m = static_cast<int>(currentMetric);
220 if (m < 0 || m >= static_cast<int>(kPlottableMetricCount)) {
221 applyRebuiltPoints(generation, {}, {});
222 return;
223 }
224
225 const auto valueRange = valueMinMaxPerMetric;
226 const float minTime = tMin;
227 const float maxTime = tMax;
228 const Metric metric = currentMetric;
229 const QPointer<FrameGraphCanvas> self(this);
230
231 std::thread([self, frames, valueRange, minTime, maxTime, metric, m, rect, generation]() {
232 std::vector<QPointF> points;
233 std::vector<size_t> frameIndices;
234
235 const float minValue = valueRange[static_cast<size_t>(m)].first;
236 const float maxValue = valueRange[static_cast<size_t>(m)].second;
237 const float invTime = maxTime > minTime ? 1.0f / (maxTime - minTime) : 0.0f;
238 const float invValue = maxValue > minValue ? 1.0f / (maxValue - minValue) : 0.0f;
239 struct Bucket {
240 bool used = false;
241 qreal x = 0.0;
242 qreal minY = std::numeric_limits<qreal>::max();
243 qreal maxY = std::numeric_limits<qreal>::lowest();
244 size_t minIndex = 0;
245 size_t maxIndex = 0;
246 };
247
248 const int bucketCount = std::max(1, rect.width() * kGraphBucketsPerPixel);
249 std::vector<Bucket> buckets(static_cast<size_t>(bucketCount));
250
251 for (size_t i = 0; i < frames->size(); ++i) {
252 const auto& frame = (*frames)[i];
253 const float timeAlpha = (frame.time - minTime) * invTime;
254 const float valueAlpha = (objectSnapshotValue(metric, frame) - minValue) * invValue;
255 if (!std::isfinite(timeAlpha) || !std::isfinite(valueAlpha)) continue;
256
257 const qreal x = rect.left() + static_cast<qreal>(timeAlpha) * rect.width();
258 const qreal y = rect.bottom() - static_cast<qreal>(valueAlpha) * rect.height();
259 if (!std::isfinite(x) || !std::isfinite(y)) continue;
260
261 const int bucketIndex = std::clamp(static_cast<int>(timeAlpha * static_cast<float>(bucketCount - 1)), 0, bucketCount - 1);
262 Bucket& bucket = buckets[static_cast<size_t>(bucketIndex)];
263 bucket.used = true;
264 bucket.x = x;
265 if (y < bucket.minY) {
266 bucket.minY = y;
267 bucket.minIndex = i;
268 }
269 if (y > bucket.maxY) {
270 bucket.maxY = y;
271 bucket.maxIndex = i;
272 }
273 }
274
275 points.reserve(static_cast<size_t>(bucketCount * 2));
276 frameIndices.reserve(points.capacity());
277
278 for (const Bucket& bucket : buckets) {
279 if (!bucket.used) continue;
280 if (bucket.minIndex <= bucket.maxIndex) {
281 points.emplace_back(bucket.x, bucket.minY);
282 frameIndices.push_back(bucket.minIndex);
283 if (bucket.maxY != bucket.minY) {
284 points.emplace_back(bucket.x, bucket.maxY);
285 frameIndices.push_back(bucket.maxIndex);
286 }
287 } else {
288 points.emplace_back(bucket.x, bucket.maxY);
289 frameIndices.push_back(bucket.maxIndex);
290 if (bucket.maxY != bucket.minY) {
291 points.emplace_back(bucket.x, bucket.minY);
292 frameIndices.push_back(bucket.minIndex);
293 }
294 }
295 }
296
297 if (!self) return;
298 QMetaObject::invokeMethod(self.data(), [self, generation, points = std::move(points), frameIndices = std::move(frameIndices)]() mutable {
299 if (!self) return;
300 self->applyRebuiltPoints(generation, std::move(points), std::move(frameIndices));
301 }, Qt::QueuedConnection);
302 }).detach();
303}
304
305void FrameGraphCanvas::invalidateCache() {
306 cacheDirty = true;
307}
308
309void FrameGraphCanvas::applyRebuiltPoints(uint64_t generation, std::vector<QPointF> points, std::vector<size_t> frameIndices) {
310 if (generation != rebuildGeneration.load(std::memory_order_relaxed)) return;
311 graphPoints = std::move(points);
312 graphPointFrameIndices = std::move(frameIndices);
313 hoverIndex = -1;
314 invalidateCache();
315 update();
316}
constexpr std::size_t kPlottableMetricCount
Definition Metric.h:21
QString metricLabel(Metric metric)
Definition Metric.h:23
Metric
Definition Metric.h:9
float objectSnapshotValue(Metric metric, const ObjectSnapshot &s)
Definition Metric.h:37
void mouseMoveEvent(QMouseEvent *event) override
void setSharedData(std::shared_ptr< const std::vector< ObjectSnapshot > > frames, const std::array< std::pair< float, float >, kPlottableMetricCount > &valueMinMax, float tMin, float tMax)
void resizeEvent(QResizeEvent *event) override
FrameGraphCanvas(QWidget *parent=nullptr)
void leaveEvent(QEvent *event) override
void setMetric(Metric metric)
void paintEvent(QPaintEvent *event) override