20#include <rhi/qshader.h>
27#include <QApplication>
28#include <QPropertyAnimation>
31#include <QResizeEvent>
51constexpr int kUboOffsetColor = 0;
52constexpr int kUboOffsetFirstSample = 16;
53constexpr int kUboOffsetScrollSample = 20;
54constexpr int kUboOffsetSampPerPixel = 24;
55constexpr int kUboOffsetViewWidth = 28;
56constexpr int kUboOffsetViewHeight = 32;
57constexpr int kUboOffsetChannelYCenter = 36;
58constexpr int kUboOffsetChannelYRange = 40;
59constexpr int kUboOffsetAmplitudeMax = 44;
60constexpr int kUboOffsetShowClipping = 48;
63constexpr int kMaxChannels = 1024;
71static QShader loadShader(
const QString& filename)
74 if (!f.open(QIODevice::ReadOnly)) {
75 qWarning() <<
"ChannelRhiView: cannot open shader" << filename;
78 return QShader::fromSerialized(f.readAll());
81static void writeFloat(quint8* base,
int byteOffset,
float v)
83 memcpy(base + byteOffset, &v,
sizeof(
float));
86static void writeFloats(quint8* base,
int byteOffset,
const float* data,
int count)
88 memcpy(base + byteOffset, data, count *
sizeof(
float));
108 setAttribute(Qt::WA_TransparentForMouseEvents);
109 setAttribute(Qt::WA_NoSystemBackground);
110 setAttribute(Qt::WA_TranslucentBackground);
111 setMouseTracking(
false);
116 setGeometry(0, 0, parentWidget()->width(), parentWidget()->height());
125 p.setRenderHint(QPainter::Antialiasing,
false);
127 if (m_view->crosshairEnabled())
128 m_view->drawCrosshair(p);
129 if (m_view->scalebarsVisible())
130 m_view->drawScalebars(p);
131 if (m_view->rulerActive())
132 m_view->drawRulerOverlay(p);
133 if (m_view->annotationSelecting())
134 m_view->drawAnnotationSelectionOverlay(p);
144 setFocusPolicy(Qt::StrongFocus);
145 setMouseTracking(
true);
146 setContextMenuPolicy(Qt::PreventContextMenu);
153#if defined(WASMBUILD) || defined(__EMSCRIPTEN__)
154 setApi(QRhiWidget::Api::OpenGL);
155#elif defined(Q_OS_MACOS) || defined(Q_OS_IOS)
156 setApi(QRhiWidget::Api::Metal);
157#elif defined(Q_OS_WIN)
158 setApi(QRhiWidget::Api::Direct3D11);
160 setApi(QRhiWidget::Api::OpenGL);
165 setAttribute(Qt::WA_NativeWindow);
169 connect(qApp, &QApplication::applicationStateChanged,
170 this, [
this](Qt::ApplicationState s) {
171 if (s == Qt::ApplicationActive)
184 if (m_model == model)
198 m_pipelineDirty =
true;
203 m_pipelineDirty =
true;
209float ChannelRhiView::clampScrollSample(
float sample)
const
212 if (m_model && m_model->totalSamples() > 0)
213 sample = qMax(sample,
static_cast<float>(m_model->firstSample()));
215 sample = qMax(sample, 0.f);
218 if (m_lastFileSample >= 0) {
220 maxScroll = qMax(maxScroll,
static_cast<float>(m_firstFileSample));
221 sample = qMin(sample, maxScroll);
230 sample = clampScrollSample(sample);
233 if (m_scrollSample == sample)
236 m_scrollSample = sample;
239 float visible = width() * m_samplesPerPixel;
240 float margin = m_prefetchFactor * visible;
241 if (sample < m_vboWindowFirst + margin ||
242 sample + visible > m_vboWindowLast - margin) {
249 if (m_overlayTotalSamples <= 0.f ||
250 sample < m_overlayFirstSample ||
251 sample + visible > m_overlayFirstSample + m_overlayTotalSamples) {
252 m_overlayDirty =
true;
263 spp = qMax(spp, 1e-4f);
264 if (qFuzzyCompare(m_samplesPerPixel, spp))
266 m_samplesPerPixel = spp;
268 m_overlayDirty =
true;
278 stopScrollAnimations();
279 if (durationMs <= 0) {
283 m_scrollTarget = clampScrollSample(targetSample);
284 m_pScrollAnim =
new QPropertyAnimation(
this,
"scrollSample",
this);
285 m_pScrollAnim->setDuration(durationMs);
286 m_pScrollAnim->setEasingCurve(QEasingCurve::OutCubic);
287 m_pScrollAnim->setStartValue(m_scrollSample);
288 m_pScrollAnim->setEndValue(m_scrollTarget);
289 m_pScrollAnim->start(QAbstractAnimation::DeleteWhenStopped);
294void ChannelRhiView::stopScrollAnimations()
297 m_pScrollAnim->stop();
299 if (m_pInertialAnim) {
300 m_pInertialAnim->stop();
301 m_pInertialAnim =
nullptr;
309 scrollTo((m_pScrollAnim ? m_scrollTarget : m_scrollSample) + deltaSamples, durationMs);
316 targetSpp = qMax(targetSpp, 1e-4f);
317 if (durationMs <= 0) {
321 auto* anim =
new QPropertyAnimation(
this,
"samplesPerPixel",
this);
322 anim->setDuration(durationMs);
323 anim->setEasingCurve(QEasingCurve::OutCubic);
324 anim->setStartValue(m_samplesPerPixel);
325 anim->setEndValue(targetSpp);
326 anim->start(QAbstractAnimation::DeleteWhenStopped);
334 m_overlayDirty =
true;
342 m_prefetchFactor = qMax(factor, 0.1f);
349 return static_cast<int>(m_scrollSample);
356 return static_cast<int>(width() * m_samplesPerPixel);
364 ch = qBound(0, ch, maxFirst);
365 if (ch == m_firstVisibleChannel)
367 m_firstVisibleChannel = ch;
369 m_pipelineDirty =
true;
378 count = qMax(1, count);
379 if (count == m_visibleChannelCount)
381 m_visibleChannelCount = count;
383 m_pipelineDirty =
true;
393 stopScrollAnimations();
401 if (visible == m_gridVisible)
403 m_gridVisible = visible;
404 m_overlayDirty =
true;
412 m_sfreq = qMax(sfreq, 0.f);
413 m_overlayDirty =
true;
421 if (first == m_firstFileSample)
423 m_firstFileSample = first;
424 m_overlayDirty =
true;
432 m_lastFileSample = last;
439 if (m_hideBadChannels == hide)
442 const int previousFirstVisibleChannel = m_firstVisibleChannel;
443 m_hideBadChannels = hide;
445 m_firstVisibleChannel = qBound(0, m_firstVisibleChannel, maxFirst);
446 if (m_firstVisibleChannel != previousFirstVisibleChannel) {
450 m_pipelineDirty =
true;
458 m_wheelScrollsChannels = channelsMode;
465 m_scrollSpeedFactor = qBound(0.25f, factor, 4.0f);
472 if (m_crosshairEnabled == enabled)
474 m_crosshairEnabled = enabled;
476 setMouseTracking(
true);
478 setMouseTracking(
false);
479 m_crosshairX = m_crosshairY = -1;
488 if (m_scalebarsVisible == visible)
490 m_scalebarsVisible = visible;
498 if (m_butterflyMode == enabled)
500 m_butterflyMode = enabled;
502 m_pipelineDirty =
true;
503 m_overlayDirty =
true;
509QVector<ChannelRhiView::ButterflyTypeGroup> ChannelRhiView::butterflyTypeGroups()
const
511 QVector<ButterflyTypeGroup> groups;
515 const QVector<int> allCh = effectiveChannelIndices();
516 QMap<QString, int> typeToGroup;
518 for (
int ch : allCh) {
519 auto info = m_model->channelInfo(ch);
520 if (m_hideBadChannels && info.bad)
523 if (typeToGroup.contains(info.typeLabel)) {
524 gIdx = typeToGroup[info.typeLabel];
526 gIdx = groups.size();
527 typeToGroup[info.typeLabel] = gIdx;
528 ButterflyTypeGroup g;
529 g.typeLabel = info.typeLabel;
530 g.color = info.color;
531 g.amplitudeMax = info.amplitudeMax;
534 groups[gIdx].channelIndices.append(ch);
541int ChannelRhiView::butterflyLaneCount()
const
545 const QVector<int> allCh = effectiveChannelIndices();
547 for (
int ch : allCh) {
548 auto info = m_model->channelInfo(ch);
549 if (m_hideBadChannels && info.bad)
551 types.insert(info.typeLabel);
560 const int previousFirstVisibleChannel = m_firstVisibleChannel;
561 m_filteredChannels = indices;
564 m_firstVisibleChannel = qBound(0, m_firstVisibleChannel, maxFirst);
565 if (m_firstVisibleChannel != previousFirstVisibleChannel) {
569 m_pipelineDirty =
true;
577 return effectiveChannelIndices().size();
582int ChannelRhiView::actualChannelAt(
int logicalIdx)
const
584 const QVector<int> indices = effectiveChannelIndices();
585 if (logicalIdx < 0 || logicalIdx >= indices.size())
587 return indices.at(logicalIdx);
592QVector<int> ChannelRhiView::effectiveChannelIndices()
const
594 QVector<int> indices;
600 if (m_filteredChannels.isEmpty()) {
601 indices.reserve(m_model->channelCount());
602 for (
int channelIndex = 0; channelIndex < m_model->channelCount(); ++channelIndex) {
603 indices.append(channelIndex);
606 indices = m_filteredChannels;
609 if (!m_hideBadChannels) {
613 QVector<int> visibleIndices;
614 visibleIndices.reserve(indices.size());
615 for (
int channelIndex : std::as_const(indices)) {
616 if (channelIndex < 0) {
620 const ChannelDisplayInfo info = m_model->channelInfo(channelIndex);
622 visibleIndices.append(channelIndex);
626 return visibleIndices;
634 m_overlayDirty =
true;
642 m_epochTriggerSamples = triggerSamples;
643 m_overlayDirty =
true;
651 if (m_bShowEpochMarkers == visible)
653 m_bShowEpochMarkers = visible;
654 m_overlayDirty =
true;
662 if (m_bShowClipping == visible)
664 m_bShowClipping = visible;
672 if (m_bZScoreMode == enabled)
674 m_bZScoreMode = enabled;
683 m_annotations = annotations;
684 m_overlayDirty =
true;
692 m_annotationSelectionEnabled = enabled;
699 if (m_bShowEvents == visible)
701 m_bShowEvents = visible;
702 m_overlayDirty =
true;
708 return m_bShowEvents;
715 if (m_bShowAnnotations == visible)
717 m_bShowAnnotations = visible;
718 m_overlayDirty =
true;
724 return m_bShowAnnotations;
729int ChannelRhiView::hitTestAnnotationBoundary(
int px,
bool& isStart)
const
731 for (
int i = 0; i < m_annotations.size(); ++i) {
732 const float xStart = (
static_cast<float>(m_annotations[i].startSample) - m_scrollSample) / m_samplesPerPixel;
733 const float xEnd = (
static_cast<float>(m_annotations[i].endSample + 1) - m_scrollSample) / m_samplesPerPixel;
735 if (qAbs(px -
static_cast<int>(xStart)) <= kAnnBoundaryHitPx) {
739 if (qAbs(px -
static_cast<int>(xEnd)) <= kAnnBoundaryHitPx) {
751 m_pipelineDirty =
true;
762 m_gpuChannels.clear();
763 m_pipelineDirty =
true;
766 m_overlayPipeline.reset();
767 m_overlaySrb.reset();
768 m_overlaySampler.reset();
769 m_overlayTex.reset();
770 m_overlayVbo.reset();
771 m_overlayDirty =
true;
776void ChannelRhiView::ensurePipeline(QRhi* rhi, QRhiRenderTarget* target)
778 if (!m_pipelineDirty)
781 m_uboStride =
static_cast<int>(
782 (52 + rhi->ubufAlignment() - 1) & ~(rhi->ubufAlignment() - 1));
788 if (m_butterflyMode) {
789 nCh = qMin(totalCh, kMaxChannels);
791 nCh = qMin(m_visibleChannelCount, totalCh - m_firstVisibleChannel);
794 nCh = qMin(nCh, kMaxChannels);
797 bool uboRecreated =
false;
798 if (!m_ubo || m_ubo->size() <
static_cast<quint32
>(nCh * m_uboStride)) {
799 m_ubo.reset(rhi->newBuffer(QRhiBuffer::Dynamic,
800 QRhiBuffer::UniformBuffer,
808 if (!m_srb || uboRecreated) {
809 m_srb.reset(rhi->newShaderResourceBindings());
810 m_srb->setBindings({QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(
812 QRhiShaderResourceBinding::VertexStage |
813 QRhiShaderResourceBinding::FragmentStage,
822 QShader vs = loadShader(QStringLiteral(
":/disp/shaders/viewers/helpers/shaders/channeldata.vert.qsb"));
823 QShader fs = loadShader(QStringLiteral(
":/disp/shaders/viewers/helpers/shaders/channeldata.frag.qsb"));
825 if (!vs.isValid() || !fs.isValid()) {
826 qWarning() <<
"ChannelRhiView: shaders not found. "
827 "Ensure qt_add_shaders is configured in CMakeLists.";
834 m_pipeline.reset(rhi->newGraphicsPipeline());
835 m_pipeline->setShaderStages({{QRhiShaderStage::Vertex, vs},
836 {QRhiShaderStage::Fragment, fs}});
838 QRhiVertexInputLayout il;
839 il.setBindings({{2 *
sizeof(float)}});
840 il.setAttributes({{0, 0, QRhiVertexInputAttribute::Float2, 0}});
842 m_pipeline->setVertexInputLayout(il);
843 m_pipeline->setShaderResourceBindings(m_srb.get());
844 m_pipeline->setRenderPassDescriptor(target->renderPassDescriptor());
845 m_pipeline->setTopology(QRhiGraphicsPipeline::LineStrip);
846 m_pipeline->setDepthTest(
false);
847 m_pipeline->setDepthWrite(
false);
850 QRhiGraphicsPipeline::TargetBlend blend;
852 blend.srcColor = QRhiGraphicsPipeline::SrcAlpha;
853 blend.dstColor = QRhiGraphicsPipeline::OneMinusSrcAlpha;
854 blend.srcAlpha = QRhiGraphicsPipeline::One;
855 blend.dstAlpha = QRhiGraphicsPipeline::OneMinusSrcAlpha;
856 m_pipeline->setTargetBlends({blend});
858 if (!m_pipeline->create()) {
859 qWarning() <<
"ChannelRhiView: failed to create graphics pipeline";
864 m_pipelineDirty =
false;
869bool ChannelRhiView::isVboDirty()
const
873 float visible = width() * m_samplesPerPixel;
874 float margin = m_prefetchFactor * visible;
875 return m_vboDirty || m_scrollSample < m_vboWindowFirst + margin || (m_scrollSample + visible) > m_vboWindowLast - margin;
880void ChannelRhiView::rebuildVBOs(QRhi* rhi, QRhiResourceUpdateBatch* batch)
887 float visible = px * m_samplesPerPixel;
890 float windowFirst = m_scrollSample - m_prefetchFactor * visible;
891 float windowLast = m_scrollSample + (1.f + m_prefetchFactor) * visible;
893 int iFirst = qMax(
static_cast<int>(windowFirst), m_model->firstSample());
894 int iLast = qMin(
static_cast<int>(windowLast),
895 m_model->firstSample() + m_model->totalSamples());
896 if (iFirst >= iLast) {
901 m_vboWindowFirst = iFirst;
902 m_vboWindowLast = iLast;
905 m_gpuChannels.resize(nCh);
908 int prefetchedSamples = iLast - iFirst;
911 int maxVertices = qMax(prefetchedSamples * 2, 2 * px * 4);
912 Q_UNUSED(maxVertices)
914 for (
int logCh = 0; logCh < nCh; ++logCh) {
915 int ch = actualChannelAt(logCh);
917 m_gpuChannels[logCh].vertexCount = 0;
921 QVector<float> verts = m_model->decimatedVertices(
922 ch, iFirst, iLast,
static_cast<int>(prefetchedSamples / m_samplesPerPixel), vboFirst);
924 if (verts.isEmpty()) {
925 m_gpuChannels[logCh].vertexCount = 0;
931 int nv = verts.size() / 2;
933 double sum = 0.0, sumSq = 0.0;
934 for (
int v = 0; v < nv; ++v) {
935 double a =
static_cast<double>(verts[v * 2 + 1]);
939 float mean =
static_cast<float>(sum / nv);
940 double var = sumSq / nv -
static_cast<double>(mean) * mean;
941 float sd = var > 0.0 ?
static_cast<float>(qSqrt(var)) : 1.f;
942 for (
int v = 0; v < nv; ++v)
943 verts[v * 2 + 1] = (verts[v * 2 + 1] - mean) / sd;
947 int vertexCount = verts.size() / 2;
948 quint32 byteSize =
static_cast<quint32
>(verts.size() *
sizeof(
float));
950 auto& gd = m_gpuChannels[logCh];
953 if (!gd.vbo ||
static_cast<quint32
>(gd.vbo->size()) < byteSize) {
954 gd.vbo.reset(rhi->newBuffer(QRhiBuffer::Dynamic,
955 QRhiBuffer::VertexBuffer,
957 if (!gd.vbo->create()) {
958 qWarning() <<
"ChannelRhiView: VBO create failed for channel" << logCh;
964 batch->updateDynamicBuffer(gd.vbo.get(), 0, byteSize,
966 gd.vertexCount = vertexCount;
967 gd.vboFirstSample = vboFirst;
975void ChannelRhiView::updateUBO(QRhiResourceUpdateBatch* batch)
977 if (!m_model || !m_ubo)
983 if (m_butterflyMode) {
984 const auto groups = butterflyTypeGroups();
985 int nLanes = groups.size();
990 QHash<int, int> chToLane;
991 for (
int g = 0; g < groups.size(); ++g)
992 for (
int ch : groups[g].channelIndices)
995 float vw =
static_cast<float>(width());
996 float vh =
static_cast<float>(height());
997 float laneRange = 2.f / nLanes;
999 QVarLengthArray<quint8> buf(m_uboStride, 0);
1000 int nToUpload = qMin(totalCh, kMaxChannels);
1002 for (
int logCh = 0; logCh < nToUpload; ++logCh) {
1003 int ch = actualChannelAt(logCh);
1004 memset(buf.data(), 0, m_uboStride);
1006 auto info = (ch >= 0) ? m_model->channelInfo(ch) : ChannelDisplayInfo{};
1007 bool hideThis = (ch < 0) || (m_hideBadChannels && info.
bad);
1009 int lane = chToLane.value(ch, -1);
1013 QColor col = hideThis ? m_bgColor
1014 : (info.
bad ? QColor(200, 60, 60, 180) : info.color);
1015 float yRng = hideThis ? 0.f : laneRange;
1017 float yCenter = (lane >= 0)
1018 ? (1.f - laneRange * (lane + 0.5f))
1022 static_cast<float>(col.redF()),
1023 static_cast<float>(col.greenF()),
1024 static_cast<float>(col.blueF()),
1025 static_cast<float>(col.alphaF())};
1027 auto* d = buf.data();
1028 writeFloats(d, kUboOffsetColor, rgba, 4);
1029 writeFloat(d, kUboOffsetFirstSample,
static_cast<float>(logCh <
static_cast<int>(m_gpuChannels.size()) ? m_gpuChannels[logCh].vboFirstSample : 0));
1030 writeFloat(d, kUboOffsetScrollSample, m_scrollSample);
1031 writeFloat(d, kUboOffsetSampPerPixel, m_samplesPerPixel);
1032 writeFloat(d, kUboOffsetViewWidth, vw);
1033 writeFloat(d, kUboOffsetViewHeight, vh);
1034 writeFloat(d, kUboOffsetChannelYCenter, yCenter);
1035 writeFloat(d, kUboOffsetChannelYRange, yRng);
1036 writeFloat(d, kUboOffsetAmplitudeMax, m_bZScoreMode ? 4.f : info.
amplitudeMax);
1037 writeFloat(d, kUboOffsetShowClipping, (m_bShowClipping && !info.
bad && !m_bZScoreMode) ? 1.f : 0.f);
1039 batch->updateDynamicBuffer(m_ubo.get(),
1040 logCh * m_uboStride,
1048 int firstCh = qBound(0, m_firstVisibleChannel, totalCh);
1049 int visCnt = qMin(m_visibleChannelCount, totalCh - firstCh);
1050 int nCh = qMin(visCnt, kMaxChannels);
1054 float vw =
static_cast<float>(width());
1055 float vh =
static_cast<float>(height());
1056 float laneRange = 2.f / nCh;
1058 QVarLengthArray<quint8> buf(m_uboStride, 0);
1060 for (
int i = 0; i < nCh; ++i) {
1061 int logCh = firstCh + i;
1062 int ch = actualChannelAt(logCh);
1063 memset(buf.data(), 0, m_uboStride);
1065 auto info = (ch >= 0) ? m_model->channelInfo(ch) : ChannelDisplayInfo{};
1066 bool hideThis = (ch < 0) || (m_hideBadChannels && info.
bad);
1068 QColor col = hideThis ? m_bgColor
1069 : (info.
bad ? QColor(200, 60, 60, 180) : info.color);
1071 float yRng = hideThis ? 0.f : laneRange;
1074 static_cast<float>(col.redF()),
1075 static_cast<float>(col.greenF()),
1076 static_cast<float>(col.blueF()),
1077 static_cast<float>(col.alphaF())};
1080 float yCenter = 1.f - laneRange * (i + 0.5f);
1082 auto* d = buf.data();
1083 writeFloats(d, kUboOffsetColor, rgba, 4);
1085 writeFloat(d, kUboOffsetFirstSample,
static_cast<float>(logCh <
static_cast<int>(m_gpuChannels.size()) ? m_gpuChannels[logCh].vboFirstSample : 0));
1086 writeFloat(d, kUboOffsetScrollSample, m_scrollSample);
1087 writeFloat(d, kUboOffsetSampPerPixel, m_samplesPerPixel);
1088 writeFloat(d, kUboOffsetViewWidth, vw);
1089 writeFloat(d, kUboOffsetViewHeight, vh);
1090 writeFloat(d, kUboOffsetChannelYCenter, yCenter);
1091 writeFloat(d, kUboOffsetChannelYRange, yRng);
1092 writeFloat(d, kUboOffsetAmplitudeMax, m_bZScoreMode ? 4.f : info.
amplitudeMax);
1093 writeFloat(d, kUboOffsetShowClipping, (m_bShowClipping && !info.
bad && !m_bZScoreMode) ? 1.f : 0.f);
1096 batch->updateDynamicBuffer(m_ubo.get(),
1114void ChannelRhiView::rebuildOverlayImage(
int logicalWidth,
int logicalHeight, qreal devicePixelRatio)
1116 const qreal dpr = qMax(devicePixelRatio, 1.0);
1117 const int pixelWidth = qMax(1, qRound(logicalWidth * dpr));
1118 const int pixelHeight = qMax(1, qRound(logicalHeight * dpr));
1120 m_overlayImage = QImage(pixelWidth, pixelHeight, QImage::Format_RGBA8888);
1121 m_overlayImage.setDevicePixelRatio(dpr);
1122 m_overlayImage.fill(Qt::transparent);
1124 if (logicalWidth <= 0 || logicalHeight <= 0 || m_sfreq <= 0.f || m_samplesPerPixel <= 0.f) {
1125 m_overlayDirty =
false;
1131 const float overlayFirst = m_overlayFirstSample;
1132 const float overlayTotal = m_overlayTotalSamples;
1133 const float overlayPixelsPerSample = (overlayTotal > 0.f)
1134 ?
static_cast<float>(logicalWidth) / overlayTotal
1137 QPainter p(&m_overlayImage);
1138 p.setCompositionMode(QPainter::CompositionMode_SourceOver);
1144 if (m_bShowAnnotations && !m_annotations.isEmpty()) {
1145 QFont font = p.font();
1146 font.setPointSizeF(8.0);
1151 const float xStart = (
static_cast<float>(annotation.startSample) - overlayFirst) * overlayPixelsPerSample;
1152 const float xEnd = (
static_cast<float>(annotation.endSample + 1) - overlayFirst) * overlayPixelsPerSample;
1153 if (xEnd < -2.f || xStart > logicalWidth + 2.f) {
1157 const float clippedStart = qBound(0.f, xStart,
static_cast<float>(logicalWidth));
1158 const float clippedEnd = qBound(0.f, xEnd,
static_cast<float>(logicalWidth));
1159 if (clippedEnd <= clippedStart) {
1163 QColor fillColor = annotation.color;
1164 fillColor.setAlpha(48);
1165 p.fillRect(QRectF(clippedStart, 0.f, clippedEnd - clippedStart,
static_cast<float>(logicalHeight)),
1168 QColor borderColor = annotation.color;
1169 borderColor.setAlpha(165);
1170 p.setPen(QPen(borderColor, 1));
1171 p.drawLine(QPointF(clippedStart, 0.f), QPointF(clippedStart,
static_cast<float>(logicalHeight)));
1172 p.drawLine(QPointF(clippedEnd, 0.f), QPointF(clippedEnd,
static_cast<float>(logicalHeight)));
1174 if (!annotation.label.trimmed().isEmpty()) {
1175 QString label = annotation.label.trimmed();
1176 QFontMetrics metrics(font);
1177 QRect labelRect = metrics.boundingRect(label);
1178 labelRect.adjust(-6, -2, 6, 2);
1179 const int labelX = qBound(4,
1180 static_cast<int>(clippedStart) + 4,
1181 qMax(4, logicalWidth - labelRect.width() - 4));
1182 labelRect.moveTopLeft(QPoint(labelX, 4));
1183 QColor pillColor = annotation.color;
1184 pillColor.setAlpha(215);
1185 p.fillRect(labelRect, pillColor);
1186 p.setPen(Qt::white);
1187 p.drawText(labelRect, Qt::AlignCenter, label);
1193 if (m_bShowEvents && !m_events.isEmpty()) {
1195 float xF = (
static_cast<float>(ev.sample) - overlayFirst) * overlayPixelsPerSample;
1196 if (xF < -2.f || xF > logicalWidth + 2.f)
1198 QColor lineColor = ev.color;
1199 lineColor.setAlpha(180);
1200 p.setPen(QPen(lineColor, 1));
1201 p.drawLine(QPointF(xF, 0.f), QPointF(xF,
static_cast<float>(logicalHeight)));
1206 if (m_bShowEpochMarkers && !m_epochTriggerSamples.isEmpty()) {
1207 QPen epochPen(QColor(100, 100, 100, 140), 1, Qt::DashLine);
1209 for (
int trigSample : m_epochTriggerSamples) {
1210 float xF = (
static_cast<float>(trigSample) - overlayFirst) * overlayPixelsPerSample;
1211 if (xF < -2.f || xF > logicalWidth + 2.f)
1213 p.drawLine(QPointF(xF, 0.f), QPointF(xF,
static_cast<float>(logicalHeight)));
1217 m_overlayDirty =
false;
1222void ChannelRhiView::ensureOverlayPipeline(QRhi* rhi, QRhiRenderTarget* target)
1224 if (m_overlayPipeline)
1234 static constexpr float kQuadVerts[] = {
1252 static constexpr int kQuadBytes =
sizeof(kQuadVerts);
1253 m_overlayVbo.reset(rhi->newBuffer(QRhiBuffer::Immutable,
1254 QRhiBuffer::VertexBuffer,
1256 if (!m_overlayVbo->create()) {
1257 m_overlayVbo.reset();
1262 m_overlayTex.reset(rhi->newTexture(QRhiTexture::RGBA8, QSize(1, 1)));
1263 m_overlayTex->create();
1265 m_overlaySampler.reset(rhi->newSampler(
1266 QRhiSampler::Linear, QRhiSampler::Linear,
1268 QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge));
1269 m_overlaySampler->create();
1273 static constexpr int kOverlayUboSize = 256;
1274 m_overlayUbo.reset(rhi->newBuffer(QRhiBuffer::Dynamic,
1275 QRhiBuffer::UniformBuffer,
1277 if (!m_overlayUbo->create()) {
1278 m_overlayVbo.reset();
1279 m_overlayUbo.reset();
1284 m_overlaySrb.reset(rhi->newShaderResourceBindings());
1285 m_overlaySrb->setBindings({QRhiShaderResourceBinding::sampledTexture(
1286 1, QRhiShaderResourceBinding::FragmentStage,
1287 m_overlayTex.get(), m_overlaySampler.get()),
1288 QRhiShaderResourceBinding::uniformBuffer(
1289 2, QRhiShaderResourceBinding::FragmentStage,
1290 m_overlayUbo.get())});
1291 m_overlaySrb->create();
1293 auto loadShader = [](
const QString& path) -> QShader {
1295 if (!f.open(QIODevice::ReadOnly)) {
1296 qWarning() <<
"ChannelRhiView: cannot open shader" << path;
1299 return QShader::fromSerialized(f.readAll());
1302 QShader vs = loadShader(QStringLiteral(
":/disp/shaders/viewers/helpers/shaders/overlay.vert.qsb"));
1303 QShader fs = loadShader(QStringLiteral(
":/disp/shaders/viewers/helpers/shaders/overlay.frag.qsb"));
1304 if (!vs.isValid() || !fs.isValid()) {
1305 m_overlayVbo.reset();
1306 m_overlayUbo.reset();
1307 m_overlaySrb.reset();
1308 m_overlaySampler.reset();
1309 m_overlayTex.reset();
1313 m_overlayPipeline.reset(rhi->newGraphicsPipeline());
1314 QRhiGraphicsPipeline::TargetBlend blend;
1315 blend.enable =
true;
1316 blend.srcColor = QRhiGraphicsPipeline::SrcAlpha;
1317 blend.dstColor = QRhiGraphicsPipeline::OneMinusSrcAlpha;
1318 blend.srcAlpha = QRhiGraphicsPipeline::One;
1319 blend.dstAlpha = QRhiGraphicsPipeline::OneMinusSrcAlpha;
1320 m_overlayPipeline->setTargetBlends({blend});
1321 m_overlayPipeline->setTopology(QRhiGraphicsPipeline::TriangleStrip);
1322 m_overlayPipeline->setDepthTest(
false);
1323 m_overlayPipeline->setDepthWrite(
false);
1324 m_overlayPipeline->setShaderStages({
1325 {QRhiShaderStage::Vertex, vs},
1326 {QRhiShaderStage::Fragment, fs},
1329 QRhiVertexInputLayout inputLayout;
1330 inputLayout.setBindings({QRhiVertexInputBinding(4 *
sizeof(
float))});
1331 inputLayout.setAttributes({QRhiVertexInputAttribute(0, 0, QRhiVertexInputAttribute::Float2, 0),
1332 QRhiVertexInputAttribute(0, 1, QRhiVertexInputAttribute::Float2, 2 *
sizeof(
float))});
1333 m_overlayPipeline->setVertexInputLayout(inputLayout);
1334 m_overlayPipeline->setShaderResourceBindings(m_overlaySrb.get());
1335 m_overlayPipeline->setRenderPassDescriptor(target->renderPassDescriptor());
1337 if (!m_overlayPipeline->create()) {
1338 qWarning() <<
"ChannelRhiView: overlay pipeline create failed";
1339 m_overlayPipeline.reset();
1346 m_overlayVboNeedsUpload =
true;
1362 QRhiResourceUpdateBatch* u = rhi->nextResourceUpdateBatch();
1363 QColor bg = m_bgColor;
1364 cb->beginPass(target, bg, {1.f, 0}, u);
1370 ensurePipeline(rhi, target);
1373 QRhiResourceUpdateBatch* u = rhi->nextResourceUpdateBatch();
1374 cb->beginPass(target, QColor(220, 0, 0), {1.f, 0}, u);
1379 QSize ps = target->pixelSize();
1380 const int pw = ps.width();
1381 const int ph = ps.height();
1382 const int logicalW = width();
1383 const int logicalH = height();
1384 const qreal overlayDpr = (logicalW > 0) ? (
static_cast<qreal
>(pw) /
static_cast<qreal
>(logicalW)) : 1.0;
1386 QRhiResourceUpdateBatch* batch = rhi->nextResourceUpdateBatch();
1389 rebuildVBOs(rhi, batch);
1394 ensureOverlayPipeline(rhi, target);
1395 bool overlayReady =
false;
1396 if (m_overlayPipeline && m_overlayVbo && m_overlayUbo && pw > 0 && ph > 0 && logicalW > 0 && logicalH > 0) {
1398 if (m_overlayVboNeedsUpload) {
1399 static constexpr float kQuadVerts[] = {
1417 batch->uploadStaticBuffer(m_overlayVbo.get(), 0,
1418 static_cast<quint32
>(
sizeof(kQuadVerts)), kQuadVerts);
1419 m_overlayVboNeedsUpload =
false;
1423 const float visibleSamples =
static_cast<float>(logicalW) * m_samplesPerPixel;
1424 const float extraSamples = kOverlayPrefetchFactor * visibleSamples;
1425 const float overlayFirstSample = m_scrollSample - extraSamples;
1426 const float overlayTotalSamples = visibleSamples + 2.0f * extraSamples;
1428 const int overlayLogicalW =
static_cast<int>(std::ceil(
1429 (1.0f + 2.0f * kOverlayPrefetchFactor) *
static_cast<float>(logicalW)));
1432 const QSize requiredTexSize(qRound(overlayLogicalW * overlayDpr),
1433 qRound(logicalH * overlayDpr));
1434 if (m_overlayDirty || requiredTexSize != m_overlayTexSize) {
1436 m_overlayFirstSample = overlayFirstSample;
1437 m_overlayTotalSamples = overlayTotalSamples;
1439 rebuildOverlayImage(overlayLogicalW, logicalH, overlayDpr);
1442 if (requiredTexSize != m_overlayTexSize) {
1443 m_overlayTex.reset(rhi->newTexture(QRhiTexture::RGBA8, requiredTexSize));
1444 m_overlayTex->create();
1446 m_overlaySrb->setBindings({QRhiShaderResourceBinding::sampledTexture(
1447 1, QRhiShaderResourceBinding::FragmentStage,
1448 m_overlayTex.get(), m_overlaySampler.get()),
1449 QRhiShaderResourceBinding::uniformBuffer(
1450 2, QRhiShaderResourceBinding::FragmentStage,
1451 m_overlayUbo.get())});
1452 m_overlaySrb->create();
1453 m_overlayTexSize = requiredTexSize;
1455 QRhiTextureUploadEntry entry(0, 0, QRhiTextureSubresourceUploadDescription(m_overlayImage));
1456 batch->uploadTexture(m_overlayTex.get(), entry);
1460 struct OverlayParams
1466 float firstFileSample;
1468 float overlayFirstSample;
1469 float overlayTotalSamples;
1471 OverlayParams params;
1472 params.scrollSample = m_scrollSample;
1473 params.samplesPerPixel = m_samplesPerPixel;
1474 params.viewWidth =
static_cast<float>(logicalW);
1475 params.sfreq = m_sfreq;
1476 params.firstFileSample =
static_cast<float>(m_firstFileSample);
1477 params.gridEnabled = m_gridVisible ? 1.0f : 0.0f;
1478 params.overlayFirstSample = m_overlayFirstSample;
1479 params.overlayTotalSamples = m_overlayTotalSamples;
1480 batch->updateDynamicBuffer(m_overlayUbo.get(), 0,
1481 static_cast<quint32
>(
sizeof(params)), ¶ms);
1483 overlayReady =
true;
1487 QColor bg = m_bgColor;
1488 cb->beginPass(target, bg, {1.f, 0}, batch);
1490 cb->setViewport(QRhiViewport(0.f, 0.f,
static_cast<float>(pw),
1491 static_cast<float>(ph)));
1494 cb->setGraphicsPipeline(m_pipeline.get());
1498 if (m_butterflyMode) {
1500 int nToRender = qMin(totalCh, kMaxChannels);
1501 for (
int logCh = 0; logCh < nToRender; ++logCh) {
1502 if (logCh >=
static_cast<int>(m_gpuChannels.size()))
1504 auto& gd = m_gpuChannels[logCh];
1505 if (!gd.vbo || gd.vertexCount < 2)
1508 quint32 dynOffset =
static_cast<quint32
>(logCh * m_uboStride);
1509 QRhiCommandBuffer::DynamicOffset dynOff{0, dynOffset};
1510 cb->setShaderResources(m_srb.get(), 1, &dynOff);
1512 QRhiCommandBuffer::VertexInput vi(gd.vbo.get(), 0);
1513 cb->setVertexInput(0, 1, &vi);
1514 cb->draw(
static_cast<quint32
>(gd.vertexCount));
1518 int firstCh = qBound(0, m_firstVisibleChannel, totalCh);
1519 int visCnt = qMin(m_visibleChannelCount, totalCh - firstCh);
1520 int nToRender = qMin(visCnt, kMaxChannels);
1522 for (
int i = 0; i < nToRender; ++i) {
1523 int logCh = firstCh + i;
1524 if (logCh >=
static_cast<int>(m_gpuChannels.size()))
1526 auto& gd = m_gpuChannels[logCh];
1527 if (!gd.vbo || gd.vertexCount < 2)
1530 quint32 dynOffset =
static_cast<quint32
>(i * m_uboStride);
1531 QRhiCommandBuffer::DynamicOffset dynOff{0, dynOffset};
1532 cb->setShaderResources(m_srb.get(), 1, &dynOff);
1534 QRhiCommandBuffer::VertexInput vi(gd.vbo.get(), 0);
1535 cb->setVertexInput(0, 1, &vi);
1536 cb->draw(
static_cast<quint32
>(gd.vertexCount));
1542 cb->setGraphicsPipeline(m_overlayPipeline.get());
1543 cb->setShaderResources(m_overlaySrb.get());
1544 QRhiCommandBuffer::VertexInput overlayVi(m_overlayVbo.get(), 0);
1545 cb->setVertexInput(0, 1, &overlayVi);
1554 QRhiWidget::paintEvent(event);
1562 const int pw = size.isValid() ? size.width() : width();
1563 const int ph = size.isValid() ? size.height() : height();
1564 if (!m_model || pw <= 0 || ph <= 0 || m_samplesPerPixel <= 0.f) {
1569 const float spp = m_samplesPerPixel *
static_cast<float>(qMax(width(), 1)) /
static_cast<float>(pw);
1571 const QColor bgColor = m_bgColor;
1573 const float sfreq = m_sfreq;
1574 const int firstFileSample = m_firstFileSample;
1576 const bool showClipping = m_bShowClipping;
1577 const QVector<EventMarker> events = m_bShowEvents ? m_events : QVector<EventMarker>();
1578 const QVector<AnnotationSpan> annotations = m_bShowAnnotations ? m_annotations : QVector<AnnotationSpan>();
1579 const QVector<int> epochMarkers = m_bShowEpochMarkers ? m_epochTriggerSamples : QVector<int>();
1583 QVector<QPair<int, int>> traces;
1585 if (m_butterflyMode) {
1586 const QVector<ButterflyTypeGroup> groups = butterflyTypeGroups();
1587 laneCount =
static_cast<int>(groups.size());
1588 for (
int lane = 0; lane < laneCount; ++lane) {
1589 for (
int ch : groups[lane].channelIndices) {
1590 traces.append({ch, lane});
1594 const QVector<int> channelIndices = effectiveChannelIndices();
1595 const int firstCh = qBound(0, m_firstVisibleChannel,
static_cast<int>(channelIndices.size()));
1596 laneCount = qMin(m_visibleChannelCount,
static_cast<int>(channelIndices.size()) - firstCh);
1597 for (
int lane = 0; lane < laneCount; ++lane) {
1598 traces.append({channelIndices[firstCh + lane], lane});
1601 if (laneCount <= 0) {
1602 QImage blank(pw, ph, QImage::Format_RGB32);
1603 blank.fill(bgColor.rgb());
1607 QImage img(pw, ph, QImage::Format_RGB32);
1608 img.fill(bgColor.rgb());
1611 p.setRenderHint(QPainter::Antialiasing,
false);
1613 const float laneH =
static_cast<float>(ph) /
static_cast<float>(laneCount);
1615 int lastSample = firstSample +
static_cast<int>(pw * spp) + 1;
1620 float samplesPerSec = sfreq;
1621 float firstBound = std::floor(
1622 (
scrollSample -
static_cast<float>(firstFileSample)) / samplesPerSec) *
1624 static_cast<float>(firstFileSample);
1627 long long bandIndex =
static_cast<long long>(
1628 (firstBound -
static_cast<float>(firstFileSample)) / samplesPerSec);
1629 bool oddBand = (bandIndex & 1) != 0;
1632 const QColor altColor(qRound(bgColor.red() * 0.92),
1633 qRound(bgColor.green() * 0.92),
1634 qRound(bgColor.blue() * 0.92));
1636 for (
float s = firstBound; s < lastSample; s += samplesPerSec, oddBand = !oddBand) {
1640 float xEnd = xStart + samplesPerSec / spp;
1641 xStart = qBound(0.f, xStart,
static_cast<float>(pw));
1642 xEnd = qBound(0.f, xEnd,
static_cast<float>(pw));
1644 p.fillRect(QRectF(xStart, 0, xEnd - xStart, ph), altColor);
1650 for (
int i = 0; i < laneCount; ++i) {
1651 float yMid = (i + 0.5f) * laneH;
1652 float yTop = i * laneH;
1655 p.setPen(QPen(QColor(205, 205, 215), 1));
1656 p.drawLine(QPointF(0, yTop), QPointF(pw, yTop));
1659 QPen guidePen(QColor(228, 228, 235), 1, Qt::DotLine);
1660 guidePen.setDashPattern({3, 4});
1662 p.drawLine(QPointF(0, yMid - laneH * 0.44f), QPointF(pw, yMid - laneH * 0.44f));
1663 p.drawLine(QPointF(0, yMid + laneH * 0.44f), QPointF(pw, yMid + laneH * 0.44f));
1665 p.setPen(QPen(QColor(210, 210, 218), 1));
1666 p.drawLine(QPointF(0, yMid), QPointF(pw, yMid));
1670 static const float kNiceIntervals[] = {
1671 0.05f, 0.1f, 0.2f, 0.5f, 1.f, 2.f, 5.f, 10.f, 30.f, 60.f};
1672 float pxPerSecond = sfreq / spp;
1673 float tickIntervalS = kNiceIntervals[0];
1674 for (
float iv : kNiceIntervals) {
1676 if (iv * pxPerSecond >= 80.f)
1679 float tickSamples = tickIntervalS * sfreq;
1680 float origin =
static_cast<float>(firstFileSample);
1681 float firstTick = std::ceil((
scrollSample - origin) / tickSamples) * tickSamples + origin;
1683 p.setPen(QPen(QColor(205, 205, 210), 1));
1684 for (
float s = firstTick; s < lastSample; s += tickSamples) {
1686 p.drawLine(QPointF(xPx, 0), QPointF(xPx, ph));
1692 if (!annotations.isEmpty()) {
1693 QFont font = p.font();
1694 font.setPointSizeF(8.0);
1699 float xStart = (
static_cast<float>(annotation.startSample) -
scrollSample) / spp;
1700 float xEnd = (
static_cast<float>(annotation.endSample + 1) -
scrollSample) / spp;
1702 if (xEnd < -2.f || xStart > pw + 2.f) {
1706 xStart = qBound(0.f, xStart,
static_cast<float>(pw));
1707 xEnd = qBound(0.f, xEnd,
static_cast<float>(pw));
1708 if (xEnd <= xStart) {
1712 QColor fillColor = annotation.color;
1713 fillColor.setAlpha(48);
1714 p.fillRect(QRectF(xStart, 0.f, xEnd - xStart,
static_cast<float>(ph)), fillColor);
1716 QColor borderColor = annotation.color;
1717 borderColor.setAlpha(165);
1718 p.setPen(QPen(borderColor, 1));
1719 p.drawLine(QPointF(xStart, 0.f), QPointF(xStart,
static_cast<float>(ph)));
1720 p.drawLine(QPointF(xEnd, 0.f), QPointF(xEnd,
static_cast<float>(ph)));
1722 if (!annotation.label.trimmed().isEmpty()) {
1723 const QString label = annotation.label.trimmed();
1724 QFontMetrics metrics(font);
1725 QRect labelRect = metrics.boundingRect(label);
1726 labelRect.adjust(-6, -2, 6, 2);
1727 const int labelX = qBound(4,
1728 static_cast<int>(xStart) + 4,
1729 qMax(4, pw - labelRect.width() - 4));
1730 labelRect.moveTopLeft(QPoint(labelX, 4));
1731 QColor pillColor = annotation.color;
1732 pillColor.setAlpha(215);
1733 p.fillRect(labelRect, pillColor);
1734 p.setPen(Qt::white);
1735 p.drawText(labelRect, Qt::AlignCenter, label);
1741 for (
const auto& [ch, lane] : std::as_const(traces)) {
1745 QVector<float> verts = model->
decimatedVertices(ch, firstSample, lastSample, pw, vboFirst);
1746 const int nVerts =
static_cast<int>(verts.size() / 2);
1757 for (
int v = 0; v < nVerts; ++v) {
1758 const double amp =
static_cast<double>(verts[v * 2 + 1]);
1762 const double mean = sum / nVerts;
1763 const double var = sumSq / nVerts - mean * mean;
1764 offset =
static_cast<float>(mean);
1765 amplitudeMax = 4.f * (var > 0.0 ?
static_cast<float>(qSqrt(var)) : 1.f);
1768 const QPen normalPen(info.
bad ? QColor(200, 60, 60, 180) : info.
color, 1.2);
1769 const QPen clipPen(QColor(255, 0, 0), 1.6);
1770 const bool doClip = showClipping && !info.
bad && !
zScoreMode;
1771 const float yMid = (
static_cast<float>(lane) + 0.5f) * laneH;
1774 seg.reserve(nVerts);
1775 bool prevClipped =
false;
1776 for (
int v = 0; v < nVerts; ++v) {
1777 const float norm = amplitudeMax > 0.f
1778 ? qBound(-2.f, (verts[v * 2 + 1] - offset) / amplitudeMax, 2.f)
1780 const QPointF pt((
static_cast<float>(vboFirst) + verts[v * 2] -
scrollSample) / spp,
1781 yMid - norm * 0.45f * laneH);
1782 const bool clipped = doClip && qAbs(norm) >= 0.95f;
1783 if (v > 0 && clipped != prevClipped) {
1785 p.setPen(prevClipped ? clipPen : normalPen);
1786 p.drawPolyline(seg);
1790 prevClipped = clipped;
1792 if (seg.size() > 1) {
1793 p.setPen(prevClipped ? clipPen : normalPen);
1794 p.drawPolyline(seg);
1801 if (!events.isEmpty() && spp > 0.f) {
1803 float xF = (
static_cast<float>(ev.sample) -
scrollSample) / spp;
1804 if (xF < -2.f || xF > pw + 2.f)
1806 int ix =
static_cast<int>(xF);
1808 QColor lineColor = ev.color;
1809 lineColor.setAlpha(180);
1810 p.setPen(QPen(lineColor, 1));
1811 p.drawLine(ix, 0, ix, ph);
1817 if (!epochMarkers.isEmpty() && spp > 0.f) {
1818 QPen epochPen(QColor(100, 100, 100, 140), 1, Qt::DashLine);
1820 for (
int trigSample : epochMarkers) {
1821 float xF = (
static_cast<float>(trigSample) -
scrollSample) / spp;
1822 if (xF < -2.f || xF > pw + 2.f)
1824 int ix =
static_cast<int>(xF);
1825 p.drawLine(ix, 0, ix, ph);
1834void ChannelRhiView::drawOverlays()
1841 if (m_overlay && (m_crosshairEnabled || m_scalebarsVisible || m_rulerActive))
1842 m_overlay->update();
1847static QString formatAmplitude(
float amp,
const QString& unit)
1849 float absAmp = qAbs(amp);
1851 return QStringLiteral(
"0 ") + unit;
1853 return QString::number(amp * 1e12f,
'f', 1) + QStringLiteral(
" p") + unit;
1855 return QString::number(amp * 1e9f,
'f', 1) + QStringLiteral(
" n") + unit;
1857 return QString::number(amp * 1e6f,
'f', 1) + QStringLiteral(
" µ") + unit;
1859 return QString::number(amp * 1e3f,
'f', 1) + QStringLiteral(
" m") + unit;
1860 return QString::number(amp,
'f', 3) + QStringLiteral(
" ") + unit;
1863static QString unitForType(
const QString& typeLabel)
1865 if (typeLabel == QStringLiteral(
"MEG grad"))
1866 return QStringLiteral(
"T/m");
1867 if (typeLabel == QStringLiteral(
"MEG mag") || typeLabel == QStringLiteral(
"MEG"))
1868 return QStringLiteral(
"T");
1869 if (typeLabel == QStringLiteral(
"EEG") ||
1870 typeLabel == QStringLiteral(
"EOG") ||
1871 typeLabel == QStringLiteral(
"ECG") ||
1872 typeLabel == QStringLiteral(
"EMG"))
1873 return QStringLiteral(
"V");
1874 return QStringLiteral(
"AU");
1881 if (m_crosshairX < 0 || m_crosshairY < 0)
1886 const int w = width();
1887 const int h = height();
1890 QPen crossPen(QColor(80, 80, 80, 160), 1, Qt::DashLine);
1892 p.drawLine(m_crosshairX, 0, m_crosshairX, h);
1893 p.drawLine(0, m_crosshairY, w, m_crosshairY);
1895 int sample =
static_cast<int>(m_scrollSample +
static_cast<float>(m_crosshairX) * m_samplesPerPixel);
1896 float timeSec = (m_sfreq > 0.f) ?
static_cast<float>(sample - m_firstFileSample) / m_sfreq : 0.f;
1897 QString channelLabel;
1901 if (m_butterflyMode) {
1903 const auto groups = butterflyTypeGroups();
1904 int nLanes = groups.size();
1907 float laneH =
static_cast<float>(h) / nLanes;
1908 int lane = qBound(0,
static_cast<int>(m_crosshairY / laneH), nLanes - 1);
1909 channelLabel = groups[lane].typeLabel;
1910 unitStr = unitForType(groups[lane].typeLabel);
1914 int visCnt = qMin(m_visibleChannelCount, totalCh - m_firstVisibleChannel);
1918 float laneH =
static_cast<float>(h) / visCnt;
1919 int row = qBound(0,
static_cast<int>(m_crosshairY / laneH), visCnt - 1);
1920 int ch = actualChannelAt(m_firstVisibleChannel + row);
1924 auto info = m_model->channelInfo(ch);
1925 value = m_model->sampleValueAt(ch, sample);
1926 channelLabel = info.
name;
1932 if (m_useClockTime && timeSec >= 0.f) {
1933 int totalMs =
static_cast<int>(timeSec * 1000.f + 0.5f);
1934 int m = totalMs / 60000;
1935 int sec = (totalMs % 60000) / 1000;
1936 int ms = totalMs % 1000;
1937 timeStr = QString(
"%1:%2.%3")
1938 .arg(m, 2, 10, QChar(
'0'))
1939 .arg(sec, 2, 10, QChar(
'0'))
1940 .arg(ms, 3, 10, QChar(
'0'));
1942 timeStr = QString::number(
static_cast<double>(timeSec),
'f', 3) + QStringLiteral(
" s");
1944 QString label = QString(
"%1 %2 %3")
1947 formatAmplitude(value, unitStr));
1950 f.setPointSizeF(8.5);
1953 QRect labelRect = fm.boundingRect(label);
1954 int lx = m_crosshairX + 10;
1955 int ly = m_crosshairY - 10;
1956 if (lx + labelRect.width() + 8 > w)
1957 lx = m_crosshairX - labelRect.width() - 18;
1958 if (ly - labelRect.height() < 4)
1959 ly = m_crosshairY + labelRect.height() + 6;
1960 labelRect.moveTopLeft(QPoint(lx, ly - labelRect.height()));
1961 labelRect.adjust(-4, -2, 4, 2);
1962 p.fillRect(labelRect, QColor(255, 255, 255, 220));
1963 p.setPen(QColor(30, 30, 30));
1964 p.drawText(labelRect, Qt::AlignCenter, label);
1971 if (m_crosshairX < 0 || m_crosshairY < 0)
1976 const int h = height();
1977 int sample =
static_cast<int>(m_scrollSample +
static_cast<float>(m_crosshairX) * m_samplesPerPixel);
1978 float timeSec = (m_sfreq > 0.f) ?
static_cast<float>(sample - m_firstFileSample) / m_sfreq : 0.f;
1980 if (m_butterflyMode) {
1981 const auto groups = butterflyTypeGroups();
1982 int nLanes = groups.size();
1985 float laneH =
static_cast<float>(h) / nLanes;
1986 int lane = qBound(0,
static_cast<int>(m_crosshairY / laneH), nLanes - 1);
1988 groups[lane].typeLabel,
1989 unitForType(groups[lane].typeLabel));
1992 int visCnt = qMin(m_visibleChannelCount, totalCh - m_firstVisibleChannel);
1995 float laneH =
static_cast<float>(h) / visCnt;
1996 int row = qBound(0,
static_cast<int>(m_crosshairY / laneH), visCnt - 1);
1997 int ch = actualChannelAt(m_firstVisibleChannel + row);
2000 auto info = m_model->channelInfo(ch);
2001 float value = m_model->sampleValueAt(ch, sample);
2014 if (m_butterflyMode) {
2015 visCnt = butterflyLaneCount();
2018 visCnt = qMin(m_visibleChannelCount, totalCh - m_firstVisibleChannel);
2023 float laneH =
static_cast<float>(height()) / visCnt;
2026 QMap<QString, float> typeScales;
2027 if (m_butterflyMode) {
2028 const auto groups = butterflyTypeGroups();
2029 for (
const auto& g : groups)
2030 if (g.amplitudeMax > 0.f)
2031 typeScales[g.typeLabel] = g.amplitudeMax;
2033 for (
int i = 0; i < visCnt; ++i) {
2034 int ch = actualChannelAt(m_firstVisibleChannel + i);
2037 auto info = m_model->channelInfo(ch);
2043 if (typeScales.isEmpty())
2047 f.setPointSizeF(8.0);
2052 const int margin = 12;
2054 const float scalePx = laneH * 0.45f;
2055 const int barHeight = qBound(20, qRound(scalePx), 60);
2056 int x = width() - margin;
2057 int y = height() - margin;
2059 for (
auto it = typeScales.constEnd(); it != typeScales.constBegin();) {
2061 QString unit = unitForType(it.key());
2062 float ampValue = it.value() *
static_cast<float>(barHeight) / scalePx;
2063 QString label = it.key() + QStringLiteral(
": ") + formatAmplitude(ampValue, unit);
2065 int textW = fm.horizontalAdvance(label);
2066 int barX = x - textW - 14;
2069 QRect bgRect(barX - 4, y - barHeight - fm.height() - 4, textW + 22, barHeight + fm.height() + 8);
2070 p.fillRect(bgRect, QColor(255, 255, 255, 200));
2073 QPen barPen(QColor(40, 40, 40), 2);
2075 int barTop = y - barHeight;
2076 p.drawLine(barX + 4, barTop, barX + 4, y);
2078 p.drawLine(barX, barTop, barX + 8, barTop);
2079 p.drawLine(barX, y, barX + 8, y);
2082 p.setPen(QColor(30, 30, 30));
2083 p.drawText(barX + 14, y - barHeight / 2 + fm.ascent() / 2, label);
2085 y -= barHeight + fm.height() + 16;
2093 int x0 = m_rulerX0, y0 = m_rulerY0;
2094 int x1 = m_rulerX1, y1 = m_rulerY1;
2096 const bool snapH = (m_rulerSnap == RulerSnap::Horizontal);
2097 const bool snapV = (m_rulerSnap == RulerSnap::Vertical);
2099 const QColor activeColor(40, 120, 200, 220);
2100 const QColor dimColor(130, 160, 200, 120);
2107 measured = QRect(QPoint(qMin(x0, x1), 0),
2108 QPoint(qMax(x0, x1), height()));
2110 measured = QRect(QPoint(0, qMin(y0, y1)),
2111 QPoint(width(), qMax(y0, y1)));
2113 measured = QRect(QPoint(qMin(x0, x1), qMin(y0, y1)),
2114 QPoint(qMax(x0, x1), qMax(y0, y1)));
2115 p.fillRect(measured, QColor(255, 255, 255, 60));
2117 p.setPen(QPen(QColor(40, 120, 200, 80), 1));
2118 p.drawRect(measured);
2122 QPen vLinePen(snapV ? dimColor : activeColor, 1, Qt::DashLine);
2124 p.drawLine(x0, 0, x0, height());
2126 p.drawLine(x1, 0, x1, height());
2130 QPen hGuidePen(activeColor, 1, Qt::DashLine);
2131 p.setPen(hGuidePen);
2132 p.drawLine(0, y0, width(), y0);
2133 p.drawLine(0, y1, width(), y1);
2137 QPen hLinePen(snapV ? dimColor : activeColor, snapH ? 2 : 1);
2140 p.drawLine(qMin(x0, x1), y0, qMax(x0, x1), y0);
2143 QPen vSpanPen(snapH ? dimColor : activeColor, snapV ? 2 : 1);
2146 p.drawLine(x0, qMin(y0, y1), x0, qMax(y0, y1));
2150 p.setPen(QPen(activeColor, 1));
2151 p.drawLine(x0 - tickLen, y0, x0 + tickLen, y0);
2152 p.drawLine(x1 - tickLen, y0, x1 + tickLen, y0);
2155 p.setPen(QPen(activeColor, 1));
2156 p.drawLine(x0, y0 - tickLen, x0, y0 + tickLen);
2157 p.drawLine(x0, y1 - tickLen, x0, y1 + tickLen);
2161 float deltaSamples =
static_cast<float>(x1 - x0) * m_samplesPerPixel;
2162 float deltaSec = (m_sfreq > 0.f) ? deltaSamples / m_sfreq : 0.f;
2164 float deltaAmp = 0.f;
2165 QString ampUnit = QStringLiteral(
"AU");
2168 int visCnt = qMin(m_visibleChannelCount, totalCh - m_firstVisibleChannel);
2170 float laneH =
static_cast<float>(height()) / visCnt;
2171 int row = qBound(0,
static_cast<int>(y0 / laneH), visCnt - 1);
2172 int ch = actualChannelAt(m_firstVisibleChannel + row);
2175 auto info = m_model->channelInfo(ch);
2177 float dyPx =
static_cast<float>(y1 - y0);
2179 deltaAmp = -dyPx * yScale;
2186 auto fmtTime = [](
float sec) -> QString {
2187 float absSec = qAbs(sec);
2189 return QString::number(sec * 1000.f,
'f', 1) + QStringLiteral(
" ms");
2190 return QString::number(sec,
'f', 3) + QStringLiteral(
" s");
2192 auto fmtAmp = [](
float amp,
const QString& unit) -> QString {
2193 float absAmp = qAbs(amp);
2195 return QString::number(amp * 1e9f,
'f', 3) + QStringLiteral(
" n") + unit;
2197 return QString::number(amp * 1e6f,
'f', 3) + QStringLiteral(
" µ") + unit;
2199 return QString::number(amp * 1e3f,
'f', 3) + QStringLiteral(
" m") + unit;
2200 return QString::number(amp,
'f', 3) + QStringLiteral(
" ") + unit;
2203 QString timeLabel = QStringLiteral(
"\u0394T = ") + fmtTime(deltaSec);
2204 QString ampLabel = QStringLiteral(
"\u0394A = ") + fmtAmp(deltaAmp, ampUnit);
2207 f.setPointSizeF(9.0);
2214 int labelX = (x0 + x1) / 2;
2215 int labelY = y0 - 6;
2219 QRect tRect = fm.boundingRect(timeLabel);
2220 tRect.moveCenter(QPoint(labelX, labelY));
2221 tRect.adjust(-4, -2, 4, 2);
2222 p.fillRect(tRect, QColor(255, 255, 255, 210));
2223 p.setPen(QColor(20, 80, 160));
2224 p.drawText(tRect, Qt::AlignCenter, timeLabel);
2229 int aLabelX = x0 + 8;
2230 int aLabelY = (y0 + y1) / 2;
2231 QRect aRect = fm.boundingRect(ampLabel);
2232 aRect.moveCenter(QPoint(aLabelX + aRect.width() / 2, aLabelY));
2233 aRect.adjust(-4, -2, 4, 2);
2234 p.fillRect(aRect, QColor(255, 255, 255, 210));
2235 p.setPen(QColor(20, 80, 160));
2236 p.drawText(aRect, Qt::AlignCenter, ampLabel);
2244 const int x0 = qMin(m_annSelX0, m_annSelX1);
2245 const int x1 = qMax(m_annSelX0, m_annSelX1);
2246 const int h = height();
2249 p.fillRect(QRect(x0, 0, x1 - x0, h), QColor(210, 60, 60, 50));
2252 QPen borderPen(QColor(210, 60, 60, 180), 2);
2253 p.setPen(borderPen);
2254 p.drawLine(x0, 0, x0, h);
2255 p.drawLine(x1, 0, x1, h);
2258 if (m_sfreq > 0.f && m_samplesPerPixel > 0.f) {
2259 float deltaSamples =
static_cast<float>(x1 - x0) * m_samplesPerPixel;
2260 float deltaSec = deltaSamples / m_sfreq;
2263 label = QString::number(deltaSec * 1000.f,
'f', 0) + QStringLiteral(
" ms");
2265 label = QString::number(deltaSec,
'f', 2) + QStringLiteral(
" s");
2268 f.setPointSizeF(8.0);
2272 QRect labelRect = fm.boundingRect(label);
2273 labelRect.adjust(-6, -2, 6, 2);
2274 labelRect.moveTopLeft(QPoint(x0 + 4, 4));
2275 p.fillRect(labelRect, QColor(210, 60, 60, 215));
2276 p.setPen(Qt::white);
2277 p.drawText(labelRect, Qt::AlignCenter, label);
2287 QRhiWidget::resizeEvent(event);
2289 m_overlayDirty =
true;
2291 m_overlay->syncSize();
2300 const QPoint delta =
event->angleDelta();
2302 if (event->modifiers() & Qt::ControlModifier) {
2304 float factor = (delta.y() > 0) ? 0.8f : 1.25f;
2305 zoomTo(m_samplesPerPixel * factor, 150);
2307 }
else if (qAbs(delta.x()) > qAbs(delta.y())) {
2310 float step = width() * m_samplesPerPixel * 0.1f * m_scrollSpeedFactor * (delta.x() > 0 ? -1.f : 1.f);
2314 }
else if (m_wheelScrollsChannels) {
2316 int channelStep = (delta.y() > 0) ? -1 : 1;
2322 float step = width() * m_samplesPerPixel * 0.15f * m_scrollSpeedFactor * (delta.y() > 0 ? -1.f : 1.f);
2336 if (event->button() == Qt::RightButton) {
2338 stopScrollAnimations();
2340 if (m_annotationSelectionEnabled) {
2342 m_annSelecting =
true;
2343 m_annSelX0 = m_annSelX1 =
event->position().toPoint().x();
2345 m_overlay->repaint();
2348 m_rulerActive =
true;
2349 m_rulerSnap = RulerSnap::Free;
2350 m_rulerX0 = m_rulerX1 = m_rulerRawX1 =
event->position().toPoint().x();
2351 m_rulerY0 = m_rulerY1 = m_rulerRawY1 =
event->position().toPoint().y();
2353 m_overlay->repaint();
2360 (event->button() == Qt::MiddleButton ||
2361 (event->button() == Qt::LeftButton && (event->modifiers() & Qt::AltModifier)))) {
2363 m_dragStartX =
event->position().toPoint().x();
2364 m_dragStartScroll = m_scrollSample;
2368 if (event->button() == Qt::LeftButton) {
2370 stopScrollAnimations();
2373 if (m_annotationSelectionEnabled && !m_annotations.isEmpty()) {
2374 bool isStart =
false;
2375 int hitIdx = hitTestAnnotationBoundary(event->position().toPoint().x(), isStart);
2377 m_annDragging =
true;
2378 m_annDragIndex = hitIdx;
2379 m_annDragIsStart = isStart;
2387 float samplePos = m_scrollSample +
static_cast<float>(
event->position().x()) * m_samplesPerPixel;
2393 m_leftButtonDown =
true;
2394 m_leftDragActivated =
false;
2395 m_leftDownX =
event->position().toPoint().x();
2396 m_leftDownScroll = m_scrollSample;
2397 m_velocityHistory.clear();
2398 m_dragTimer.start();
2399 m_velocityHistory.append({m_leftDownX, 0});
2403 QRhiWidget::mousePressEvent(event);
2411 if (m_annDragging) {
2413 int newSample =
static_cast<int>(m_scrollSample +
static_cast<float>(
event->position().toPoint().x()) * m_samplesPerPixel);
2414 newSample = qMax(newSample, m_firstFileSample);
2415 if (m_lastFileSample >= 0)
2416 newSample = qMin(newSample, m_lastFileSample);
2418 if (m_annDragIndex >= 0 && m_annDragIndex < m_annotations.size()) {
2419 if (m_annDragIsStart)
2420 m_annotations[m_annDragIndex].startSample = newSample;
2422 m_annotations[m_annDragIndex].endSample = newSample;
2423 m_overlayDirty =
true;
2431 if (m_annSelecting) {
2432 m_annSelX1 =
event->position().toPoint().x();
2434 m_overlay->repaint();
2439 if (m_rulerActive) {
2440 m_rulerRawX1 =
event->position().toPoint().x();
2441 m_rulerRawY1 =
event->position().toPoint().y();
2445 int dx = qAbs(m_rulerRawX1 - m_rulerX0);
2446 int dy = qAbs(m_rulerRawY1 - m_rulerY0);
2447 const int kSnapThresh = 8;
2448 if (dx < kSnapThresh && dy < kSnapThresh) {
2449 m_rulerSnap = RulerSnap::Free;
2450 }
else if (dx > dy * 2) {
2451 m_rulerSnap = RulerSnap::Horizontal;
2452 }
else if (dy > dx * 2) {
2453 m_rulerSnap = RulerSnap::Vertical;
2455 m_rulerSnap = RulerSnap::Free;
2459 switch (m_rulerSnap) {
2460 case RulerSnap::Horizontal:
2461 m_rulerX1 = m_rulerRawX1;
2462 m_rulerY1 = m_rulerY0;
2464 case RulerSnap::Vertical:
2465 m_rulerX1 = m_rulerX0;
2466 m_rulerY1 = m_rulerRawY1;
2469 m_rulerX1 = m_rulerRawX1;
2470 m_rulerY1 = m_rulerRawY1;
2475 m_overlay->repaint();
2481 int dx =
event->position().toPoint().x() - m_dragStartX;
2482 float newScroll = m_dragStartScroll -
static_cast<float>(dx) * m_samplesPerPixel;
2487 if (m_leftButtonDown) {
2488 int x =
event->position().toPoint().x();
2489 int dx = x - m_leftDownX;
2490 if (!m_leftDragActivated && qAbs(dx) > 5)
2491 m_leftDragActivated =
true;
2492 if (m_leftDragActivated) {
2493 float newScroll = m_leftDownScroll -
static_cast<float>(dx) * m_samplesPerPixel;
2497 qint64 now = m_dragTimer.elapsed();
2498 m_velocityHistory.append({x, now});
2499 while (m_velocityHistory.size() > 1 &&
2500 now - m_velocityHistory.first().t > 100)
2501 m_velocityHistory.removeFirst();
2509 if (m_crosshairEnabled) {
2510 m_crosshairX =
event->position().toPoint().x();
2511 m_crosshairY =
event->position().toPoint().y();
2513 m_overlay->repaint();
2521 if (m_annotationSelectionEnabled && !m_annotations.isEmpty()) {
2522 bool isStart =
false;
2523 int hitIdx = hitTestAnnotationBoundary(event->position().toPoint().x(), isStart);
2525 if (m_annHoverIndex != hitIdx || m_annHoverIsStart != isStart) {
2526 m_annHoverIndex = hitIdx;
2527 m_annHoverIsStart = isStart;
2528 setCursor(Qt::SizeHorCursor);
2530 }
else if (m_annHoverIndex >= 0) {
2531 m_annHoverIndex = -1;
2536 QRhiWidget::mouseMoveEvent(event);
2544 if (m_annDragging && event->button() == Qt::LeftButton) {
2545 int newSample =
static_cast<int>(m_scrollSample +
static_cast<float>(
event->position().toPoint().x()) * m_samplesPerPixel);
2546 newSample = qMax(newSample, m_firstFileSample);
2547 if (m_lastFileSample >= 0)
2548 newSample = qMin(newSample, m_lastFileSample);
2551 m_annDragging =
false;
2552 m_annDragIndex = -1;
2558 if (m_annSelecting && event->button() == Qt::RightButton) {
2559 m_annSelX1 =
event->position().toPoint().x();
2560 m_annSelecting =
false;
2562 m_overlay->repaint();
2564 const int x0 = qMin(m_annSelX0, m_annSelX1);
2565 const int x1 = qMax(m_annSelX0, m_annSelX1);
2566 if (qAbs(x1 - x0) > 3) {
2567 int startSample =
static_cast<int>(m_scrollSample +
static_cast<float>(x0) * m_samplesPerPixel);
2568 int endSample =
static_cast<int>(m_scrollSample +
static_cast<float>(x1) * m_samplesPerPixel);
2570 startSample = qMax(startSample, m_firstFileSample);
2571 if (m_lastFileSample >= 0)
2572 endSample = qMin(endSample, m_lastFileSample);
2574 if (endSample >= startSample)
2581 if (m_rulerActive && event->button() == Qt::RightButton) {
2582 m_rulerRawX1 =
event->position().toPoint().x();
2583 m_rulerRawY1 =
event->position().toPoint().y();
2585 switch (m_rulerSnap) {
2586 case RulerSnap::Horizontal:
2587 m_rulerX1 = m_rulerRawX1;
2588 m_rulerY1 = m_rulerY0;
2590 case RulerSnap::Vertical:
2591 m_rulerX1 = m_rulerX0;
2592 m_rulerY1 = m_rulerRawY1;
2595 m_rulerX1 = m_rulerRawX1;
2596 m_rulerY1 = m_rulerRawY1;
2599 m_rulerActive =
false;
2601 m_overlay->repaint();
2607 if (m_dragging && (event->button() == Qt::MiddleButton || event->button() == Qt::LeftButton)) {
2612 if (event->button() == Qt::LeftButton && m_leftButtonDown) {
2613 if (!m_leftDragActivated) {
2615 float samplePos = m_leftDownScroll +
static_cast<float>(
event->position().x()) * m_samplesPerPixel;
2619 if (m_velocityHistory.size() >= 2) {
2620 auto oldest = m_velocityHistory.first();
2621 auto newest = m_velocityHistory.last();
2622 float dt =
static_cast<float>(newest.t - oldest.t);
2624 float dx =
static_cast<float>(newest.x - oldest.x);
2626 float velSampPerMs = -(dx / dt) * m_samplesPerPixel;
2627 float speed = qAbs(velSampPerMs);
2631 float durationMs = qBound(500.f, speed * 1.0f, 5000.f);
2632 float targetScroll = m_scrollSample + velSampPerMs * durationMs / 3.f;
2633 targetScroll = qMax(targetScroll,
static_cast<float>(m_firstFileSample));
2635 m_pInertialAnim =
new QPropertyAnimation(
this,
"scrollSample",
this);
2636 m_pInertialAnim->setDuration(
static_cast<int>(durationMs));
2637 m_pInertialAnim->setEasingCurve(QEasingCurve::OutCubic);
2638 m_pInertialAnim->setStartValue(m_scrollSample);
2639 m_pInertialAnim->setEndValue(targetScroll);
2640 connect(m_pInertialAnim, &QPropertyAnimation::finished,
this, [
this]() {
2641 m_pInertialAnim =
nullptr;
2643 m_pInertialAnim->start(QAbstractAnimation::DeleteWhenStopped);
2648 m_leftButtonDown =
false;
2649 m_leftDragActivated =
false;
2653 QRhiWidget::mouseReleaseEvent(event);
2660 if (!m_model || event->button() != Qt::LeftButton) {
2661 QRhiWidget::mouseDoubleClickEvent(event);
2666 int visCnt = qMin(m_visibleChannelCount, totalCh - m_firstVisibleChannel);
2670 float laneH =
static_cast<float>(height()) / visCnt;
2671 int row =
static_cast<int>(
event->position().y() / laneH);
2672 if (row >= 0 && row < visCnt) {
2673 int ch = actualChannelAt(m_firstVisibleChannel + row);
2675 auto info = m_model->channelInfo(ch);
2676 m_model->setChannelBad(ch, !info.
bad);
QRhi-based GPU-accelerated channel time-series renderer used by the modern raw browser.
2-D display widgets and visualisation helpers (charts, topography, colour maps).
Channel display metadata (read-only from the renderer's perspective).
Circular-buffer Qt model exposing a rolling window of the live FIFF stream as a table.
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
QRhi-based GPU-accelerated channel time-series renderer used by the modern raw browser.
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)
void renderFrame(QRhi *rhi, QRhiRenderTarget *target, QRhiCommandBuffer *cb)
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)
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)
QImage renderToImage(const QSize &size=QSize()) const
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 scrollBy(float deltaSamples, int durationMs=200)
void setWheelScrollsChannels(bool channelsMode)
void mouseDoubleClickEvent(QMouseEvent *event) override
Stimulus / event marker — a coloured vertical line at a given sample.
Time-span annotation overlay.