42#include <rhi/qshader.h>
49#include <QApplication>
50#include <QPropertyAnimation>
53#include <QResizeEvent>
72constexpr int kUboOffsetColor = 0;
73constexpr int kUboOffsetFirstSample = 16;
74constexpr int kUboOffsetScrollSample = 20;
75constexpr int kUboOffsetSampPerPixel = 24;
76constexpr int kUboOffsetViewWidth = 28;
77constexpr int kUboOffsetViewHeight = 32;
78constexpr int kUboOffsetChannelYCenter = 36;
79constexpr int kUboOffsetChannelYRange = 40;
80constexpr int kUboOffsetAmplitudeMax = 44;
81constexpr int kUboOffsetShowClipping = 48;
84constexpr int kMaxChannels = 1024;
88constexpr float kDefaultPrefetch = 1.0f;
95static QShader loadShader(
const QString &filename)
98 if (!f.open(QIODevice::ReadOnly)) {
99 qWarning() <<
"ChannelRhiView: cannot open shader" << filename;
102 return QShader::fromSerialized(f.readAll());
105static void writeFloat(quint8 *base,
int byteOffset,
float v)
107 memcpy(base + byteOffset, &v,
sizeof(
float));
110static void writeFloats(quint8 *base,
int byteOffset,
const float *data,
int count)
112 memcpy(base + byteOffset, data, count *
sizeof(
float));
129 : QWidget(parent), m_view(parent)
131 setAttribute(Qt::WA_TransparentForMouseEvents);
132 setAttribute(Qt::WA_NoSystemBackground);
133 setAttribute(Qt::WA_TranslucentBackground);
134 setMouseTracking(
false);
137 void syncSize() { setGeometry(0, 0, parentWidget()->width(), parentWidget()->height()); }
144 p.setRenderHint(QPainter::Antialiasing,
false);
146 if (m_view->crosshairEnabled())
147 m_view->drawCrosshair(p);
148 if (m_view->scalebarsVisible())
149 m_view->drawScalebars(p);
150 if (m_view->rulerActive())
151 m_view->drawRulerOverlay(p);
152 if (m_view->annotationSelecting())
153 m_view->drawAnnotationSelectionOverlay(p);
163 setFocusPolicy(Qt::StrongFocus);
164 setMouseTracking(
true);
165 setContextMenuPolicy(Qt::PreventContextMenu);
172 connect(&m_tileWatcher, &QFutureWatcher<TileResult>::finished,
this, [
this]() {
173 m_tileRebuildPending =
false;
175 bool dirtiedDuringBuild = m_tileDirty;
176 bool tileAccepted =
false;
178 if (!m_tileWatcher.isCanceled()) {
179 TileResult r = m_tileWatcher.result();
180 if (!r.image.isNull()) {
181 m_tileImage = std::move(r.image);
182 m_tileSampleFirst = r.sampleFirst;
183 m_tileSamplesPerPixel = r.samplesPerPixel;
184 m_tileFirstChannel = r.firstChannel;
185 m_tileVisibleCount = r.visibleCount;
195 if (tileAccepted || dirtiedDuringBuild) {
196 if (dirtiedDuringBuild)
203# if defined(WASMBUILD) || defined(__EMSCRIPTEN__)
204 setApi(QRhiWidget::Api::OpenGL);
205# elif defined(Q_OS_MACOS) || defined(Q_OS_IOS)
206 setApi(QRhiWidget::Api::Metal);
207# elif defined(Q_OS_WIN)
208 setApi(QRhiWidget::Api::Direct3D11);
210 setApi(QRhiWidget::Api::OpenGL);
215 setAttribute(Qt::WA_NativeWindow);
219 connect(qApp, &QApplication::applicationStateChanged,
220 this, [
this](Qt::ApplicationState s) {
221 if (s == Qt::ApplicationActive)
234 if (m_model == model)
249 m_pipelineDirty =
true;
255 m_pipelineDirty =
true;
264 if (m_model && m_model->totalSamples() > 0)
265 sample = qMax(sample,
static_cast<float>(m_model->firstSample()));
267 sample = qMax(sample, 0.f);
270 if (m_lastFileSample >= 0) {
272 maxScroll = qMax(maxScroll,
static_cast<float>(m_firstFileSample));
273 sample = qMin(sample, maxScroll);
276 if (qFuzzyCompare(m_scrollSample, sample))
279 m_scrollSample = sample;
285 if (!m_tileImage.isNull() && m_tileSamplesPerPixel > 0.f) {
286 float vis = width() * m_samplesPerPixel;
287 float tileEnd = m_tileSampleFirst + m_tileImage.width() * m_tileSamplesPerPixel;
288 if (m_scrollSample < m_tileSampleFirst + vis ||
289 m_scrollSample + vis > tileEnd - vis)
294 float visible = width() * m_samplesPerPixel;
295 float margin = m_prefetchFactor * visible;
296 if (sample < m_vboWindowFirst + margin ||
297 sample + visible > m_vboWindowLast - margin) {
304 if (m_overlayTotalSamples <= 0.f ||
305 sample < m_overlayFirstSample ||
306 sample + visible > m_overlayFirstSample + m_overlayTotalSamples) {
307 m_overlayDirty =
true;
318 spp = qMax(spp, 1e-4f);
319 if (qFuzzyCompare(m_samplesPerPixel, spp))
321 m_samplesPerPixel = spp;
323 m_overlayDirty =
true;
333 if (durationMs <= 0) {
337 auto *anim =
new QPropertyAnimation(
this,
"scrollSample",
this);
338 anim->setDuration(durationMs);
339 anim->setEasingCurve(QEasingCurve::OutCubic);
340 anim->setStartValue(m_scrollSample);
341 anim->setEndValue(targetSample);
342 anim->start(QAbstractAnimation::DeleteWhenStopped);
349 targetSpp = qMax(targetSpp, 1e-4f);
350 if (durationMs <= 0) {
354 auto *anim =
new QPropertyAnimation(
this,
"samplesPerPixel",
this);
355 anim->setDuration(durationMs);
356 anim->setEasingCurve(QEasingCurve::OutCubic);
357 anim->setStartValue(m_samplesPerPixel);
358 anim->setEndValue(targetSpp);
359 anim->start(QAbstractAnimation::DeleteWhenStopped);
368 m_overlayDirty =
true;
376 m_prefetchFactor = qMax(factor, 0.1f);
383 return static_cast<int>(m_scrollSample);
390 return static_cast<int>(width() * m_samplesPerPixel);
398 ch = qBound(0, ch, maxFirst);
399 if (ch == m_firstVisibleChannel)
401 m_firstVisibleChannel = ch;
404 m_pipelineDirty =
true;
413 count = qMax(1, count);
414 if (count == m_visibleChannelCount)
416 m_visibleChannelCount = count;
419 m_pipelineDirty =
true;
428 if (m_frozen && m_pInertialAnim) {
429 m_pInertialAnim->stop();
430 m_pInertialAnim =
nullptr;
438 if (visible == m_gridVisible)
440 m_gridVisible = visible;
442 m_overlayDirty =
true;
450 m_sfreq = qMax(sfreq, 0.f);
452 m_overlayDirty =
true;
460 if (first == m_firstFileSample)
462 m_firstFileSample = first;
464 m_overlayDirty =
true;
472 m_lastFileSample = last;
479 if (m_hideBadChannels == hide)
482 const int previousFirstVisibleChannel = m_firstVisibleChannel;
483 m_hideBadChannels = hide;
485 m_firstVisibleChannel = qBound(0, m_firstVisibleChannel, maxFirst);
486 if (m_firstVisibleChannel != previousFirstVisibleChannel) {
490 m_pipelineDirty =
true;
499 m_wheelScrollsChannels = channelsMode;
506 m_scrollSpeedFactor = qBound(0.25f, factor, 4.0f);
513 if (m_crosshairEnabled == enabled)
515 m_crosshairEnabled = enabled;
517 setMouseTracking(
true);
519 setMouseTracking(
false);
520 m_crosshairX = m_crosshairY = -1;
529 if (m_scalebarsVisible == visible)
531 m_scalebarsVisible = visible;
539 if (m_butterflyMode == enabled)
541 m_butterflyMode = enabled;
543 m_pipelineDirty =
true;
545 m_overlayDirty =
true;
551QVector<ChannelRhiView::ButterflyTypeGroup> ChannelRhiView::butterflyTypeGroups()
const
553 QVector<ButterflyTypeGroup> groups;
557 const QVector<int> allCh = effectiveChannelIndices();
558 QMap<QString, int> typeToGroup;
560 for (
int ch : allCh) {
561 auto info = m_model->channelInfo(ch);
562 if (m_hideBadChannels && info.bad)
565 if (typeToGroup.contains(info.typeLabel)) {
566 gIdx = typeToGroup[info.typeLabel];
568 gIdx = groups.size();
569 typeToGroup[info.typeLabel] = gIdx;
570 ButterflyTypeGroup g;
571 g.typeLabel = info.typeLabel;
572 g.color = info.color;
573 g.amplitudeMax = info.amplitudeMax;
576 groups[gIdx].channelIndices.append(ch);
583int ChannelRhiView::butterflyLaneCount()
const
587 const QVector<int> allCh = effectiveChannelIndices();
589 for (
int ch : allCh) {
590 auto info = m_model->channelInfo(ch);
591 if (m_hideBadChannels && info.bad)
593 types.insert(info.typeLabel);
602 const int previousFirstVisibleChannel = m_firstVisibleChannel;
603 m_filteredChannels = indices;
606 m_firstVisibleChannel = qBound(0, m_firstVisibleChannel, maxFirst);
607 if (m_firstVisibleChannel != previousFirstVisibleChannel) {
611 m_pipelineDirty =
true;
620 return effectiveChannelIndices().size();
625int ChannelRhiView::actualChannelAt(
int logicalIdx)
const
627 const QVector<int> indices = effectiveChannelIndices();
628 if (logicalIdx < 0 || logicalIdx >= indices.size())
630 return indices.at(logicalIdx);
635QVector<int> ChannelRhiView::effectiveChannelIndices()
const
637 QVector<int> indices;
643 if (m_filteredChannels.isEmpty()) {
644 indices.reserve(m_model->channelCount());
645 for (
int channelIndex = 0; channelIndex < m_model->channelCount(); ++channelIndex) {
646 indices.append(channelIndex);
649 indices = m_filteredChannels;
652 if (!m_hideBadChannels) {
656 QVector<int> visibleIndices;
657 visibleIndices.reserve(indices.size());
658 for (
int channelIndex : std::as_const(indices)) {
659 if (channelIndex < 0) {
663 const ChannelDisplayInfo info = m_model->channelInfo(channelIndex);
665 visibleIndices.append(channelIndex);
669 return visibleIndices;
678 m_overlayDirty =
true;
686 m_epochTriggerSamples = triggerSamples;
688 m_overlayDirty =
true;
696 if (m_bShowEpochMarkers == visible)
698 m_bShowEpochMarkers = visible;
700 m_overlayDirty =
true;
708 if (m_bShowClipping == visible)
710 m_bShowClipping = visible;
719 if (m_bZScoreMode == enabled)
721 m_bZScoreMode = enabled;
731 m_annotations = annotations;
733 m_overlayDirty =
true;
741 m_annotationSelectionEnabled = enabled;
748 if (m_bShowEvents == visible)
return;
749 m_bShowEvents = visible;
751 m_overlayDirty =
true;
761 if (m_bShowAnnotations == visible)
return;
762 m_bShowAnnotations = visible;
764 m_overlayDirty =
true;
772int ChannelRhiView::hitTestAnnotationBoundary(
int px,
bool &isStart)
const
774 for (
int i = 0; i < m_annotations.size(); ++i) {
775 const float xStart = (
static_cast<float>(m_annotations[i].startSample) - m_scrollSample) / m_samplesPerPixel;
776 const float xEnd = (
static_cast<float>(m_annotations[i].endSample + 1) - m_scrollSample) / m_samplesPerPixel;
778 if (qAbs(px -
static_cast<int>(xStart)) <= kAnnBoundaryHitPx) {
782 if (qAbs(px -
static_cast<int>(xEnd)) <= kAnnBoundaryHitPx) {
794 m_pipelineDirty =
true;
805 m_gpuChannels.clear();
806 m_pipelineDirty =
true;
809 m_overlayPipeline.reset();
810 m_overlaySrb.reset();
811 m_overlaySampler.reset();
812 m_overlayTex.reset();
813 m_overlayVbo.reset();
814 m_overlayDirty =
true;
819void ChannelRhiView::ensurePipeline()
821 if (!m_pipelineDirty)
824 QRhi *rhi = this->rhi();
828 m_uboStride =
static_cast<int>(
829 (52 + rhi->ubufAlignment() - 1) & ~(rhi->ubufAlignment() - 1));
835 if (m_butterflyMode) {
836 nCh = qMin(totalCh, kMaxChannels);
838 nCh = qMin(m_visibleChannelCount, totalCh - m_firstVisibleChannel);
841 nCh = qMin(nCh, kMaxChannels);
844 bool uboRecreated =
false;
845 if (!m_ubo || m_ubo->size() < nCh * m_uboStride) {
846 m_ubo.reset(rhi->newBuffer(QRhiBuffer::Dynamic,
847 QRhiBuffer::UniformBuffer,
855 if (!m_srb || uboRecreated) {
856 m_srb.reset(rhi->newShaderResourceBindings());
858 QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(
860 QRhiShaderResourceBinding::VertexStage |
861 QRhiShaderResourceBinding::FragmentStage,
871 QShader vs = loadShader(QStringLiteral(
":/disp/shaders/viewers/helpers/shaders/channeldata.vert.qsb"));
872 QShader fs = loadShader(QStringLiteral(
":/disp/shaders/viewers/helpers/shaders/channeldata.frag.qsb"));
874 if (!vs.isValid() || !fs.isValid()) {
875 qWarning() <<
"ChannelRhiView: shaders not found. "
876 "Ensure qt_add_shaders is configured in CMakeLists.";
883 m_pipeline.reset(rhi->newGraphicsPipeline());
884 m_pipeline->setShaderStages({
885 { QRhiShaderStage::Vertex, vs },
886 { QRhiShaderStage::Fragment, fs }
889 QRhiVertexInputLayout il;
890 il.setBindings({{ 2 *
sizeof(float) }});
891 il.setAttributes({{ 0, 0, QRhiVertexInputAttribute::Float2, 0 }});
893 m_pipeline->setVertexInputLayout(il);
894 m_pipeline->setShaderResourceBindings(m_srb.get());
895 m_pipeline->setRenderPassDescriptor(renderTarget()->renderPassDescriptor());
896 m_pipeline->setTopology(QRhiGraphicsPipeline::LineStrip);
897 m_pipeline->setDepthTest(
false);
898 m_pipeline->setDepthWrite(
false);
901 QRhiGraphicsPipeline::TargetBlend blend;
903 blend.srcColor = QRhiGraphicsPipeline::SrcAlpha;
904 blend.dstColor = QRhiGraphicsPipeline::OneMinusSrcAlpha;
905 blend.srcAlpha = QRhiGraphicsPipeline::One;
906 blend.dstAlpha = QRhiGraphicsPipeline::OneMinusSrcAlpha;
907 m_pipeline->setTargetBlends({ blend });
909 if (!m_pipeline->create()) {
910 qWarning() <<
"ChannelRhiView: failed to create graphics pipeline";
915 m_pipelineDirty =
false;
920bool ChannelRhiView::isVboDirty()
const
924 float visible = width() * m_samplesPerPixel;
925 float margin = m_prefetchFactor * visible;
927 || m_scrollSample < m_vboWindowFirst + margin
928 || (m_scrollSample + visible) > m_vboWindowLast - margin;
933void ChannelRhiView::rebuildVBOs(QRhiResourceUpdateBatch *batch)
938 QRhi *rhi = this->rhi();
944 float visible = px * m_samplesPerPixel;
947 float windowFirst = m_scrollSample - m_prefetchFactor * visible;
948 float windowLast = m_scrollSample + (1.f + m_prefetchFactor) * visible;
950 int iFirst = qMax(
static_cast<int>(windowFirst), m_model->firstSample());
951 int iLast = qMin(
static_cast<int>(windowLast),
952 m_model->firstSample() + m_model->totalSamples());
953 if (iFirst >= iLast) {
958 m_vboWindowFirst = iFirst;
959 m_vboWindowLast = iLast;
962 m_gpuChannels.resize(nCh);
965 int prefetchedSamples = iLast - iFirst;
968 int maxVertices = qMax(prefetchedSamples * 2, 2 * px * 4);
969 Q_UNUSED(maxVertices)
971 for (
int logCh = 0; logCh < nCh; ++logCh) {
972 int ch = actualChannelAt(logCh);
974 m_gpuChannels[logCh].vertexCount = 0;
978 QVector<float> verts = m_model->decimatedVertices(
979 ch, iFirst, iLast,
static_cast<int>(prefetchedSamples / m_samplesPerPixel), vboFirst);
981 if (verts.isEmpty()) {
982 m_gpuChannels[logCh].vertexCount = 0;
988 int nv = verts.size() / 2;
990 double sum = 0.0, sumSq = 0.0;
991 for (
int v = 0; v < nv; ++v) {
992 double a =
static_cast<double>(verts[v * 2 + 1]);
996 float mean =
static_cast<float>(sum / nv);
997 double var = sumSq / nv -
static_cast<double>(mean) * mean;
998 float sd = var > 0.0 ?
static_cast<float>(qSqrt(var)) : 1.f;
999 for (
int v = 0; v < nv; ++v)
1000 verts[v * 2 + 1] = (verts[v * 2 + 1] - mean) / sd;
1004 int vertexCount = verts.size() / 2;
1005 quint32 byteSize =
static_cast<quint32
>(verts.size() *
sizeof(
float));
1007 auto &gd = m_gpuChannels[logCh];
1010 if (!gd.vbo ||
static_cast<quint32
>(gd.vbo->size()) < byteSize) {
1011 gd.vbo.reset(rhi->newBuffer(QRhiBuffer::Dynamic,
1012 QRhiBuffer::VertexBuffer,
1014 if (!gd.vbo->create()) {
1015 qWarning() <<
"ChannelRhiView: VBO create failed for channel" << logCh;
1021 batch->updateDynamicBuffer(gd.vbo.get(), 0, byteSize,
1023 gd.vertexCount = vertexCount;
1024 gd.vboFirstSample = vboFirst;
1032void ChannelRhiView::updateUBO(QRhiResourceUpdateBatch *batch)
1034 if (!m_model || !m_ubo)
1040 if (m_butterflyMode) {
1041 const auto groups = butterflyTypeGroups();
1042 int nLanes = groups.size();
1047 QHash<int, int> chToLane;
1048 for (
int g = 0; g < groups.size(); ++g)
1049 for (
int ch : groups[g].channelIndices)
1052 float vw =
static_cast<float>(width());
1053 float vh =
static_cast<float>(height());
1054 float laneRange = 2.f / nLanes;
1056 QVarLengthArray<quint8> buf(m_uboStride, 0);
1057 int nToUpload = qMin(totalCh, kMaxChannels);
1059 for (
int logCh = 0; logCh < nToUpload; ++logCh) {
1060 int ch = actualChannelAt(logCh);
1061 memset(buf.data(), 0, m_uboStride);
1063 auto info = (ch >= 0) ? m_model->channelInfo(ch) : ChannelDisplayInfo{};
1064 bool hideThis = (ch < 0) || (m_hideBadChannels && info.
bad);
1066 int lane = chToLane.value(ch, -1);
1070 QColor col = hideThis ? m_bgColor
1071 : (info.
bad ? QColor(200, 60, 60, 180) : info.color);
1072 float yRng = hideThis ? 0.f : laneRange;
1074 float yCenter = (lane >= 0)
1075 ? (1.f - laneRange * (lane + 0.5f))
1079 static_cast<float>(col.redF()),
1080 static_cast<float>(col.greenF()),
1081 static_cast<float>(col.blueF()),
1082 static_cast<float>(col.alphaF())
1085 auto *d = buf.data();
1086 writeFloats(d, kUboOffsetColor, rgba, 4);
1087 writeFloat (d, kUboOffsetFirstSample,
static_cast<float>(logCh <
static_cast<int>(m_gpuChannels.size())
1088 ? m_gpuChannels[logCh].vboFirstSample : 0));
1089 writeFloat (d, kUboOffsetScrollSample, m_scrollSample);
1090 writeFloat (d, kUboOffsetSampPerPixel, m_samplesPerPixel);
1091 writeFloat (d, kUboOffsetViewWidth, vw);
1092 writeFloat (d, kUboOffsetViewHeight, vh);
1093 writeFloat (d, kUboOffsetChannelYCenter, yCenter);
1094 writeFloat (d, kUboOffsetChannelYRange, yRng);
1095 writeFloat (d, kUboOffsetAmplitudeMax, m_bZScoreMode ? 4.f : info.
amplitudeMax);
1096 writeFloat (d, kUboOffsetShowClipping, (m_bShowClipping && !info.
bad && !m_bZScoreMode) ? 1.f : 0.f);
1098 batch->updateDynamicBuffer(m_ubo.get(),
1099 logCh * m_uboStride,
1107 int firstCh = qBound(0, m_firstVisibleChannel, totalCh);
1108 int visCnt = qMin(m_visibleChannelCount, totalCh - firstCh);
1109 int nCh = qMin(visCnt, kMaxChannels);
1113 float vw =
static_cast<float>(width());
1114 float vh =
static_cast<float>(height());
1115 float laneRange = 2.f / nCh;
1117 QVarLengthArray<quint8> buf(m_uboStride, 0);
1119 for (
int i = 0; i < nCh; ++i) {
1120 int logCh = firstCh + i;
1121 int ch = actualChannelAt(logCh);
1122 memset(buf.data(), 0, m_uboStride);
1124 auto info = (ch >= 0) ? m_model->channelInfo(ch) : ChannelDisplayInfo{};
1125 bool hideThis = (ch < 0) || (m_hideBadChannels && info.
bad);
1127 QColor col = hideThis ? m_bgColor
1128 : (info.
bad ? QColor(200, 60, 60, 180) : info.color);
1130 float yRng = hideThis ? 0.f : laneRange;
1133 static_cast<float>(col.redF()),
1134 static_cast<float>(col.greenF()),
1135 static_cast<float>(col.blueF()),
1136 static_cast<float>(col.alphaF())
1140 float yCenter = 1.f - laneRange * (i + 0.5f);
1142 auto *d = buf.data();
1143 writeFloats(d, kUboOffsetColor, rgba, 4);
1145 writeFloat (d, kUboOffsetFirstSample,
static_cast<float>(logCh <
static_cast<int>(m_gpuChannels.size())
1146 ? m_gpuChannels[logCh].vboFirstSample : 0));
1147 writeFloat (d, kUboOffsetScrollSample, m_scrollSample);
1148 writeFloat (d, kUboOffsetSampPerPixel, m_samplesPerPixel);
1149 writeFloat (d, kUboOffsetViewWidth, vw);
1150 writeFloat (d, kUboOffsetViewHeight, vh);
1151 writeFloat (d, kUboOffsetChannelYCenter, yCenter);
1152 writeFloat (d, kUboOffsetChannelYRange, yRng);
1153 writeFloat (d, kUboOffsetAmplitudeMax, m_bZScoreMode ? 4.f : info.
amplitudeMax);
1154 writeFloat (d, kUboOffsetShowClipping, (m_bShowClipping && !info.
bad && !m_bZScoreMode) ? 1.f : 0.f);
1157 batch->updateDynamicBuffer(m_ubo.get(),
1175void ChannelRhiView::rebuildOverlayImage(
int logicalWidth,
int logicalHeight, qreal devicePixelRatio)
1177 const qreal dpr = qMax(devicePixelRatio, 1.0);
1178 const int pixelWidth = qMax(1, qRound(logicalWidth * dpr));
1179 const int pixelHeight = qMax(1, qRound(logicalHeight * dpr));
1181 m_overlayImage = QImage(pixelWidth, pixelHeight, QImage::Format_RGBA8888);
1182 m_overlayImage.setDevicePixelRatio(dpr);
1183 m_overlayImage.fill(Qt::transparent);
1185 if (logicalWidth <= 0 || logicalHeight <= 0 || m_sfreq <= 0.f || m_samplesPerPixel <= 0.f) {
1186 m_overlayDirty =
false;
1192 const float overlayFirst = m_overlayFirstSample;
1193 const float overlayTotal = m_overlayTotalSamples;
1194 const float overlayPixelsPerSample = (overlayTotal > 0.f)
1195 ?
static_cast<float>(logicalWidth) / overlayTotal
1198 QPainter p(&m_overlayImage);
1199 p.setCompositionMode(QPainter::CompositionMode_SourceOver);
1205 if (m_bShowAnnotations && !m_annotations.isEmpty()) {
1206 QFont font = p.font();
1207 font.setPointSizeF(8.0);
1212 const float xStart = (
static_cast<float>(annotation.startSample) - overlayFirst) * overlayPixelsPerSample;
1213 const float xEnd = (
static_cast<float>(annotation.endSample + 1) - overlayFirst) * overlayPixelsPerSample;
1214 if (xEnd < -2.f || xStart > logicalWidth + 2.f) {
1218 const float clippedStart = qBound(0.f, xStart,
static_cast<float>(logicalWidth));
1219 const float clippedEnd = qBound(0.f, xEnd,
static_cast<float>(logicalWidth));
1220 if (clippedEnd <= clippedStart) {
1224 QColor fillColor = annotation.color;
1225 fillColor.setAlpha(48);
1226 p.fillRect(QRectF(clippedStart, 0.f, clippedEnd - clippedStart,
static_cast<float>(logicalHeight)),
1229 QColor borderColor = annotation.color;
1230 borderColor.setAlpha(165);
1231 p.setPen(QPen(borderColor, 1));
1232 p.drawLine(QPointF(clippedStart, 0.f), QPointF(clippedStart,
static_cast<float>(logicalHeight)));
1233 p.drawLine(QPointF(clippedEnd, 0.f), QPointF(clippedEnd,
static_cast<float>(logicalHeight)));
1235 if (!annotation.label.trimmed().isEmpty()) {
1236 QString label = annotation.label.trimmed();
1237 QFontMetrics metrics(font);
1238 QRect labelRect = metrics.boundingRect(label);
1239 labelRect.adjust(-6, -2, 6, 2);
1240 const int labelX = qBound(4,
1241 static_cast<int>(clippedStart) + 4,
1242 qMax(4, logicalWidth - labelRect.width() - 4));
1243 labelRect.moveTopLeft(QPoint(labelX, 4));
1244 QColor pillColor = annotation.color;
1245 pillColor.setAlpha(215);
1246 p.fillRect(labelRect, pillColor);
1247 p.setPen(Qt::white);
1248 p.drawText(labelRect, Qt::AlignCenter, label);
1254 if (m_bShowEvents && !m_events.isEmpty()) {
1256 float xF = (
static_cast<float>(ev.sample) - overlayFirst) * overlayPixelsPerSample;
1257 if (xF < -2.f || xF > logicalWidth + 2.f)
1259 QColor lineColor = ev.color;
1260 lineColor.setAlpha(180);
1261 p.setPen(QPen(lineColor, 1));
1262 p.drawLine(QPointF(xF, 0.f), QPointF(xF,
static_cast<float>(logicalHeight)));
1267 if (m_bShowEpochMarkers && !m_epochTriggerSamples.isEmpty()) {
1268 QPen epochPen(QColor(100, 100, 100, 140), 1, Qt::DashLine);
1270 for (
int trigSample : m_epochTriggerSamples) {
1271 float xF = (
static_cast<float>(trigSample) - overlayFirst) * overlayPixelsPerSample;
1272 if (xF < -2.f || xF > logicalWidth + 2.f)
1274 p.drawLine(QPointF(xF, 0.f), QPointF(xF,
static_cast<float>(logicalHeight)));
1278 m_overlayDirty =
false;
1283void ChannelRhiView::ensureOverlayPipeline()
1285 QRhi *rhi = this->rhi();
1286 if (!rhi || !renderTarget())
1288 if (m_overlayPipeline)
1298 static constexpr float kQuadVerts[] = {
1299 -1.f, -1.f, 0.f, 1.f,
1300 -1.f, 1.f, 0.f, 0.f,
1301 1.f, -1.f, 1.f, 1.f,
1304 static constexpr int kQuadBytes =
sizeof(kQuadVerts);
1305 m_overlayVbo.reset(rhi->newBuffer(QRhiBuffer::Immutable,
1306 QRhiBuffer::VertexBuffer,
1308 if (!m_overlayVbo->create()) {
1309 m_overlayVbo.reset();
1314 m_overlayTex.reset(rhi->newTexture(QRhiTexture::RGBA8, QSize(1, 1)));
1315 m_overlayTex->create();
1317 m_overlaySampler.reset(rhi->newSampler(
1318 QRhiSampler::Linear, QRhiSampler::Linear,
1320 QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge));
1321 m_overlaySampler->create();
1325 static constexpr int kOverlayUboSize = 256;
1326 m_overlayUbo.reset(rhi->newBuffer(QRhiBuffer::Dynamic,
1327 QRhiBuffer::UniformBuffer,
1329 if (!m_overlayUbo->create()) {
1330 m_overlayVbo.reset();
1331 m_overlayUbo.reset();
1336 m_overlaySrb.reset(rhi->newShaderResourceBindings());
1337 m_overlaySrb->setBindings({
1338 QRhiShaderResourceBinding::sampledTexture(
1339 1, QRhiShaderResourceBinding::FragmentStage,
1340 m_overlayTex.get(), m_overlaySampler.get()),
1341 QRhiShaderResourceBinding::uniformBuffer(
1342 2, QRhiShaderResourceBinding::FragmentStage,
1345 m_overlaySrb->create();
1347 auto loadShader = [](
const QString &path) -> QShader {
1349 if (!f.open(QIODevice::ReadOnly)) {
1350 qWarning() <<
"ChannelRhiView: cannot open shader" << path;
1353 return QShader::fromSerialized(f.readAll());
1356 QShader vs = loadShader(QStringLiteral(
":/disp/shaders/viewers/helpers/shaders/overlay.vert.qsb"));
1357 QShader fs = loadShader(QStringLiteral(
":/disp/shaders/viewers/helpers/shaders/overlay.frag.qsb"));
1358 if (!vs.isValid() || !fs.isValid()) {
1359 m_overlayVbo.reset();
1360 m_overlayUbo.reset();
1361 m_overlaySrb.reset();
1362 m_overlaySampler.reset();
1363 m_overlayTex.reset();
1367 m_overlayPipeline.reset(rhi->newGraphicsPipeline());
1368 QRhiGraphicsPipeline::TargetBlend blend;
1369 blend.enable =
true;
1370 blend.srcColor = QRhiGraphicsPipeline::SrcAlpha;
1371 blend.dstColor = QRhiGraphicsPipeline::OneMinusSrcAlpha;
1372 blend.srcAlpha = QRhiGraphicsPipeline::One;
1373 blend.dstAlpha = QRhiGraphicsPipeline::OneMinusSrcAlpha;
1374 m_overlayPipeline->setTargetBlends({ blend });
1375 m_overlayPipeline->setTopology(QRhiGraphicsPipeline::TriangleStrip);
1376 m_overlayPipeline->setDepthTest(
false);
1377 m_overlayPipeline->setDepthWrite(
false);
1378 m_overlayPipeline->setShaderStages({
1379 { QRhiShaderStage::Vertex, vs },
1380 { QRhiShaderStage::Fragment, fs },
1383 QRhiVertexInputLayout inputLayout;
1384 inputLayout.setBindings({ QRhiVertexInputBinding(4 *
sizeof(
float)) });
1385 inputLayout.setAttributes({
1386 QRhiVertexInputAttribute(0, 0, QRhiVertexInputAttribute::Float2, 0),
1387 QRhiVertexInputAttribute(0, 1, QRhiVertexInputAttribute::Float2, 2 *
sizeof(
float))
1389 m_overlayPipeline->setVertexInputLayout(inputLayout);
1390 m_overlayPipeline->setShaderResourceBindings(m_overlaySrb.get());
1391 m_overlayPipeline->setRenderPassDescriptor(renderTarget()->renderPassDescriptor());
1393 if (!m_overlayPipeline->create()) {
1394 qWarning() <<
"ChannelRhiView: overlay pipeline create failed";
1395 m_overlayPipeline.reset();
1402 m_overlayVboNeedsUpload =
true;
1411 QRhiResourceUpdateBatch *u = rhi()->nextResourceUpdateBatch();
1412 QColor bg = m_bgColor;
1413 cb->beginPass(renderTarget(), bg, {1.f, 0}, u);
1422 QRhiResourceUpdateBatch *u = rhi()->nextResourceUpdateBatch();
1423 cb->beginPass(renderTarget(), QColor(220, 0, 0), {1.f, 0}, u);
1428 QSize ps = renderTarget()->pixelSize();
1429 const int pw = ps.width();
1430 const int ph = ps.height();
1431 const int logicalW = width();
1432 const int logicalH = height();
1433 const qreal overlayDpr = (logicalW > 0) ? (
static_cast<qreal
>(pw) /
static_cast<qreal
>(logicalW)) : 1.0;
1435 QRhiResourceUpdateBatch *batch = rhi()->nextResourceUpdateBatch();
1443 ensureOverlayPipeline();
1444 bool overlayReady =
false;
1445 if (m_overlayPipeline && m_overlayVbo && m_overlayUbo && pw > 0 && ph > 0 && logicalW > 0 && logicalH > 0) {
1448 if (m_overlayVboNeedsUpload) {
1449 static constexpr float kQuadVerts[] = {
1450 -1.f, -1.f, 0.f, 1.f,
1451 -1.f, 1.f, 0.f, 0.f,
1452 1.f, -1.f, 1.f, 1.f,
1455 batch->uploadStaticBuffer(m_overlayVbo.get(), 0,
1456 static_cast<quint32
>(
sizeof(kQuadVerts)), kQuadVerts);
1457 m_overlayVboNeedsUpload =
false;
1461 const float visibleSamples =
static_cast<float>(logicalW) * m_samplesPerPixel;
1462 const float extraSamples = kOverlayPrefetchFactor * visibleSamples;
1463 const float overlayFirstSample = m_scrollSample - extraSamples;
1464 const float overlayTotalSamples = visibleSamples + 2.0f * extraSamples;
1466 const int overlayLogicalW =
static_cast<int>(std::ceil(
1467 (1.0f + 2.0f * kOverlayPrefetchFactor) *
static_cast<float>(logicalW)));
1470 const QSize requiredTexSize(qRound(overlayLogicalW * overlayDpr),
1471 qRound(logicalH * overlayDpr));
1472 if (m_overlayDirty || requiredTexSize != m_overlayTexSize) {
1474 m_overlayFirstSample = overlayFirstSample;
1475 m_overlayTotalSamples = overlayTotalSamples;
1477 rebuildOverlayImage(overlayLogicalW, logicalH, overlayDpr);
1480 if (requiredTexSize != m_overlayTexSize) {
1481 m_overlayTex.reset(rhi()->newTexture(QRhiTexture::RGBA8, requiredTexSize));
1482 m_overlayTex->create();
1484 m_overlaySrb->setBindings({
1485 QRhiShaderResourceBinding::sampledTexture(
1486 1, QRhiShaderResourceBinding::FragmentStage,
1487 m_overlayTex.get(), m_overlaySampler.get()),
1488 QRhiShaderResourceBinding::uniformBuffer(
1489 2, QRhiShaderResourceBinding::FragmentStage,
1492 m_overlaySrb->create();
1493 m_overlayTexSize = requiredTexSize;
1495 QRhiTextureUploadEntry entry(0, 0, QRhiTextureSubresourceUploadDescription(m_overlayImage));
1496 batch->uploadTexture(m_overlayTex.get(), entry);
1500 struct OverlayParams {
1505 float firstFileSample;
1507 float overlayFirstSample;
1508 float overlayTotalSamples;
1510 OverlayParams params;
1511 params.scrollSample = m_scrollSample;
1512 params.samplesPerPixel = m_samplesPerPixel;
1513 params.viewWidth =
static_cast<float>(logicalW);
1514 params.sfreq = m_sfreq;
1515 params.firstFileSample =
static_cast<float>(m_firstFileSample);
1516 params.gridEnabled = m_gridVisible ? 1.0f : 0.0f;
1517 params.overlayFirstSample = m_overlayFirstSample;
1518 params.overlayTotalSamples = m_overlayTotalSamples;
1519 batch->updateDynamicBuffer(m_overlayUbo.get(), 0,
1520 static_cast<quint32
>(
sizeof(params)), ¶ms);
1522 overlayReady =
true;
1526 QColor bg = m_bgColor;
1527 cb->beginPass(renderTarget(), bg, {1.f, 0}, batch);
1529 cb->setViewport(QRhiViewport(0.f, 0.f,
static_cast<float>(pw),
1530 static_cast<float>(ph)));
1533 cb->setGraphicsPipeline(m_pipeline.get());
1537 if (m_butterflyMode) {
1539 int nToRender = qMin(totalCh, kMaxChannels);
1540 for (
int logCh = 0; logCh < nToRender; ++logCh) {
1541 if (logCh >=
static_cast<int>(m_gpuChannels.size()))
1543 auto &gd = m_gpuChannels[logCh];
1544 if (!gd.vbo || gd.vertexCount < 2)
1547 quint32 dynOffset =
static_cast<quint32
>(logCh * m_uboStride);
1548 QRhiCommandBuffer::DynamicOffset dynOff{0, dynOffset};
1549 cb->setShaderResources(m_srb.get(), 1, &dynOff);
1551 QRhiCommandBuffer::VertexInput vi(gd.vbo.get(), 0);
1552 cb->setVertexInput(0, 1, &vi);
1553 cb->draw(
static_cast<quint32
>(gd.vertexCount));
1557 int firstCh = qBound(0, m_firstVisibleChannel, totalCh);
1558 int visCnt = qMin(m_visibleChannelCount, totalCh - firstCh);
1559 int nToRender = qMin(visCnt, kMaxChannels);
1561 for (
int i = 0; i < nToRender; ++i) {
1562 int logCh = firstCh + i;
1563 if (logCh >=
static_cast<int>(m_gpuChannels.size()))
1565 auto &gd = m_gpuChannels[logCh];
1566 if (!gd.vbo || gd.vertexCount < 2)
1569 quint32 dynOffset =
static_cast<quint32
>(i * m_uboStride);
1570 QRhiCommandBuffer::DynamicOffset dynOff{0, dynOffset};
1571 cb->setShaderResources(m_srb.get(), 1, &dynOff);
1573 QRhiCommandBuffer::VertexInput vi(gd.vbo.get(), 0);
1574 cb->setVertexInput(0, 1, &vi);
1575 cb->draw(
static_cast<quint32
>(gd.vertexCount));
1581 cb->setGraphicsPipeline(m_overlayPipeline.get());
1582 cb->setShaderResources(m_overlaySrb.get());
1583 QRhiCommandBuffer::VertexInput overlayVi(m_overlayVbo.get(), 0);
1584 cb->setVertexInput(0, 1, &overlayVi);
1593 QRhiWidget::paintEvent(event);
1601bool ChannelRhiView::isTileFresh()
const
1603 if (m_tileDirty || m_tileImage.isNull() || m_tileSamplesPerPixel <= 0.f)
1605 if (!qFuzzyCompare(m_tileSamplesPerPixel, m_samplesPerPixel))
1607 if (m_tileFirstChannel != m_firstVisibleChannel)
1610 int visibleCount = qMin(m_visibleChannelCount, totalCh - m_firstVisibleChannel);
1611 if (m_tileVisibleCount != visibleCount)
1615 float visibleSamples = width() * m_samplesPerPixel;
1616 float tileEnd = m_tileSampleFirst + m_tileImage.width() * m_tileSamplesPerPixel;
1617 if (m_scrollSample < m_tileSampleFirst + visibleSamples)
1619 if (m_scrollSample + visibleSamples > tileEnd - visibleSamples)
1627void ChannelRhiView::scheduleTileRebuild()
1630 if (m_tileRebuildPending)
1637 m_tileImage = QImage(qMax(width(), 1), qMax(height(), 1), QImage::Format_RGB32);
1638 m_tileImage.fill(m_bgColor.rgb());
1639 m_tileSampleFirst = m_scrollSample;
1640 m_tileSamplesPerPixel = qMax(m_samplesPerPixel, 1e-4f);
1641 m_tileFirstChannel = m_firstVisibleChannel;
1642 m_tileVisibleCount = 0;
1643 m_tileDirty =
false;
1648 ChannelDataModel *model = m_model.data();
1650 float spp = m_samplesPerPixel;
1651 int firstCh = m_firstVisibleChannel;
1652 int visCnt = m_visibleChannelCount;
1655 QColor bg = m_bgColor;
1656 bool gridVis = m_gridVisible;
1657 float sfreq = m_sfreq;
1658 int firstFileSample = m_firstFileSample;
1659 bool hideBad = m_hideBadChannels;
1660 QVector<int> chIndices = m_filteredChannels;
1661 QVector<EventMarker> eventsSnap = m_bShowEvents ? m_events : QVector<EventMarker>();
1662 QVector<AnnotationSpan> annotationsSnap = m_bShowAnnotations ? m_annotations : QVector<AnnotationSpan>();
1663 QVector<int> epochSnap = m_bShowEpochMarkers ? m_epochTriggerSamples : QVector<int>();
1664 bool clipSnap = m_bShowClipping;
1665 bool zscoreSnap = m_bZScoreMode;
1667 m_tileDirty =
false;
1668 m_tileRebuildPending =
true;
1669 m_tileWatcher.setFuture(QtConcurrent::run([=]() {
1670 return ChannelRhiView::buildTile(model,
scrollSample, spp, firstCh, visCnt,
1671 pw, ph, bg, gridVis, sfreq, firstFileSample,
1672 hideBad, chIndices, eventsSnap, annotationsSnap, epochSnap,
1673 clipSnap, zscoreSnap);
1679ChannelRhiView::TileResult ChannelRhiView::buildTile(
1681 float scrollSample,
float spp,
1682 int firstCh,
int visCnt,
1684 QColor bgColor,
bool gridVisible,
1685 float sfreq,
int firstFileSample,
1686 bool hideBadChannels,
1687 const QVector<int> &channelIndices,
1688 const QVector<EventMarker> &events,
1689 const QVector<AnnotationSpan> &annotations,
1690 const QVector<int> &epochMarkers,
1695 out.samplesPerPixel = spp;
1696 out.firstChannel = firstCh;
1698 if (!model || pw <= 0 || ph <= 0 || spp <= 0.f)
1701 int totalCh = channelIndices.isEmpty() ? model->
channelCount() : channelIndices.size();
1702 int visibleCount = qMin(visCnt, totalCh - firstCh);
1703 if (visibleCount <= 0)
1706 out.visibleCount = visibleCount;
1708 const int kTileMult = 5;
1709 int tilePixWidth = pw * kTileMult;
1710 float visibleSamples = pw * spp;
1713 out.sampleFirst = tileStart;
1715 QImage img(tilePixWidth, ph, QImage::Format_RGB32);
1716 img.fill(bgColor.rgb());
1719 p.setRenderHint(QPainter::Antialiasing,
false);
1721 float laneH =
static_cast<float>(ph) / visibleCount;
1722 int firstSample =
static_cast<int>(tileStart);
1723 int lastSample = firstSample +
static_cast<int>(tilePixWidth * spp) + 1;
1728 float samplesPerSec = sfreq;
1729 float firstBound = std::floor(
1730 (tileStart -
static_cast<float>(firstFileSample)) / samplesPerSec
1731 ) * samplesPerSec +
static_cast<float>(firstFileSample);
1734 long long bandIndex =
static_cast<long long>(
1735 (firstBound -
static_cast<float>(firstFileSample)) / samplesPerSec);
1736 bool oddBand = (bandIndex & 1) != 0;
1740 qBound(0, bgColor.red() - 10, 255),
1741 qBound(0, bgColor.green() - 10, 255),
1742 qBound(0, bgColor.blue() - 10, 255)
1745 for (
float s = firstBound; s < lastSample; s += samplesPerSec, oddBand = !oddBand) {
1748 float xStart = (s - tileStart) / spp;
1749 float xEnd = xStart + samplesPerSec / spp;
1750 xStart = qBound(0.f, xStart,
static_cast<float>(tilePixWidth));
1751 xEnd = qBound(0.f, xEnd,
static_cast<float>(tilePixWidth));
1753 p.fillRect(QRectF(xStart, 0, xEnd - xStart, ph), altColor);
1759 for (
int i = 0; i < visibleCount; ++i) {
1760 float yMid = (i + 0.5f) * laneH;
1761 float yTop = i * laneH;
1764 p.setPen(QPen(QColor(205, 205, 215), 1));
1765 p.drawLine(QPointF(0, yTop), QPointF(tilePixWidth, yTop));
1768 QPen guidePen(QColor(228, 228, 235), 1, Qt::DotLine);
1769 guidePen.setDashPattern({3, 4});
1771 p.drawLine(QPointF(0, yMid - laneH * 0.44f), QPointF(tilePixWidth, yMid - laneH * 0.44f));
1772 p.drawLine(QPointF(0, yMid + laneH * 0.44f), QPointF(tilePixWidth, yMid + laneH * 0.44f));
1774 p.setPen(QPen(QColor(210, 210, 218), 1));
1775 p.drawLine(QPointF(0, yMid), QPointF(tilePixWidth, yMid));
1779 static const float kNiceIntervals[] = {
1780 0.05f, 0.1f, 0.2f, 0.5f, 1.f, 2.f, 5.f, 10.f, 30.f, 60.f
1782 float pxPerSecond = sfreq / spp;
1783 float tickIntervalS = kNiceIntervals[0];
1784 for (
float iv : kNiceIntervals) {
1786 if (iv * pxPerSecond >= 80.f)
1789 float tickSamples = tickIntervalS * sfreq;
1790 float origin =
static_cast<float>(firstFileSample);
1791 float firstTick = std::ceil((tileStart - origin) / tickSamples) * tickSamples + origin;
1793 p.setPen(QPen(QColor(205, 205, 210), 1));
1794 for (
float s = firstTick; s < lastSample; s += tickSamples) {
1795 float xPx = (s - tileStart) / spp;
1796 p.drawLine(QPointF(xPx, 0), QPointF(xPx, ph));
1802 if (!annotations.isEmpty()) {
1803 QFont font = p.font();
1804 font.setPointSizeF(8.0);
1809 float xStart = (
static_cast<float>(annotation.startSample) - tileStart) / spp;
1810 float xEnd = (
static_cast<float>(annotation.endSample + 1) - tileStart) / spp;
1812 if (xEnd < -2.f || xStart > tilePixWidth + 2.f) {
1816 xStart = qBound(0.f, xStart,
static_cast<float>(tilePixWidth));
1817 xEnd = qBound(0.f, xEnd,
static_cast<float>(tilePixWidth));
1818 if (xEnd <= xStart) {
1822 QColor fillColor = annotation.color;
1823 fillColor.setAlpha(48);
1824 p.fillRect(QRectF(xStart, 0.f, xEnd - xStart,
static_cast<float>(ph)), fillColor);
1826 QColor borderColor = annotation.color;
1827 borderColor.setAlpha(165);
1828 p.setPen(QPen(borderColor, 1));
1829 p.drawLine(QPointF(xStart, 0.f), QPointF(xStart,
static_cast<float>(ph)));
1830 p.drawLine(QPointF(xEnd, 0.f), QPointF(xEnd,
static_cast<float>(ph)));
1832 if (!annotation.label.trimmed().isEmpty()) {
1833 const QString label = annotation.label.trimmed();
1834 QFontMetrics metrics(font);
1835 QRect labelRect = metrics.boundingRect(label);
1836 labelRect.adjust(-6, -2, 6, 2);
1837 const int labelX = qBound(4,
1838 static_cast<int>(xStart) + 4,
1839 qMax(4, tilePixWidth - labelRect.width() - 4));
1840 labelRect.moveTopLeft(QPoint(labelX, 4));
1841 QColor pillColor = annotation.color;
1842 pillColor.setAlpha(215);
1843 p.fillRect(labelRect, pillColor);
1844 p.setPen(Qt::white);
1845 p.drawText(labelRect, Qt::AlignCenter, label);
1851 for (
int i = 0; i < visibleCount; ++i) {
1852 int logIdx = firstCh + i;
1853 int ch = channelIndices.isEmpty() ? logIdx
1854 : (logIdx < channelIndices.size() ? channelIndices[logIdx] : -1);
1865 ch, firstSample, lastSample, tilePixWidth, vboFirst);
1866 if (verts.size() < 4)
1869 QColor col = info.
bad ? QColor(190, 40, 40) : info.color;
1870 QPen normalPen(col, 1.2);
1871 QPen clipPen(QColor(255, 0, 0), 1.6);
1873 float yMid = (i + 0.5f) * laneH;
1874 int nVerts = verts.size() / 2;
1878 float zMean = 0.f, zStd = 1.f;
1880 double sum = 0.0, sumSq = 0.0;
1881 for (
int v = 0; v < nVerts; ++v) {
1882 double a =
static_cast<double>(verts[v * 2 + 1]);
1886 zMean =
static_cast<float>(sum / nVerts);
1887 double var = sumSq / nVerts -
static_cast<double>(zMean) * zMean;
1888 zStd = var > 0.0 ?
static_cast<float>(qSqrt(var)) : 1.f;
1890 yScale = (laneH * 0.45f) / 4.f;
1897 bool doClip = showClipping && !info.
bad && !
zScoreMode && clipThresh > 0.f;
1901 p.setPen(normalPen);
1903 poly.reserve(nVerts);
1904 for (
int v = 0; v < nVerts; ++v) {
1905 float samplePos = vboFirst + verts[v * 2];
1906 float xPx = (samplePos - tileStart) / spp;
1907 float amp = verts[v * 2 + 1];
1909 float yPx = yMid - amp * yScale;
1910 poly.append(QPointF(xPx, yPx));
1912 p.drawPolyline(poly);
1916 seg.reserve(nVerts);
1917 bool prevClipped =
false;
1919 for (
int v = 0; v < nVerts; ++v) {
1920 float amp = verts[v * 2 + 1];
1921 float samplePos = vboFirst + verts[v * 2];
1922 float xPx = (samplePos - tileStart) / spp;
1923 float yPx = yMid - amp * yScale;
1924 bool clipped = qAbs(amp) >= clipThresh;
1926 if (v > 0 && clipped != prevClipped) {
1929 seg.append(QPointF(xPx, yPx));
1930 p.setPen(prevClipped ? clipPen : normalPen);
1931 p.drawPolyline(seg);
1935 seg.append(QPointF(xPx, yPx));
1936 prevClipped = clipped;
1939 if (seg.size() > 1) {
1940 p.setPen(prevClipped ? clipPen : normalPen);
1941 p.drawPolyline(seg);
1949 if (!events.isEmpty() && spp > 0.f) {
1951 float xF = (
static_cast<float>(ev.sample) - tileStart) / spp;
1952 if (xF < -2.f || xF > tilePixWidth + 2.f)
1954 int ix =
static_cast<int>(xF);
1956 QColor lineColor = ev.color;
1957 lineColor.setAlpha(180);
1958 p.setPen(QPen(lineColor, 1));
1959 p.drawLine(ix, 0, ix, ph);
1965 if (!epochMarkers.isEmpty() && spp > 0.f) {
1966 QPen epochPen(QColor(100, 100, 100, 140), 1, Qt::DashLine);
1968 for (
int trigSample : epochMarkers) {
1969 float xF = (
static_cast<float>(trigSample) - tileStart) / spp;
1970 if (xF < -2.f || xF > tilePixWidth + 2.f)
1972 int ix =
static_cast<int>(xF);
1973 p.drawLine(ix, 0, ix, ph);
1977 out.image = std::move(img);
1983void ChannelRhiView::drawOverlays()
1990 if (m_overlay && (m_crosshairEnabled || m_scalebarsVisible || m_rulerActive))
1991 m_overlay->update();
1996static QString formatAmplitude(
float amp,
const QString &unit)
1998 float absAmp = qAbs(amp);
2000 return QStringLiteral(
"0 ") + unit;
2002 return QString::number(amp * 1e12f,
'f', 1) + QStringLiteral(
" p") + unit;
2004 return QString::number(amp * 1e9f,
'f', 1) + QStringLiteral(
" n") + unit;
2006 return QString::number(amp * 1e6f,
'f', 1) + QStringLiteral(
" µ") + unit;
2008 return QString::number(amp * 1e3f,
'f', 1) + QStringLiteral(
" m") + unit;
2009 return QString::number(amp,
'f', 3) + QStringLiteral(
" ") + unit;
2012static QString unitForType(
const QString &typeLabel)
2014 if (typeLabel == QStringLiteral(
"MEG grad"))
2015 return QStringLiteral(
"T/m");
2016 if (typeLabel == QStringLiteral(
"MEG mag") || typeLabel == QStringLiteral(
"MEG"))
2017 return QStringLiteral(
"T");
2018 if (typeLabel == QStringLiteral(
"EEG") ||
2019 typeLabel == QStringLiteral(
"EOG") ||
2020 typeLabel == QStringLiteral(
"ECG") ||
2021 typeLabel == QStringLiteral(
"EMG"))
2022 return QStringLiteral(
"V");
2023 return QStringLiteral(
"AU");
2030 if (m_crosshairX < 0 || m_crosshairY < 0)
2035 const int w = width();
2036 const int h = height();
2039 QPen crossPen(QColor(80, 80, 80, 160), 1, Qt::DashLine);
2041 p.drawLine(m_crosshairX, 0, m_crosshairX, h);
2042 p.drawLine(0, m_crosshairY, w, m_crosshairY);
2044 int sample =
static_cast<int>(m_scrollSample +
static_cast<float>(m_crosshairX) * m_samplesPerPixel);
2045 float timeSec = (m_sfreq > 0.f) ?
static_cast<float>(sample - m_firstFileSample) / m_sfreq : 0.f;
2046 QString channelLabel;
2050 if (m_butterflyMode) {
2052 const auto groups = butterflyTypeGroups();
2053 int nLanes = groups.size();
2056 float laneH =
static_cast<float>(h) / nLanes;
2057 int lane = qBound(0,
static_cast<int>(m_crosshairY / laneH), nLanes - 1);
2058 channelLabel = groups[lane].typeLabel;
2059 unitStr = unitForType(groups[lane].typeLabel);
2063 int visCnt = qMin(m_visibleChannelCount, totalCh - m_firstVisibleChannel);
2067 float laneH =
static_cast<float>(h) / visCnt;
2068 int row = qBound(0,
static_cast<int>(m_crosshairY / laneH), visCnt - 1);
2069 int ch = actualChannelAt(m_firstVisibleChannel + row);
2073 auto info = m_model->channelInfo(ch);
2074 value = m_model->sampleValueAt(ch, sample);
2075 channelLabel = info.
name;
2081 if (m_useClockTime && timeSec >= 0.f) {
2082 int totalMs =
static_cast<int>(timeSec * 1000.f + 0.5f);
2083 int m = totalMs / 60000;
2084 int sec = (totalMs % 60000) / 1000;
2085 int ms = totalMs % 1000;
2086 timeStr = QString(
"%1:%2.%3")
2087 .arg(m, 2, 10, QChar(
'0'))
2088 .arg(sec, 2, 10, QChar(
'0'))
2089 .arg(ms, 3, 10, QChar(
'0'));
2091 timeStr = QString::number(
static_cast<double>(timeSec),
'f', 3) + QStringLiteral(
" s");
2093 QString label = QString(
"%1 %2 %3")
2096 formatAmplitude(value, unitStr));
2099 f.setPointSizeF(8.5);
2102 QRect labelRect = fm.boundingRect(label);
2103 int lx = m_crosshairX + 10;
2104 int ly = m_crosshairY - 10;
2105 if (lx + labelRect.width() + 8 > w)
2106 lx = m_crosshairX - labelRect.width() - 18;
2107 if (ly - labelRect.height() < 4)
2108 ly = m_crosshairY + labelRect.height() + 6;
2109 labelRect.moveTopLeft(QPoint(lx, ly - labelRect.height()));
2110 labelRect.adjust(-4, -2, 4, 2);
2111 p.fillRect(labelRect, QColor(255, 255, 255, 220));
2112 p.setPen(QColor(30, 30, 30));
2113 p.drawText(labelRect, Qt::AlignCenter, label);
2120 if (m_crosshairX < 0 || m_crosshairY < 0)
2125 const int h = height();
2126 int sample =
static_cast<int>(m_scrollSample +
static_cast<float>(m_crosshairX) * m_samplesPerPixel);
2127 float timeSec = (m_sfreq > 0.f) ?
static_cast<float>(sample - m_firstFileSample) / m_sfreq : 0.f;
2129 if (m_butterflyMode) {
2130 const auto groups = butterflyTypeGroups();
2131 int nLanes = groups.size();
2132 if (nLanes <= 0)
return;
2133 float laneH =
static_cast<float>(h) / nLanes;
2134 int lane = qBound(0,
static_cast<int>(m_crosshairY / laneH), nLanes - 1);
2136 groups[lane].typeLabel,
2137 unitForType(groups[lane].typeLabel));
2140 int visCnt = qMin(m_visibleChannelCount, totalCh - m_firstVisibleChannel);
2141 if (visCnt <= 0)
return;
2142 float laneH =
static_cast<float>(h) / visCnt;
2143 int row = qBound(0,
static_cast<int>(m_crosshairY / laneH), visCnt - 1);
2144 int ch = actualChannelAt(m_firstVisibleChannel + row);
2146 auto info = m_model->channelInfo(ch);
2147 float value = m_model->sampleValueAt(ch, sample);
2160 if (m_butterflyMode) {
2161 visCnt = butterflyLaneCount();
2164 visCnt = qMin(m_visibleChannelCount, totalCh - m_firstVisibleChannel);
2169 float laneH =
static_cast<float>(height()) / visCnt;
2172 QMap<QString, float> typeScales;
2173 if (m_butterflyMode) {
2174 const auto groups = butterflyTypeGroups();
2175 for (
const auto &g : groups)
2176 if (g.amplitudeMax > 0.f)
2177 typeScales[g.typeLabel] = g.amplitudeMax;
2179 for (
int i = 0; i < visCnt; ++i) {
2180 int ch = actualChannelAt(m_firstVisibleChannel + i);
2181 if (ch < 0)
continue;
2182 auto info = m_model->channelInfo(ch);
2188 if (typeScales.isEmpty())
2192 f.setPointSizeF(8.0);
2197 const int margin = 12;
2198 const int barHeight = qBound(20,
static_cast<int>(laneH * 0.35f), 60);
2199 int x = width() - margin;
2200 int y = height() - margin;
2202 for (
auto it = typeScales.constEnd(); it != typeScales.constBegin(); ) {
2204 QString unit = unitForType(it.key());
2205 float ampValue = it.value();
2206 QString label = it.key() + QStringLiteral(
": ") + formatAmplitude(ampValue, unit);
2208 int textW = fm.horizontalAdvance(label);
2209 int barX = x - textW - 14;
2212 QRect bgRect(barX - 4, y - barHeight - fm.height() - 4, textW + 22, barHeight + fm.height() + 8);
2213 p.fillRect(bgRect, QColor(255, 255, 255, 200));
2216 QPen barPen(QColor(40, 40, 40), 2);
2218 int barTop = y - barHeight;
2219 p.drawLine(barX + 4, barTop, barX + 4, y);
2221 p.drawLine(barX, barTop, barX + 8, barTop);
2222 p.drawLine(barX, y, barX + 8, y);
2225 p.setPen(QColor(30, 30, 30));
2226 p.drawText(barX + 14, y - barHeight / 2 + fm.ascent() / 2, label);
2228 y -= barHeight + fm.height() + 16;
2236 int x0 = m_rulerX0, y0 = m_rulerY0;
2237 int x1 = m_rulerX1, y1 = m_rulerY1;
2239 const bool snapH = (m_rulerSnap == RulerSnap::Horizontal);
2240 const bool snapV = (m_rulerSnap == RulerSnap::Vertical);
2242 const QColor activeColor(40, 120, 200, 220);
2243 const QColor dimColor(130, 160, 200, 120);
2250 measured = QRect(QPoint(qMin(x0, x1), 0),
2251 QPoint(qMax(x0, x1), height()));
2253 measured = QRect(QPoint(0, qMin(y0, y1)),
2254 QPoint(width(), qMax(y0, y1)));
2256 measured = QRect(QPoint(qMin(x0, x1), qMin(y0, y1)),
2257 QPoint(qMax(x0, x1), qMax(y0, y1)));
2258 p.fillRect(measured, QColor(255, 255, 255, 60));
2260 p.setPen(QPen(QColor(40, 120, 200, 80), 1));
2261 p.drawRect(measured);
2265 QPen vLinePen(snapV ? dimColor : activeColor, 1, Qt::DashLine);
2267 p.drawLine(x0, 0, x0, height());
2269 p.drawLine(x1, 0, x1, height());
2273 QPen hGuidePen(activeColor, 1, Qt::DashLine);
2274 p.setPen(hGuidePen);
2275 p.drawLine(0, y0, width(), y0);
2276 p.drawLine(0, y1, width(), y1);
2280 QPen hLinePen(snapV ? dimColor : activeColor, snapH ? 2 : 1);
2283 p.drawLine(qMin(x0, x1), y0, qMax(x0, x1), y0);
2286 QPen vSpanPen(snapH ? dimColor : activeColor, snapV ? 2 : 1);
2289 p.drawLine(x0, qMin(y0, y1), x0, qMax(y0, y1));
2293 p.setPen(QPen(activeColor, 1));
2294 p.drawLine(x0 - tickLen, y0, x0 + tickLen, y0);
2295 p.drawLine(x1 - tickLen, y0, x1 + tickLen, y0);
2298 p.setPen(QPen(activeColor, 1));
2299 p.drawLine(x0, y0 - tickLen, x0, y0 + tickLen);
2300 p.drawLine(x0, y1 - tickLen, x0, y1 + tickLen);
2304 float deltaSamples =
static_cast<float>(x1 - x0) * m_samplesPerPixel;
2305 float deltaSec = (m_sfreq > 0.f) ? deltaSamples / m_sfreq : 0.f;
2307 float deltaAmp = 0.f;
2308 QString ampUnit = QStringLiteral(
"AU");
2311 int visCnt = qMin(m_visibleChannelCount, totalCh - m_firstVisibleChannel);
2313 float laneH =
static_cast<float>(height()) / visCnt;
2314 int row = qBound(0,
static_cast<int>(y0 / laneH), visCnt - 1);
2315 int ch = actualChannelAt(m_firstVisibleChannel + row);
2317 auto info = m_model->channelInfo(ch);
2319 float dyPx =
static_cast<float>(y1 - y0);
2321 deltaAmp = -dyPx * yScale;
2328 auto fmtTime = [](
float sec) -> QString {
2329 float absSec = qAbs(sec);
2331 return QString::number(sec * 1000.f,
'f', 1) + QStringLiteral(
" ms");
2332 return QString::number(sec,
'f', 3) + QStringLiteral(
" s");
2334 auto fmtAmp = [](
float amp,
const QString &unit) -> QString {
2335 float absAmp = qAbs(amp);
2337 return QString::number(amp * 1e9f,
'f', 3) + QStringLiteral(
" n") + unit;
2339 return QString::number(amp * 1e6f,
'f', 3) + QStringLiteral(
" µ") + unit;
2341 return QString::number(amp * 1e3f,
'f', 3) + QStringLiteral(
" m") + unit;
2342 return QString::number(amp,
'f', 3) + QStringLiteral(
" ") + unit;
2345 QString timeLabel = QStringLiteral(
"\u0394T = ") + fmtTime(deltaSec);
2346 QString ampLabel = QStringLiteral(
"\u0394A = ") + fmtAmp(deltaAmp, ampUnit);
2349 f.setPointSizeF(9.0);
2356 int labelX = (x0 + x1) / 2;
2357 int labelY = y0 - 6;
2361 QRect tRect = fm.boundingRect(timeLabel);
2362 tRect.moveCenter(QPoint(labelX, labelY));
2363 tRect.adjust(-4, -2, 4, 2);
2364 p.fillRect(tRect, QColor(255, 255, 255, 210));
2365 p.setPen(QColor(20, 80, 160));
2366 p.drawText(tRect, Qt::AlignCenter, timeLabel);
2371 int aLabelX = x0 + 8;
2372 int aLabelY = (y0 + y1) / 2;
2373 QRect aRect = fm.boundingRect(ampLabel);
2374 aRect.moveCenter(QPoint(aLabelX + aRect.width() / 2, aLabelY));
2375 aRect.adjust(-4, -2, 4, 2);
2376 p.fillRect(aRect, QColor(255, 255, 255, 210));
2377 p.setPen(QColor(20, 80, 160));
2378 p.drawText(aRect, Qt::AlignCenter, ampLabel);
2386 const int x0 = qMin(m_annSelX0, m_annSelX1);
2387 const int x1 = qMax(m_annSelX0, m_annSelX1);
2388 const int h = height();
2391 p.fillRect(QRect(x0, 0, x1 - x0, h), QColor(210, 60, 60, 50));
2394 QPen borderPen(QColor(210, 60, 60, 180), 2);
2395 p.setPen(borderPen);
2396 p.drawLine(x0, 0, x0, h);
2397 p.drawLine(x1, 0, x1, h);
2400 if (m_sfreq > 0.f && m_samplesPerPixel > 0.f) {
2401 float deltaSamples =
static_cast<float>(x1 - x0) * m_samplesPerPixel;
2402 float deltaSec = deltaSamples / m_sfreq;
2405 label = QString::number(deltaSec * 1000.f,
'f', 0) + QStringLiteral(
" ms");
2407 label = QString::number(deltaSec,
'f', 2) + QStringLiteral(
" s");
2410 f.setPointSizeF(8.0);
2414 QRect labelRect = fm.boundingRect(label);
2415 labelRect.adjust(-6, -2, 6, 2);
2416 labelRect.moveTopLeft(QPoint(x0 + 4, 4));
2417 p.fillRect(labelRect, QColor(210, 60, 60, 215));
2418 p.setPen(Qt::white);
2419 p.drawText(labelRect, Qt::AlignCenter, label);
2429 QRhiWidget::resizeEvent(event);
2431 m_overlayDirty =
true;
2433 if (m_overlay) m_overlay->syncSize();
2442 const QPoint delta =
event->angleDelta();
2444 if (event->modifiers() & Qt::ControlModifier) {
2446 float factor = (delta.y() > 0) ? 0.8f : 1.25f;
2447 zoomTo(m_samplesPerPixel * factor, 150);
2449 }
else if (qAbs(delta.x()) > qAbs(delta.y())) {
2452 float step = width() * m_samplesPerPixel * 0.1f * m_scrollSpeedFactor
2453 * (delta.x() > 0 ? -1.f : 1.f);
2454 scrollTo(m_scrollSample + step, 100);
2457 }
else if (m_wheelScrollsChannels) {
2459 int channelStep = (delta.y() > 0) ? -1 : 1;
2465 float step = width() * m_samplesPerPixel * 0.15f * m_scrollSpeedFactor
2466 * (delta.y() > 0 ? -1.f : 1.f);
2467 scrollTo(m_scrollSample + step, 100);
2480 if (event->button() == Qt::RightButton) {
2482 if (m_pInertialAnim) {
2483 m_pInertialAnim->stop();
2484 m_pInertialAnim =
nullptr;
2487 if (m_annotationSelectionEnabled) {
2489 m_annSelecting =
true;
2490 m_annSelX0 = m_annSelX1 =
event->position().toPoint().x();
2491 if (m_overlay) m_overlay->repaint();
2494 m_rulerActive =
true;
2495 m_rulerSnap = RulerSnap::Free;
2496 m_rulerX0 = m_rulerX1 = m_rulerRawX1 =
event->position().toPoint().x();
2497 m_rulerY0 = m_rulerY1 = m_rulerRawY1 =
event->position().toPoint().y();
2498 if (m_overlay) m_overlay->repaint();
2505 (event->button() == Qt::MiddleButton ||
2506 (event->button() == Qt::LeftButton && (event->modifiers() & Qt::AltModifier)))) {
2508 m_dragStartX =
event->position().toPoint().x();
2509 m_dragStartScroll = m_scrollSample;
2513 if (event->button() == Qt::LeftButton) {
2515 if (m_pInertialAnim) {
2516 m_pInertialAnim->stop();
2517 m_pInertialAnim =
nullptr;
2521 if (m_annotationSelectionEnabled && !m_annotations.isEmpty()) {
2522 bool isStart =
false;
2523 int hitIdx = hitTestAnnotationBoundary(event->position().toPoint().x(), isStart);
2525 m_annDragging =
true;
2526 m_annDragIndex = hitIdx;
2527 m_annDragIsStart = isStart;
2535 float samplePos = m_scrollSample
2536 +
static_cast<float>(
event->position().x()) * m_samplesPerPixel;
2542 m_leftButtonDown =
true;
2543 m_leftDragActivated =
false;
2544 m_leftDownX =
event->position().toPoint().x();
2545 m_leftDownScroll = m_scrollSample;
2546 m_velocityHistory.clear();
2547 m_dragTimer.start();
2548 m_velocityHistory.append({m_leftDownX, 0});
2552 QRhiWidget::mousePressEvent(event);
2560 if (m_annDragging) {
2562 int newSample =
static_cast<int>(m_scrollSample
2563 +
static_cast<float>(
event->position().toPoint().x()) * m_samplesPerPixel);
2564 newSample = qMax(newSample, m_firstFileSample);
2565 if (m_lastFileSample >= 0)
2566 newSample = qMin(newSample, m_lastFileSample);
2568 if (m_annDragIndex >= 0 && m_annDragIndex < m_annotations.size()) {
2569 if (m_annDragIsStart)
2570 m_annotations[m_annDragIndex].startSample = newSample;
2572 m_annotations[m_annDragIndex].endSample = newSample;
2573 m_overlayDirty =
true;
2582 if (m_annSelecting) {
2583 m_annSelX1 =
event->position().toPoint().x();
2584 if (m_overlay) m_overlay->repaint();
2589 if (m_rulerActive) {
2590 m_rulerRawX1 =
event->position().toPoint().x();
2591 m_rulerRawY1 =
event->position().toPoint().y();
2595 int dx = qAbs(m_rulerRawX1 - m_rulerX0);
2596 int dy = qAbs(m_rulerRawY1 - m_rulerY0);
2597 const int kSnapThresh = 8;
2598 if (dx < kSnapThresh && dy < kSnapThresh) {
2599 m_rulerSnap = RulerSnap::Free;
2600 }
else if (dx > dy * 2) {
2601 m_rulerSnap = RulerSnap::Horizontal;
2602 }
else if (dy > dx * 2) {
2603 m_rulerSnap = RulerSnap::Vertical;
2605 m_rulerSnap = RulerSnap::Free;
2609 switch (m_rulerSnap) {
2610 case RulerSnap::Horizontal:
2611 m_rulerX1 = m_rulerRawX1;
2612 m_rulerY1 = m_rulerY0;
2614 case RulerSnap::Vertical:
2615 m_rulerX1 = m_rulerX0;
2616 m_rulerY1 = m_rulerRawY1;
2619 m_rulerX1 = m_rulerRawX1;
2620 m_rulerY1 = m_rulerRawY1;
2624 if (m_overlay) m_overlay->repaint();
2630 int dx =
event->position().toPoint().x() - m_dragStartX;
2631 float newScroll = m_dragStartScroll -
static_cast<float>(dx) * m_samplesPerPixel;
2636 if (m_leftButtonDown) {
2637 int x =
event->position().toPoint().x();
2638 int dx = x - m_leftDownX;
2639 if (!m_leftDragActivated && qAbs(dx) > 5)
2640 m_leftDragActivated =
true;
2641 if (m_leftDragActivated) {
2642 float newScroll = m_leftDownScroll -
static_cast<float>(dx) * m_samplesPerPixel;
2646 qint64 now = m_dragTimer.elapsed();
2647 m_velocityHistory.append({x, now});
2648 while (m_velocityHistory.size() > 1 &&
2649 now - m_velocityHistory.first().t > 100)
2650 m_velocityHistory.removeFirst();
2658 if (m_crosshairEnabled) {
2659 m_crosshairX =
event->position().toPoint().x();
2660 m_crosshairY =
event->position().toPoint().y();
2661 if (m_overlay) m_overlay->repaint();
2669 if (m_annotationSelectionEnabled && !m_annotations.isEmpty()) {
2670 bool isStart =
false;
2671 int hitIdx = hitTestAnnotationBoundary(event->position().toPoint().x(), isStart);
2673 if (m_annHoverIndex != hitIdx || m_annHoverIsStart != isStart) {
2674 m_annHoverIndex = hitIdx;
2675 m_annHoverIsStart = isStart;
2676 setCursor(Qt::SizeHorCursor);
2678 }
else if (m_annHoverIndex >= 0) {
2679 m_annHoverIndex = -1;
2684 QRhiWidget::mouseMoveEvent(event);
2692 if (m_annDragging && event->button() == Qt::LeftButton) {
2693 int newSample =
static_cast<int>(m_scrollSample
2694 +
static_cast<float>(
event->position().toPoint().x()) * m_samplesPerPixel);
2695 newSample = qMax(newSample, m_firstFileSample);
2696 if (m_lastFileSample >= 0)
2697 newSample = qMin(newSample, m_lastFileSample);
2700 m_annDragging =
false;
2701 m_annDragIndex = -1;
2707 if (m_annSelecting && event->button() == Qt::RightButton) {
2708 m_annSelX1 =
event->position().toPoint().x();
2709 m_annSelecting =
false;
2710 if (m_overlay) m_overlay->repaint();
2712 const int x0 = qMin(m_annSelX0, m_annSelX1);
2713 const int x1 = qMax(m_annSelX0, m_annSelX1);
2714 if (qAbs(x1 - x0) > 3) {
2715 int startSample =
static_cast<int>(m_scrollSample +
static_cast<float>(x0) * m_samplesPerPixel);
2716 int endSample =
static_cast<int>(m_scrollSample +
static_cast<float>(x1) * m_samplesPerPixel);
2718 startSample = qMax(startSample, m_firstFileSample);
2719 if (m_lastFileSample >= 0)
2720 endSample = qMin(endSample, m_lastFileSample);
2722 if (endSample >= startSample)
2729 if (m_rulerActive && event->button() == Qt::RightButton) {
2730 m_rulerRawX1 =
event->position().toPoint().x();
2731 m_rulerRawY1 =
event->position().toPoint().y();
2733 switch (m_rulerSnap) {
2734 case RulerSnap::Horizontal:
2735 m_rulerX1 = m_rulerRawX1; m_rulerY1 = m_rulerY0;
break;
2736 case RulerSnap::Vertical:
2737 m_rulerX1 = m_rulerX0; m_rulerY1 = m_rulerRawY1;
break;
2739 m_rulerX1 = m_rulerRawX1; m_rulerY1 = m_rulerRawY1;
break;
2741 m_rulerActive =
false;
2742 if (m_overlay) m_overlay->repaint();
2748 if (m_dragging && (event->button() == Qt::MiddleButton ||
2749 event->button() == Qt::LeftButton)) {
2754 if (event->button() == Qt::LeftButton && m_leftButtonDown) {
2755 if (!m_leftDragActivated) {
2757 float samplePos = m_leftDownScroll
2758 +
static_cast<float>(
event->position().x()) * m_samplesPerPixel;
2762 if (m_velocityHistory.size() >= 2) {
2763 auto oldest = m_velocityHistory.first();
2764 auto newest = m_velocityHistory.last();
2765 float dt =
static_cast<float>(newest.t - oldest.t);
2767 float dx =
static_cast<float>(newest.x - oldest.x);
2769 float velSampPerMs = -(dx / dt) * m_samplesPerPixel;
2770 float speed = qAbs(velSampPerMs);
2774 float durationMs = qBound(500.f, speed * 1.0f, 5000.f);
2775 float targetScroll = m_scrollSample + velSampPerMs * durationMs / 3.f;
2776 targetScroll = qMax(targetScroll,
static_cast<float>(m_firstFileSample));
2778 m_pInertialAnim =
new QPropertyAnimation(
this,
"scrollSample",
this);
2779 m_pInertialAnim->setDuration(
static_cast<int>(durationMs));
2780 m_pInertialAnim->setEasingCurve(QEasingCurve::OutCubic);
2781 m_pInertialAnim->setStartValue(m_scrollSample);
2782 m_pInertialAnim->setEndValue(targetScroll);
2783 connect(m_pInertialAnim, &QPropertyAnimation::finished,
this, [
this]() {
2784 m_pInertialAnim =
nullptr;
2786 m_pInertialAnim->start(QAbstractAnimation::DeleteWhenStopped);
2791 m_leftButtonDown =
false;
2792 m_leftDragActivated =
false;
2796 QRhiWidget::mouseReleaseEvent(event);
2803 if (!m_model || event->button() != Qt::LeftButton) {
2804 QRhiWidget::mouseDoubleClickEvent(event);
2809 int visCnt = qMin(m_visibleChannelCount, totalCh - m_firstVisibleChannel);
2813 float laneH =
static_cast<float>(height()) / visCnt;
2814 int row =
static_cast<int>(
event->position().y() / laneH);
2815 if (row >= 0 && row < visCnt) {
2816 int ch = actualChannelAt(m_firstVisibleChannel + row);
2818 auto info = m_model->channelInfo(ch);
2819 m_model->setChannelBad(ch, !info.
bad);
Declaration of the ChannelRhiView class.
2-D display widgets and visualisation helpers (charts, topography, colour maps).
ChannelDataModel – lightweight data container for ChannelDataView / ChannelRhiView.
ChannelDisplayInfo channelInfo(int channelIdx) const
QVector< float > decimatedVertices(int channelIdx, int firstSample, int lastSample, int pixelWidth, int &vboFirstSample) const
CrosshairOverlay(ChannelRhiView *parent)
void paintEvent(QPaintEvent *) override
ChannelRhiView – QRhiWidget-based channel signal renderer.
void viewResized(int newWidth, int newHeight)
ChannelRhiView(QWidget *parent=nullptr)
void resizeEvent(QResizeEvent *event) override
void setAnnotationSelectionEnabled(bool enabled)
void setFirstFileSample(int first)
void channelOffsetChanged(int firstChannel)
void samplesPerPixelChanged(float spp)
void setLastFileSample(int last)
void sampleRangeSelected(int startSample, int endSample)
void setEvents(const QVector< EventMarker > &events)
void setAnnotations(const QVector< AnnotationSpan > &annotations)
friend class ::CrosshairOverlay
void render(QRhiCommandBuffer *cb) override
void scrollTo(float targetSample, int durationMs=200)
void setPrefetchFactor(float factor)
void setButterflyMode(bool enabled)
void drawScalebars(QPainter &p)
void setScalebarsVisible(bool visible)
void setSamplesPerPixel(float spp)
void wheelEvent(QWheelEvent *event) override
void annotationBoundaryMoved(int annotationIndex, bool isStartBoundary, int newSample)
int visibleFirstSample() const
void setModel(ChannelDataModel *model)
bool hideBadChannels() const
void setEventsVisible(bool visible)
void setCrosshairEnabled(bool enabled)
void mousePressEvent(QMouseEvent *event) override
void initialize(QRhiCommandBuffer *cb) override
void setScrollSpeedFactor(float factor)
void setClippingVisible(bool visible)
void setEpochMarkersVisible(bool visible)
void mouseMoveEvent(QMouseEvent *event) override
int visibleSampleCount() const
~ChannelRhiView() override
void drawCrosshair(QPainter &p)
void releaseResources() override
void setFirstVisibleChannel(int ch)
void setVisibleChannelCount(int count)
void setSfreq(float sfreq)
void setEpochMarkers(const QVector< int > &triggerSamples)
void setAnnotationsVisible(bool visible)
void setChannelIndices(const QVector< int > &indices)
void drawAnnotationSelectionOverlay(QPainter &p)
bool eventsVisible() const
void setZScoreMode(bool enabled)
int totalLogicalChannels() const
void setHideBadChannels(bool hide)
void sampleClicked(int sample)
void setBackgroundColor(const QColor &color)
void setScrollSample(float sample)
void mouseReleaseEvent(QMouseEvent *event) override
void paintEvent(QPaintEvent *event) override
void cursorDataChanged(float timeSec, float amplitude, const QString &channelName, const QString &unitLabel)
bool annotationsVisible() const
void drawRulerOverlay(QPainter &p)
void setGridVisible(bool visible)
void scrollSampleChanged(float sample)
void zoomTo(float targetSpp, int durationMs=200)
void setFrozen(bool frozen)
void setWheelScrollsChannels(bool channelsMode)
void mouseDoubleClickEvent(QMouseEvent *event) override
Stimulus / event marker — a coloured vertical line at a given sample.
Time-span annotation overlay.